Last active
March 29, 2024 10:02
-
-
Save palankai/f73a18ce06751ab8f245 to your computer and use it in GitHub Desktop.
Python Specification Pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
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!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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()