Skip to content

u8slvn/sutoppu

Repository files navigation

Sutoppu

Pypi Version Python Version CI Coverage Status Project license Code Style

Sutoppu (ストップ - Japanese from English Stop) is a simple python implementation of Specification pattern.

What is Specification Pattern?

See Wikipedia.

In computer programming, the specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic. The pattern is frequently used in the context of domain-driven design.

More information: Eric Evans and Martin Fowler article about Specifications

Basic usage

Installation

$ pip install sutoppu

Usage

from sutoppu import Specification


class Fruit:
    def __init__(self, color: str, sweet: bool, bitter: bool) -> None:
        self.color = color
        self.sweet = sweet
        self.bitter = bitter


class FruitIsBitter(Specification[Fruit]):
    description = 'The given fruit must be bitter.'

    def is_satisfied_by(self, fruit: Fruit) -> bool:
        return fruit.bitter is True


class FruitIsSweet(Specification[Fruit]):
    description = 'The given fruit must be sweet.'

    def is_satisfied_by(self, fruit: Fruit) -> bool:
        return fruit.sweet is True


class FruitIsYellow(Specification[Fruit]):
    description = 'The given fruit must be yellow.'

    def is_satisfied_by(self, fruit: Fruit) -> bool:
        return fruit.color == 'yellow'
>>> lemon = Fruit(color='yellow', sweet=False, bitter=True)
>>> is_a_lemon = FruitIsYellow() & FruitIsBitter() & ~FruitIsSweet()
>>> is_a_lemon.is_satisfied_by(lemon)
True

Operators

Sutoppu uses bitwise operator overloading to provide simplified syntax.

And:

>>> my_spec = SpecificationA() & SpecificationB()

Or:

>>> my_spec = SpecificationA() | SpecificationB()

Not:

>>> my_spec = ~SpecificationA()

Lighter syntax

If you find the is_satisfied_by method inconvenient, you can alternatively call the specification as shown below.

>>> lemon = Fruit(color='yellow', sweet=False, bitter=True)
>>> is_a_lime = FruitIsGreen() & FruitIsBitter() & ~FruitIsSweet()
>>> is_a_lime(lemon)
False

Error reporting

It can be difficult to know which specification failed in complex concatenated rules. Sutoppu allows listing all the failed specifications by getting the errors attribute after use. The errors attribute is reset each time the specification is used. For each failed specification, it returns a dict with the name of the specification class as key and the description provided in the class as value. In the case where the specification failed with a not condition, the description is prefixed with Not ~.

>>> apple = Fruit(color='red', sweet=True, bitter=False)
>>> is_a_lemon = FruitIsYellow() & FruitIsBitter() & ~ FruitIsSweet()
>>> is_a_lemon.is_satisfied_by(apple)
False
>>> is_a_lemon.errors
{
    'FruitIsColored': 'The given fruit must be yellow.',
    'FruitIsBitter': 'The given fruit must be bitter.',
    'FruitIsSweet': 'Not ~ The given fruit must be sweet.'
}