-
-
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_by
implementedThen the specification: