From b9f805b2f4977e417deac705c1f0767343fb4e1d Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Tue, 3 Feb 2026 17:14:20 +0000 Subject: [PATCH] feat(validators): add composite validators Implement AllOf and AnyOf for combining multiple checks into composite validation rules. --- src/veritext/validators/composite.py | 90 ++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/veritext/validators/composite.py diff --git a/src/veritext/validators/composite.py b/src/veritext/validators/composite.py new file mode 100644 index 0000000..ab608ca --- /dev/null +++ b/src/veritext/validators/composite.py @@ -0,0 +1,90 @@ +"""Composite validators for combining multiple checks.""" + +from veritext.core.types import CheckResult, ValidationContext, ValidationResult +from veritext.validators.base import Check + + +class AllOf: + """Passes only if all checks pass.""" + + def __init__(self, checks: list[Check]) -> None: + """ + Initialise the AllOf composite validator. + + Args: + checks: List of checks that must all pass. + + Raises: + ValueError: If checks list is empty. + """ + if not checks: + raise ValueError("checks list cannot be empty") + + self._checks = checks + + @property + def name(self) -> str: + """Return the name of this composite check.""" + return "all_of" + + def check(self, text: str, context: ValidationContext) -> ValidationResult: + """ + Run all checks and return aggregate result. + + Args: + text: The text to validate. + context: Validation context containing reference text and metadata. + + Returns: + ValidationResult that passes only if all checks pass. + """ + results: list[CheckResult] = [] + for check in self._checks: + results.append(check.check(text, context)) + + all_passed = all(r.passed for r in results) + + return ValidationResult(passed=all_passed, checks=results) + + +class AnyOf: + """Passes if any check passes.""" + + def __init__(self, checks: list[Check]) -> None: + """ + Initialise the AnyOf composite validator. + + Args: + checks: List of checks where at least one must pass. + + Raises: + ValueError: If checks list is empty. + """ + if not checks: + raise ValueError("checks list cannot be empty") + + self._checks = checks + + @property + def name(self) -> str: + """Return the name of this composite check.""" + return "any_of" + + def check(self, text: str, context: ValidationContext) -> ValidationResult: + """ + Run all checks and return aggregate result. + + Args: + text: The text to validate. + context: Validation context containing reference text and metadata. + + Returns: + ValidationResult that passes if any check passes. + """ + results: list[CheckResult] = [] + for check in self._checks: + results.append(check.check(text, context)) + + any_passed = any(r.passed for r in results) + + return ValidationResult(passed=any_passed, checks=results)