A simple python implementation of Specification pattern.
['business-rules', 'ddd', 'python', 'specification', 'specification-pattern']
A simple python implementation of Specification pattern.
 Sutoppu
**Sutoppu** (ストップ - Japanese from English *Stop*) is a simple python implementation of Specification pattern.
## What is Specification Pattern?
See [Wikipedia](https://en.wikipedia.org/wiki/Specification_pattern).
> 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](https://www.martinfowler.com/apsupp/spec.pdf)
## Basic usage
### Installation
```sh
$ pip install sutoppu
```
### Usage
```python
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'
```
```python
>>> 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:
```python
>>> my_spec = SpecificationA() & SpecificationB()
```
Or:
```python
>>> my_spec = SpecificationA() | SpecificationB()
```
Not:
```python
>>> my_spec = ~SpecificationA()
```
### Lighter syntax
If you find the `is_satisfied_by` method inconvenient, you can alternatively call the specification as shown below.
```python
>>> 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 ~`.
```python
>>> 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.'
}
```