-
-
Save palankai/f73a18ce06751ab8f245 to your computer and use it in GitHub Desktop.
| class Specification: | |
| def __and__(self, other): | |
| return And(self, other) | |
| def __or__(self, other): | |
| return Or(self, other) | |
| def __xor__(self, other): | |
| return Xor(self, other) | |
| def __invert__(self): | |
| return Invert(self) | |
| def is_satisfied_by(self, candidate): | |
| raise NotImplementedError() | |
| def remainder_unsatisfied_by(self, candidate): | |
| if self.is_satisfied_by(candidate): | |
| return None | |
| else: | |
| return self | |
| class CompositeSpecification(Specification): | |
| pass | |
| class MultaryCompositeSpecification(CompositeSpecification): | |
| def __init__(self, *specifications): | |
| self.specifications = specifications | |
| class And(MultaryCompositeSpecification): | |
| def __and__(self, other): | |
| if isinstance(other, And): | |
| self.specifications += other.specifications | |
| else: | |
| self.specifications += (other, ) | |
| return self | |
| def is_satisfied_by(self, candidate): | |
| satisfied = all([ | |
| specification.is_satisfied_by(candidate) | |
| for specification in self.specifications | |
| ]) | |
| return satisfied | |
| def remainder_unsatisfied_by(self, candidate): | |
| non_satisfied = [ | |
| specification | |
| for specification in self.specifications | |
| if not specification.is_satisfied_by(candidate) | |
| ] | |
| if not non_satisfied: | |
| return None | |
| if len(non_satisfied) == 1: | |
| return non_satisfied[0] | |
| if len(non_satisfied) == len(self.specifications): | |
| return self | |
| return And(*non_satisfied) | |
| class Or(MultaryCompositeSpecification): | |
| def __or__(self, other): | |
| if isinstance(other, Or): | |
| self.specifications += other.specifications | |
| else: | |
| self.specifications += (other, ) | |
| return self | |
| def is_satisfied_by(self, candidate): | |
| satisfied = any([ | |
| specification.is_satisfied_by(candidate) | |
| for specification in self.specifications | |
| ]) | |
| return satisfied | |
| class UnaryCompositeSpecification(CompositeSpecification): | |
| def __init__(self, specification): | |
| self.specification = specification | |
| class Invert(UnaryCompositeSpecification): | |
| def is_satisfied_by(self, candidate): | |
| return not self.specification.is_satisfied_by(candidate) | |
| class BinaryCompositeSpecification(CompositeSpecification): | |
| def __init__(self, left, right): | |
| self.left = left | |
| self.right = right | |
| class Xor(BinaryCompositeSpecification): | |
| def is_satisfied_by(self, candidate): | |
| return ( | |
| self.left.is_satisfied_by(candidate) ^ | |
| self.right.is_satisfied_by(candidate) | |
| ) | |
| class NullaryCompositeSpecification(CompositeSpecification): | |
| pass | |
| class TrueSpecification(NullaryCompositeSpecification): | |
| def is_satisfied_by(self, candidate): | |
| return True | |
| class FalseSpecification(NullaryCompositeSpecification): | |
| def is_satisfied_by(self, candidate): | |
| return False |
Thank @palankai, I'm searching for a specification pattern, written in python. It's really helpful for me. Great snippet!
Works well in user lists, have you been able to use this for DB queries?
specification = (UserIsActive() & FromSpecificDomain(domain="@example.com"))
results = session.query(User).filter(specification.is_satisfied_by(User)).all()
Hi @mariusvrstr,
I haven't used it for queries; I only used it for access control.
For in-memory operation, it should work out of the box.
Something like this:
def filter(collection: List, specification: Specification):
return [i for i in collection if specification.is_satisfied_by(i)]For queries, I'd try something like this:
class Query:
def __and__(self, other):
return QueryAnd(self, other)
def __or__(self, other):
return QueryOr(self, other)
class CompositeQuery(Query):
def __init__(self, *queries):
self.queries = queries
class QueryAnd(CompositeQuery):
def translate(self):
return f"({' AND '.join([q.translate() for q in self.queries])})"
class QueryOr(CompositeQuery):
def translate(self):
return f"({' OR '.join([q.translate() for q in self.queries])})"
class UserIsActive(Query):
def translate(self):
return "user.is_active = 1"
class FromSpecificDomain(Query):
def __init__(self, domain):
self.domain = domain
def translate(self):
return f"user.domain = '{self.domain}'"
class LocalUser(Query):
def translate(self):
return "user.domain = 'local'"
if __name__ == "__main__":
q = UserIsActive() & (FromSpecificDomain("example.com") | LocalUser())
print(q.translate())This prints:
(user.is_active = 1 AND (user.domain = 'example.com' OR user.domain = 'local'))
I've implemented the "same" in Rust:
https://github.com/palankai/rust_design_patterns_in_practice/blob/master/specification/src/lib.rs
It is still early stages and isn't perfect (at this time), but it should work.
(I might refactor a bit, so if you see 404, look around in the repo)
Thanks!
Given a few specification, just and only the
is_satisfied_byimplementedThen the specification: