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

1"""Fluent validation chain using Railway Oriented Programming. 

2 

3Supports sequential assertions, error aggregation, alternative predicates 

4(.otherwise()), and value transformation (.then()). 

5 

6Basic usage: 

7 >>> validate(email, name="email") 

8 ... .assert_that(is_str) 

9 ... .assert_that(is_email) 

10 ... .execute() 

11 ... .raise_if_invalid() 

12 

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() 

19 

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() 

27 

28See Also: 

29 - dsl.invariants.internals.validation_chain: Architecture and type flow details 

30 

31""" 

32 

33from collections.abc import Callable 

34from typing import final 

35 

36from ocarina.dsl.invariants.internals.validation_chain import ( 

37 ValidationAssertBlock, 

38 ValidationStartBlock, 

39) 

40 

41type Predicate[T] = Callable[[T], None] 

42 

43type ValidationChainBuilder[T] = Callable[ 

44 [ValidationStartBlock[T]], ValidationAssertBlock[T] 

45] 

46 

47 

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. 

54 

55 Args: 

56 value: The value to validate. 

57 name: Name for error messages. 

58 build_chain: Function that builds the validation chain. 

59 

60 Returns: 

61 The completed validation chain. 

62 

63 """ 

64 start_block = validate(value, name=name) 

65 return build_chain(start_block) 

66 

67 

68def validate[T](value: T, *, name: str | None = None) -> ValidationStartBlock[T]: 

69 """Entry point for creating a chainable validation. 

70 

71 Args: 

72 value: The value to validate. 

73 name: Optional name for this value in error messages (e.g., "email", "age"). 

74 

75 Returns: 

76 A ValidationStartBlock ready to accept assertions. 

77 

78 Example: 

79 >>> # Simple validation 

80 >>> validate(42).assert_that(is_positive).execute().raise_if_invalid() 

81 

82 >>> # Named validation 

83 >>> validate(email, name="email") 

84 ... .assert_that(is_str) 

85 ... .assert_that(is_email) 

86 ... .execute() 

87 ... .raise_if_invalid() 

88 

89 """ 

90 return ValidationStartBlock(value, name=name) 

91 

92 

93class _BaseCustomInvariantValidator: 

94 """Base class for creating domain-specific invariant validators. 

95 

96 This provides a factory pattern for creating reusable, composable 

97 validation functions with consistent error handling. 

98 """ 

99 

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. 

107 

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. 

113 

114 Returns: 

115 The completed validation chain. 

116 

117 """ 

118 return _create_business_invariant_validator( 

119 value, name, lambda chain: build_chain(chain, value) 

120 ) 

121 

122 

123@final 

124class BusinessInvariantValidator(_BaseCustomInvariantValidator): 

125 """Factory for creating business domain invariant validators. 

126 

127 Use this for domain-specific validation logic (e.g., "user must be adult", 

128 "order total must match items"). 

129 

130 """ 

131 

132 

133@final 

134class FrameworkInvariantValidator(_BaseCustomInvariantValidator): 

135 """Factory for creating framework-level invariant validators. 

136 

137 Use this for technical/framework validation logic (e.g., "config must be valid", 

138 "test structure must be correct"). 

139 

140 """