Source code for valid8r.core.combinators

"""Combinators for creating complex validation rules.

This module provides functions to combine validators using logical operations like AND, OR, and NOT.
These combinators allow for creation of complex validation chains.
"""

from __future__ import annotations

from typing import (
    TYPE_CHECKING,
    TypeVar,
)

from valid8r.core.maybe import (
    Failure,
    Maybe,
    Success,
)

if TYPE_CHECKING:
    from collections.abc import (
        Callable,
        Sequence,
    )

    from valid8r.core.errors import ValidationError

[docs] T = TypeVar('T')
[docs] def and_then(first: Callable[[T], Maybe[T]], second: Callable[[T], Maybe[T]]) -> Callable[[T], Maybe[T]]: """Combine two validators with logical AND (both must succeed). Args: first: The first validator function second: The second validator function Returns: A new validator function that passes only if both validators pass """ def combined_validator(value: T) -> Maybe[T]: result = first(value) match result: case Success(value): return second(value) case _: return result return combined_validator
[docs] def or_else(first: Callable[[T], Maybe[T]], second: Callable[[T], Maybe[T]]) -> Callable[[T], Maybe[T]]: """Combine two validators with logical OR (either can succeed). Args: first: The first validator function second: The second validator function Returns: A new validator function that passes if either validator passes """ def combined_validator(value: T) -> Maybe[T]: result = first(value) match result: case Success(value): return result case _: return second(value) return combined_validator
[docs] def not_validator(validator: Callable[[T], Maybe[T]], error_message: str) -> Callable[[T], Maybe[T]]: """Negate a validator (success becomes failure and vice versa). Args: validator: The validator function to negate error_message: Error message for when the negated validator fails Returns: A new validator function that passes if the original validator fails """ def negated_validator(value: T) -> Maybe[T]: result = validator(value) if result.is_failure(): return Maybe.success(value) return Maybe.failure(error_message) return negated_validator
[docs] def validate_all( value: str, parser: Callable[[str], Maybe[T]], validators: Sequence[Callable[[T], Maybe[T]]], ) -> Maybe[T]: """Parse and validate a value, collecting ALL validation errors. Unlike bind() which stops at the first failure, validate_all() runs all validators and collects every error. This is useful for form validation where users need to see all problems at once. Args: value: The string value to parse and validate parser: A parser function that converts the string to type T validators: A sequence of validator functions to apply to the parsed value Returns: Success containing the validated value if all validators pass, or Failure containing a list of ValidationErrors if any validators fail. If the parser fails, returns the parser's Failure immediately. Examples: Collect all validation errors: >>> from valid8r.core.parsers import parse_int >>> from valid8r.core.validators import minimum, maximum, predicate >>> is_positive = minimum(0, 'Must be positive') >>> is_even = predicate(lambda x: x % 2 == 0, 'Must be even') >>> result = validate_all('-5', parse_int, [is_positive, is_even]) >>> result.is_failure() True Success when all validators pass: >>> result = validate_all('42', parse_int, [is_positive, is_even]) >>> result.is_success() True >>> result.value_or(0) 42 """ parse_result = parser(value) if parse_result.is_failure(): return parse_result parsed_value = parse_result.value_or(None) # type: ignore[arg-type] if parsed_value is None: return parse_result if not validators: return parse_result errors: list[ValidationError] = [] for validator in validators: result = validator(parsed_value) if result.is_failure() and isinstance(result, Failure): errors.append(result.validation_error) if errors: return Failure(errors) # type: ignore[arg-type] return Maybe.success(parsed_value)