Coverage for src / ocarina / dsl / invariants / validate.py: 77.78%
18 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-04 16:22 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-04 16:22 +0200
1"""Fluent validation chain using Railway Oriented Programming.
3Supports sequential assertions, error aggregation, alternative predicates
4(.otherwise()), and value transformation (.then()).
6Basic usage:
7 >>> validate(email, name="email")
8 ... .assert_that(is_str)
9 ... .assert_that(is_email)
10 ... .execute()
11 ... .raise_if_invalid()
13With alternatives:
14 >>> validate(age, name="age")
15 ... .assert_that(is_equal_to(18), msg="Must be 18 or at most 65")
16 ... .otherwise(is_less_than_or_equal_to(65), msg="Must be 18 or at most 65")
17 ... .execute()
18 ... .raise_if_invalid()
20With chained values:
21 >>> validate(user, name="user")
22 ... .assert_that(is_not_none)
23 ... .then(user.email, name="user.email")
24 ... .assert_that(is_email)
25 ... .execute()
26 ... .raise_if_invalid()
28See Also:
29 - dsl.invariants.internals.validation_chain: Architecture and type flow details
31"""
33from collections.abc import Callable
34from typing import final
36from ocarina.dsl.invariants.internals.validation_chain import (
37 ValidationAssertBlock,
38 ValidationStartBlock,
39)
41type Predicate[T] = Callable[[T], None]
43type ValidationChainBuilder[T] = Callable[
44 [ValidationStartBlock[T]], ValidationAssertBlock[T]
45]
48def _create_business_invariant_validator[T](
49 value: T,
50 name: str,
51 build_chain: ValidationChainBuilder[T],
52) -> ValidationAssertBlock[T]:
53 """Create custom invariant validators.
55 Args:
56 value: The value to validate.
57 name: Name for error messages.
58 build_chain: Function that builds the validation chain.
60 Returns:
61 The completed validation chain.
63 """
64 start_block = validate(value, name=name)
65 return build_chain(start_block)
68def validate[T](value: T, *, name: str | None = None) -> ValidationStartBlock[T]:
69 """Entry point for creating a chainable validation.
71 Args:
72 value: The value to validate.
73 name: Optional name for this value in error messages (e.g., "email", "age").
75 Returns:
76 A ValidationStartBlock ready to accept assertions.
78 Example:
79 >>> # Simple validation
80 >>> validate(42).assert_that(is_positive).execute().raise_if_invalid()
82 >>> # Named validation
83 >>> validate(email, name="email")
84 ... .assert_that(is_str)
85 ... .assert_that(is_email)
86 ... .execute()
87 ... .raise_if_invalid()
89 """
90 return ValidationStartBlock(value, name=name)
93class _BaseCustomInvariantValidator:
94 """Base class for creating domain-specific invariant validators.
96 This provides a factory pattern for creating reusable, composable
97 validation functions with consistent error handling.
98 """
100 @staticmethod
101 def create[T](
102 value: T,
103 name: str,
104 build_chain: Callable[[ValidationStartBlock[T], T], ValidationAssertBlock[T]],
105 ) -> ValidationAssertBlock[T]:
106 """Create a custom invariant validator.
108 Args:
109 value: The value to validate.
110 name: Name for error messages.
111 build_chain: Function that builds the val···idation chain, receiving
112 both the start block and the value.
114 Returns:
115 The completed validation chain.
117 """
118 return _create_business_invariant_validator(
119 value, name, lambda chain: build_chain(chain, value)
120 )
123@final
124class BusinessInvariantValidator(_BaseCustomInvariantValidator):
125 """Factory for creating business domain invariant validators.
127 Use this for domain-specific validation logic (e.g., "user must be adult",
128 "order total must match items").
130 """
133@final
134class FrameworkInvariantValidator(_BaseCustomInvariantValidator):
135 """Factory for creating framework-level invariant validators.
137 Use this for technical/framework validation logic (e.g., "config must be valid",
138 "test structure must be correct").
140 """