domainic-type 0.1.0.alpha.2.1.0 → 0.1.0.alpha.3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/LICENSE +1 -1
- data/README.md +28 -4
- data/lib/domainic/type/accessors.rb +41 -0
- data/lib/domainic/type/behavior/enumerable_behavior.rb +262 -0
- data/lib/domainic/type/behavior/numeric_behavior.rb +340 -0
- data/lib/domainic/type/behavior/sizable_behavior.rb +246 -0
- data/lib/domainic/type/behavior/string_behavior.rb +379 -0
- data/lib/domainic/type/behavior.rb +239 -0
- data/lib/domainic/type/config/registry.yml +101 -0
- data/lib/domainic/type/constraint/behavior.rb +342 -0
- data/lib/domainic/type/constraint/constraints/all_constraint.rb +81 -0
- data/lib/domainic/type/constraint/constraints/and_constraint.rb +105 -0
- data/lib/domainic/type/constraint/constraints/any_constraint.rb +83 -0
- data/lib/domainic/type/constraint/constraints/case_constraint.rb +104 -0
- data/lib/domainic/type/constraint/constraints/character_set_constraint.rb +111 -0
- data/lib/domainic/type/constraint/constraints/divisibility_constraint.rb +126 -0
- data/lib/domainic/type/constraint/constraints/emptiness_constraint.rb +69 -0
- data/lib/domainic/type/constraint/constraints/equality_constraint.rb +75 -0
- data/lib/domainic/type/constraint/constraints/finiteness_constraint.rb +123 -0
- data/lib/domainic/type/constraint/constraints/inclusion_constraint.rb +74 -0
- data/lib/domainic/type/constraint/constraints/match_pattern_constraint.rb +87 -0
- data/lib/domainic/type/constraint/constraints/method_presence_constraint.rb +72 -0
- data/lib/domainic/type/constraint/constraints/none_constraint.rb +83 -0
- data/lib/domainic/type/constraint/constraints/nor_constraint.rb +105 -0
- data/lib/domainic/type/constraint/constraints/not_constraint.rb +76 -0
- data/lib/domainic/type/constraint/constraints/or_constraint.rb +106 -0
- data/lib/domainic/type/constraint/constraints/ordering_constraint.rb +75 -0
- data/lib/domainic/type/constraint/constraints/parity_constraint.rb +102 -0
- data/lib/domainic/type/constraint/constraints/polarity_constraint.rb +147 -0
- data/lib/domainic/type/constraint/constraints/range_constraint.rb +135 -0
- data/lib/domainic/type/constraint/constraints/type_constraint.rb +110 -0
- data/lib/domainic/type/constraint/constraints/uniqueness_constraint.rb +69 -0
- data/lib/domainic/type/constraint/resolver.rb +172 -0
- data/lib/domainic/type/constraint/set.rb +266 -0
- data/lib/domainic/type/definitions.rb +364 -0
- data/lib/domainic/type/types/core/array_type.rb +48 -0
- data/lib/domainic/type/types/core/float_type.rb +39 -0
- data/lib/domainic/type/types/core/hash_type.rb +143 -0
- data/lib/domainic/type/types/core/integer_type.rb +38 -0
- data/lib/domainic/type/types/core/string_type.rb +51 -0
- data/lib/domainic/type/types/core/symbol_type.rb +51 -0
- data/lib/domainic/type/types/specification/anything_type.rb +22 -0
- data/lib/domainic/type/types/specification/duck_type.rb +55 -0
- data/lib/domainic/type/types/specification/enum_type.rb +26 -0
- data/lib/domainic/type/types/specification/union_type.rb +26 -0
- data/lib/domainic/type/types/specification/void_type.rb +12 -0
- data/lib/domainic/type.rb +7 -0
- data/lib/domainic-type.rb +3 -0
- data/sig/domainic/type/accessors.rbs +22 -0
- data/sig/domainic/type/behavior/enumerable_behavior.rbs +238 -0
- data/sig/domainic/type/behavior/numeric_behavior.rbs +299 -0
- data/sig/domainic/type/behavior/sizable_behavior.rbs +218 -0
- data/sig/domainic/type/behavior/string_behavior.rbs +315 -0
- data/sig/domainic/type/behavior.rbs +153 -0
- data/sig/domainic/type/constraint/behavior.rbs +258 -0
- data/sig/domainic/type/constraint/constraints/all_constraint.rbs +55 -0
- data/sig/domainic/type/constraint/constraints/and_constraint.rbs +72 -0
- data/sig/domainic/type/constraint/constraints/any_constraint.rbs +57 -0
- data/sig/domainic/type/constraint/constraints/case_constraint.rbs +73 -0
- data/sig/domainic/type/constraint/constraints/character_set_constraint.rbs +82 -0
- data/sig/domainic/type/constraint/constraints/divisibility_constraint.rbs +91 -0
- data/sig/domainic/type/constraint/constraints/emptiness_constraint.rbs +54 -0
- data/sig/domainic/type/constraint/constraints/equality_constraint.rbs +60 -0
- data/sig/domainic/type/constraint/constraints/finiteness_constraint.rbs +82 -0
- data/sig/domainic/type/constraint/constraints/inclusion_constraint.rbs +59 -0
- data/sig/domainic/type/constraint/constraints/match_pattern_constraint.rbs +66 -0
- data/sig/domainic/type/constraint/constraints/method_presence_constraint.rbs +51 -0
- data/sig/domainic/type/constraint/constraints/none_constraint.rbs +57 -0
- data/sig/domainic/type/constraint/constraints/nor_constraint.rbs +72 -0
- data/sig/domainic/type/constraint/constraints/not_constraint.rbs +56 -0
- data/sig/domainic/type/constraint/constraints/or_constraint.rbs +74 -0
- data/sig/domainic/type/constraint/constraints/ordering_constraint.rbs +60 -0
- data/sig/domainic/type/constraint/constraints/parity_constraint.rbs +71 -0
- data/sig/domainic/type/constraint/constraints/polarity_constraint.rbs +101 -0
- data/sig/domainic/type/constraint/constraints/range_constraint.rbs +88 -0
- data/sig/domainic/type/constraint/constraints/type_constraint.rbs +86 -0
- data/sig/domainic/type/constraint/constraints/uniqueness_constraint.rbs +54 -0
- data/sig/domainic/type/constraint/resolver.rbs +117 -0
- data/sig/domainic/type/constraint/set.rbs +159 -0
- data/sig/domainic/type/definitions.rbs +304 -0
- data/sig/domainic/type/types/core/array_type.rbs +42 -0
- data/sig/domainic/type/types/core/float_type.rbs +33 -0
- data/sig/domainic/type/types/core/hash_type.rbs +107 -0
- data/sig/domainic/type/types/core/integer_type.rbs +32 -0
- data/sig/domainic/type/types/core/string_type.rbs +45 -0
- data/sig/domainic/type/types/core/symbol_type.rbs +45 -0
- data/sig/domainic/type/types/specification/anything_type.rbs +14 -0
- data/sig/domainic/type/types/specification/duck_type.rbs +41 -0
- data/sig/domainic/type/types/specification/enum_type.rbs +14 -0
- data/sig/domainic/type/types/specification/union_type.rbs +14 -0
- data/sig/domainic/type/types/specification/void_type.rbs +8 -0
- data/sig/domainic/type.rbs +5 -0
- data/sig/domainic-type.rbs +1 -0
- data/sig/manifest.yaml +2 -0
- metadata +108 -71
@@ -0,0 +1,258 @@
|
|
1
|
+
module Domainic
|
2
|
+
module Type
|
3
|
+
module Constraint
|
4
|
+
# A module providing core functionality for implementing type constraints.
|
5
|
+
#
|
6
|
+
# The Behavior module serves as the foundation for all type constraints in the Domainic::Type system.
|
7
|
+
# It provides a flexible interface for defining how values should be constrained, supporting both
|
8
|
+
# simple type checking and complex validation rules.
|
9
|
+
#
|
10
|
+
# Key features include:
|
11
|
+
# - Flexible value access through configurable accessors
|
12
|
+
# - Support for custom validation logic
|
13
|
+
# - Coercion hooks for both actual and expected values
|
14
|
+
# - Detailed failure reporting
|
15
|
+
# - Type-aware error messages
|
16
|
+
#
|
17
|
+
# @abstract Implementing classes must override {#satisfies_constraint?} to define their specific
|
18
|
+
# constraint logic.
|
19
|
+
#
|
20
|
+
# @example Implementing a basic numeric constraint
|
21
|
+
# class GreaterThanConstraint
|
22
|
+
# include Domainic::Type::Constraint::Behavior
|
23
|
+
#
|
24
|
+
# def short_description
|
25
|
+
# "greater than #{@expected}"
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# def short_violation_description
|
29
|
+
# @actual.to_s
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# protected
|
33
|
+
#
|
34
|
+
# def satisfies_constraint?
|
35
|
+
# @actual > @expected
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# def validate_expectation!(expectation)
|
39
|
+
# raise ArgumentError, 'Expected value must be numeric' unless expectation.is_a?(Numeric)
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
44
|
+
# @since 0.1.0
|
45
|
+
module Behavior[Expected < Object, Actual < Object, Options < Hash[Symbol, untyped]]
|
46
|
+
type options = { ?abort_on_failure: bool, ?coerce_with: Array[Proc] | Proc }
|
47
|
+
|
48
|
+
@result: bool?
|
49
|
+
|
50
|
+
@quantifier_description: (String | Symbol)?
|
51
|
+
|
52
|
+
@options: options
|
53
|
+
|
54
|
+
@expected: Expected
|
55
|
+
|
56
|
+
@actual: Actual
|
57
|
+
|
58
|
+
@accessor: Type::accessor
|
59
|
+
|
60
|
+
attr_reader quantifier_description: (String | Symbol)?
|
61
|
+
|
62
|
+
# Initialize a new constraint instance.
|
63
|
+
#
|
64
|
+
# @param accessor [Symbol] The accessor to use to retrieve the value being constrained
|
65
|
+
# @param quantifier_description [String, Symbol, nil] The description of how the constraint applies
|
66
|
+
# to elements, such as "all", "any", or "none" for collection constraints, or a specific type name
|
67
|
+
# for type constraints. Used to form natural language descriptions like "having elements of String"
|
68
|
+
# or "containing any of [1, 2, 3]"
|
69
|
+
#
|
70
|
+
# @raise [ArgumentError] if the accessor is not included in {VALID_ACCESSORS}
|
71
|
+
# @return [Behavior] A new instance of the constraint.
|
72
|
+
def initialize: (Type::accessor accessor, ?(String | Symbol)? quantifier_description) -> void
|
73
|
+
|
74
|
+
# Whether to abort further validation on an unsatisfied constraint.
|
75
|
+
#
|
76
|
+
# When this is true it tells the type to stop validating the value against the remaining constraints.
|
77
|
+
# This is particularly useful for fundamental type constraints where subsequent validations would
|
78
|
+
# be meaningless if the basic type check fails.
|
79
|
+
#
|
80
|
+
# @return [Boolean] Whether to abort on failure.
|
81
|
+
def abort_on_failure?: () -> bool
|
82
|
+
|
83
|
+
# Set the expected value to compare against.
|
84
|
+
#
|
85
|
+
# @param expectation [Object] The expected value to compare against.
|
86
|
+
#
|
87
|
+
# @raise [ArgumentError] if the expectation is invalid according to {#validate_expectation!}
|
88
|
+
# @return [self] The constraint instance.
|
89
|
+
def expecting: (untyped expectation) -> self
|
90
|
+
|
91
|
+
# Whether the constraint is a failure.
|
92
|
+
#
|
93
|
+
# @return [Boolean] `true` if the constraint is a failure, `false` otherwise.
|
94
|
+
def failure?: () -> bool
|
95
|
+
|
96
|
+
alias failed? failure?
|
97
|
+
|
98
|
+
# The full description of the constraint.
|
99
|
+
#
|
100
|
+
# @return [String, nil] The full description of the constraint.
|
101
|
+
def full_description: () -> String?
|
102
|
+
|
103
|
+
# The full description of the violations that caused the constraint to be unsatisfied.
|
104
|
+
#
|
105
|
+
# @return [String, nil] The full description of the constraint when it fails.
|
106
|
+
def full_violation_description: () -> String?
|
107
|
+
|
108
|
+
# Whether the constraint is satisfied.
|
109
|
+
#
|
110
|
+
# This method orchestrates the constraint validation process by:
|
111
|
+
# 1. Accessing the value using the configured accessor
|
112
|
+
# 2. Coercing the actual value if needed
|
113
|
+
# 3. Checking if the constraint is satisfied
|
114
|
+
# 4. Handling any errors that occur during validation
|
115
|
+
#
|
116
|
+
# @param value [Object] The value to validate against the constraint.
|
117
|
+
#
|
118
|
+
# @return [Boolean] Whether the constraint is satisfied.
|
119
|
+
def satisfied?: (Actual value) -> bool
|
120
|
+
|
121
|
+
# The short description of the constraint.
|
122
|
+
#
|
123
|
+
# This is used to help compose a error message when the constraint is not satisfied.
|
124
|
+
# Implementing classes should override this to provide meaningful descriptions of their
|
125
|
+
# constraint behavior.
|
126
|
+
#
|
127
|
+
# @return [String] The description of the constraint.
|
128
|
+
def short_description: () -> String
|
129
|
+
|
130
|
+
# The short description of the violations that caused the constraint to be unsatisfied.
|
131
|
+
#
|
132
|
+
# This is used to help compose a error message when the constraint is not satisfied.
|
133
|
+
# Implementing classes can override this to provide more specific failure messages.
|
134
|
+
#
|
135
|
+
# @return [String] The description of the constraint when it fails.
|
136
|
+
def short_violation_description: () -> String
|
137
|
+
|
138
|
+
# Whether the constraint is a success.
|
139
|
+
#
|
140
|
+
# @return [Boolean] `true` if the constraint is a success, `false` otherwise.
|
141
|
+
def successful?: () -> bool
|
142
|
+
|
143
|
+
alias success? successful?
|
144
|
+
|
145
|
+
# Merge additional options into the constraint.
|
146
|
+
#
|
147
|
+
# @param options [Hash{String, Symbol => Object}] Additional options
|
148
|
+
# @option options [Boolean] :abort_on_failure (false) Whether to {#abort_on_failure?}
|
149
|
+
# @option options [Array<Proc>, Proc] :coerce_with Coercers to run on the value before validating the
|
150
|
+
# constraint.
|
151
|
+
#
|
152
|
+
# @return [self] The constraint instance.
|
153
|
+
def with_options: (?options & Options options) -> self
|
154
|
+
|
155
|
+
# Coerce the value being validated into the expected type.
|
156
|
+
#
|
157
|
+
# This hook allows implementing classes to transform the actual value before validation.
|
158
|
+
# This is particularly useful when the constraint needs to handle multiple input formats
|
159
|
+
# or needs to normalize values before comparison.
|
160
|
+
#
|
161
|
+
# @example Coerce input into an array
|
162
|
+
# def coerce_actual(actual)
|
163
|
+
# Array(actual)
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# @param actual [Object] The actual value to coerce.
|
167
|
+
#
|
168
|
+
# @return [Object] The coerced value.
|
169
|
+
def coerce_actual: (untyped actual) -> Actual
|
170
|
+
|
171
|
+
# Coerce actual values using type-provided coercion procs
|
172
|
+
#
|
173
|
+
# This method processes the actual value through any type-level coercion procs
|
174
|
+
# that were provided via options. This runs after the constraint's own coercion
|
175
|
+
# but before validation.
|
176
|
+
#
|
177
|
+
# @param actual [Object] The actual value to coerce
|
178
|
+
#
|
179
|
+
# @return [Object] The coerced value
|
180
|
+
def coerce_actual_for_type: (untyped actual) -> untyped
|
181
|
+
|
182
|
+
# Coerce the expected value into the expected type.
|
183
|
+
#
|
184
|
+
# This hook allows implementing classes to transform or normalize the expected value
|
185
|
+
# when it's set. This is useful for handling different formats of expected values
|
186
|
+
# or combining multiple expectations.
|
187
|
+
#
|
188
|
+
# @example Coerce a range specification
|
189
|
+
# def coerce_expectation(expectation)
|
190
|
+
# case expectation
|
191
|
+
# when Range then { minimum: expectation.begin, maximum: expectation.end }
|
192
|
+
# when Hash then @expected.merge(expectation)
|
193
|
+
# else expectation
|
194
|
+
# end
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# @param expectation [Object] The expected value to coerce.
|
198
|
+
#
|
199
|
+
# @return [Object] The coerced value.
|
200
|
+
def coerce_expectation: (untyped expectation) -> Expected
|
201
|
+
|
202
|
+
# The primary implementation of the constraint.
|
203
|
+
#
|
204
|
+
# This is the core method that all constraints must implement to define their specific
|
205
|
+
# validation logic. It is called by {#satisfied?} after the value has been accessed
|
206
|
+
# and coerced.
|
207
|
+
#
|
208
|
+
# The implementing class has access to two instance variables:
|
209
|
+
# - @actual: The actual value being validated (after coercion)
|
210
|
+
# - @expected: The expected value to validate against (after coercion)
|
211
|
+
#
|
212
|
+
# @example Implementing a greater than constraint
|
213
|
+
# def satisfies_constraint?
|
214
|
+
# @actual > @expected
|
215
|
+
# end
|
216
|
+
#
|
217
|
+
# @raise [NotImplementedError] if the including class doesn't implement this method
|
218
|
+
# @return [Boolean] Whether the constraint is satisfied.
|
219
|
+
def satisfies_constraint?: () -> bool
|
220
|
+
|
221
|
+
# Validate the expected value.
|
222
|
+
#
|
223
|
+
# This hook allows implementing classes to validate the expected value when it's set.
|
224
|
+
# Override this method when the constraint requires specific types or formats for
|
225
|
+
# the expected value.
|
226
|
+
#
|
227
|
+
# @example Validate numeric expectation
|
228
|
+
# def validate_expectation!(expectation)
|
229
|
+
# return if expectation.is_a?(Numeric)
|
230
|
+
#
|
231
|
+
# raise ArgumentError, "Expected value must be numeric, got #{expectation.class}"
|
232
|
+
# end
|
233
|
+
#
|
234
|
+
# @param expectation [Object] The expected value to validate.
|
235
|
+
#
|
236
|
+
# @return [void]
|
237
|
+
def validate_expectation!: (untyped expectation) -> void
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
# Generate the full description for the corresponding short description.
|
242
|
+
#
|
243
|
+
# @param description [String] The short description to expand.
|
244
|
+
#
|
245
|
+
# @return [String] The full description.
|
246
|
+
def full_description_for: (String description) -> String?
|
247
|
+
|
248
|
+
# Validate the accessor.
|
249
|
+
#
|
250
|
+
# @param accessor [Symbol] The accessor to validate.
|
251
|
+
#
|
252
|
+
# @raise [ArgumentError] if the accessor is not included in {VALID_ACCESSORS}.
|
253
|
+
# @return [void]
|
254
|
+
def validate_accessor!: (Type::accessor accessor) -> void
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Domainic
|
2
|
+
module Type
|
3
|
+
module Constraint
|
4
|
+
# A constraint that ensures all elements in an enumerable satisfy a given constraint.
|
5
|
+
#
|
6
|
+
# The AllConstraint allows applying a constraint to every element within an enumerable value,
|
7
|
+
# making it possible to validate collections where each element must meet certain criteria.
|
8
|
+
#
|
9
|
+
# Key features:
|
10
|
+
# - Validates each element against the expected constraint
|
11
|
+
# - Short-circuits on first failing element
|
12
|
+
# - Provides clear error messages about failing elements
|
13
|
+
# - Handles empty collections appropriately
|
14
|
+
#
|
15
|
+
# @example Validating array of strings
|
16
|
+
# string_constraint = StringConstraint.new(:self)
|
17
|
+
# all_strings = AllConstraint.new(:self, string_constraint)
|
18
|
+
#
|
19
|
+
# all_strings.satisfied?(['a', 'b', 'c']) # => true
|
20
|
+
# all_strings.satisfied?(['a', 1, 'c']) # => false
|
21
|
+
#
|
22
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
23
|
+
# @since 0.1.0
|
24
|
+
class AllConstraint
|
25
|
+
include Behavior[Behavior[untyped, untyped, untyped], Enumerable, { }]
|
26
|
+
|
27
|
+
# Get a description of what the constraint expects.
|
28
|
+
#
|
29
|
+
# @return [String] the constraint description
|
30
|
+
def short_description: ...
|
31
|
+
|
32
|
+
# The description of the violations that caused the constraint to be unsatisfied.
|
33
|
+
#
|
34
|
+
# This is used to help compose a error message when the constraint is not satisfied.
|
35
|
+
# Implementing classes can override this to provide more specific failure messages.
|
36
|
+
#
|
37
|
+
# @return [String] The description of the constraint when it fails.
|
38
|
+
def short_violation_description: ...
|
39
|
+
|
40
|
+
# Check if all elements satisfy the expected constraint.
|
41
|
+
#
|
42
|
+
# @return [Boolean] whether the constraint is satisfied
|
43
|
+
def satisfies_constraint?: ...
|
44
|
+
|
45
|
+
# Validate that the expectation is a valid constraint.
|
46
|
+
#
|
47
|
+
# @param expectation [Object] the expectation to validate
|
48
|
+
#
|
49
|
+
# @raise [ArgumentError] if the expectation is not a valid constraint
|
50
|
+
# @return [void]
|
51
|
+
def validate_expectation!: ...
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Domainic
|
2
|
+
module Type
|
3
|
+
module Constraint
|
4
|
+
# A constraint that combines multiple constraints with logical AND behavior.
|
5
|
+
#
|
6
|
+
# The AndConstraint validates that a value satisfies all of its provided constraints,
|
7
|
+
# implementing logical AND behavior. This enables validation rules like "must be both
|
8
|
+
# a string and non-empty" or "must be numeric and positive".
|
9
|
+
#
|
10
|
+
# Key features:
|
11
|
+
# - Combines multiple constraints with AND logic
|
12
|
+
# - Short-circuits on first failing constraint
|
13
|
+
# - Provides clear error messages for failing validations
|
14
|
+
# - Supports incremental constraint addition
|
15
|
+
#
|
16
|
+
# @example Validating a value is both a String and non-empty
|
17
|
+
# string_constraint = StringConstraint.new(:self)
|
18
|
+
# non_empty = LengthConstraint.new(:length, minimum: 1)
|
19
|
+
# string_and_non_empty = AndConstraint.new(:self, [string_constraint, non_empty])
|
20
|
+
#
|
21
|
+
# string_and_non_empty.satisfied?("test") # => true
|
22
|
+
# string_and_non_empty.satisfied?("") # => false
|
23
|
+
# string_and_non_empty.satisfied?(123) # => false
|
24
|
+
#
|
25
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
26
|
+
# @since 0.1.0
|
27
|
+
class AndConstraint
|
28
|
+
include Behavior[Array[Behavior[untyped, untyped, untyped]], untyped, { }]
|
29
|
+
|
30
|
+
# Get a description of what the constraint expects.
|
31
|
+
#
|
32
|
+
# @return [String] a description combining all constraint descriptions with 'and'
|
33
|
+
def short_description: ...
|
34
|
+
|
35
|
+
def expecting: (Behavior[untyped, untyped, untyped]) -> self
|
36
|
+
|
37
|
+
# The description of the violations that caused the constraint to be unsatisfied.
|
38
|
+
#
|
39
|
+
# This method provides detailed feedback about which constraints failed,
|
40
|
+
# listing all violations that prevented validation from succeeding.
|
41
|
+
#
|
42
|
+
# @return [String] The combined violation descriptions from all constraints
|
43
|
+
def short_violation_description: ...
|
44
|
+
|
45
|
+
# Coerce the expectation into an array and append new constraints.
|
46
|
+
#
|
47
|
+
# This enables both initializing with an array of constraints and adding
|
48
|
+
# new constraints incrementally via expecting().
|
49
|
+
#
|
50
|
+
# @param expectation [Behavior] the constraint to add
|
51
|
+
#
|
52
|
+
# @return [Array<Behavior>] the updated array of constraints
|
53
|
+
def coerce_expectation: (untyped expectation) -> Array[Behavior[untyped, untyped, untyped]]
|
54
|
+
|
55
|
+
# Check if the value satisfies all expected constraints.
|
56
|
+
#
|
57
|
+
# Short-circuits on the first failing constraint for efficiency.
|
58
|
+
#
|
59
|
+
# @return [Boolean] whether all constraints are satisfied
|
60
|
+
def satisfies_constraint?: ...
|
61
|
+
|
62
|
+
# Validate that the expectation is an array of valid constraints.
|
63
|
+
#
|
64
|
+
# @param expectation [Object] the expectation to validate
|
65
|
+
#
|
66
|
+
# @raise [ArgumentError] if the expectation is not valid
|
67
|
+
# @return [void]
|
68
|
+
def validate_expectation!: ...
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Domainic
|
2
|
+
module Type
|
3
|
+
module Constraint
|
4
|
+
# A constraint that ensures any element in an enumerable satisfies a given constraint.
|
5
|
+
#
|
6
|
+
# The AnyConstraint validates that at least one element in an enumerable value meets
|
7
|
+
# the provided constraint. This enables validation rules like "must contain at least
|
8
|
+
# one string" or "must have any positive number".
|
9
|
+
#
|
10
|
+
# Key features:
|
11
|
+
# - Validates enumerable elements against a constraint
|
12
|
+
# - Short-circuits on first satisfying element
|
13
|
+
# - Provides clear error messages for failing elements
|
14
|
+
# - Handles non-enumerable values gracefully
|
15
|
+
#
|
16
|
+
# @example Validating an array contains any string
|
17
|
+
# string_constraint = StringConstraint.new(:self)
|
18
|
+
# any_string = AnyConstraint.new(:self, string_constraint)
|
19
|
+
#
|
20
|
+
# any_string.satisfied?(['a', 1, 'c']) # => true
|
21
|
+
# any_string.satisfied?([1, 2, 3]) # => false
|
22
|
+
# any_string.satisfied?(nil) # => false (not enumerable)
|
23
|
+
#
|
24
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
25
|
+
# @since 0.1.0
|
26
|
+
class AnyConstraint
|
27
|
+
include Behavior[Behavior[untyped, untyped, untyped], Enumerable, { }]
|
28
|
+
|
29
|
+
# Get a description of what the constraint expects.
|
30
|
+
#
|
31
|
+
# @return [String] a description combining all constraint descriptions
|
32
|
+
def short_description: ...
|
33
|
+
|
34
|
+
# The description of the violations that caused the constraint to be unsatisfied.
|
35
|
+
#
|
36
|
+
# This method provides detailed feedback when no constraints are satisfied,
|
37
|
+
# listing all the ways in which the value failed validation.
|
38
|
+
#
|
39
|
+
# @return [String] The combined violation descriptions from all constraints
|
40
|
+
def short_violation_description: ...
|
41
|
+
|
42
|
+
# Check if the value satisfies any of the expected constraints.
|
43
|
+
#
|
44
|
+
# @return [Boolean] whether any constraint is satisfied
|
45
|
+
def satisfies_constraint?: ...
|
46
|
+
|
47
|
+
# Validate that the expectation is an array of valid constraints.
|
48
|
+
#
|
49
|
+
# @param expectation [Object] the expectation to validate
|
50
|
+
#
|
51
|
+
# @raise [ArgumentError] if the expectation is not valid
|
52
|
+
# @return [void]
|
53
|
+
def validate_expectation!: ...
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Domainic
|
2
|
+
module Type
|
3
|
+
module Constraint
|
4
|
+
# A constraint for validating string case formatting.
|
5
|
+
#
|
6
|
+
# This constraint verifies that string values conform to specific case formats:
|
7
|
+
# - upper: all characters are uppercase
|
8
|
+
# - lower: all characters are lowercase
|
9
|
+
# - mixed: contains both uppercase and lowercase characters
|
10
|
+
# - title: words are capitalized (first letter uppercase, rest lowercase)
|
11
|
+
#
|
12
|
+
# @example Uppercase validation
|
13
|
+
# constraint = CaseConstraint.new(:self, :upper)
|
14
|
+
# constraint.satisfied?("HELLO") # => true
|
15
|
+
# constraint.satisfied?("Hello") # => false
|
16
|
+
#
|
17
|
+
# @example Title case validation
|
18
|
+
# constraint = CaseConstraint.new(:self, :title)
|
19
|
+
# constraint.satisfied?("Hello World") # => true
|
20
|
+
# constraint.satisfied?("hello world") # => false
|
21
|
+
#
|
22
|
+
# @example Mixed case validation
|
23
|
+
# constraint = CaseConstraint.new(:self, :mixed)
|
24
|
+
# constraint.satisfied?("helloWORLD") # => true
|
25
|
+
# constraint.satisfied?("HELLO") # => false
|
26
|
+
#
|
27
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
28
|
+
# @since 0.1.0
|
29
|
+
class CaseConstraint
|
30
|
+
type expected = :upper | :lower | :mixed | :title
|
31
|
+
|
32
|
+
include Behavior[expected, untyped, { }]
|
33
|
+
|
34
|
+
# Valid case format options
|
35
|
+
#
|
36
|
+
# @return [Array<Symbol>] List of valid case formats
|
37
|
+
VALID_CASES: Array[expected]
|
38
|
+
|
39
|
+
# Get a human-readable description of the case requirement.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# constraint = CaseConstraint.new(:self, :upper)
|
43
|
+
# constraint.short_description # => "upper case"
|
44
|
+
#
|
45
|
+
# @return [String] A description of the case requirement
|
46
|
+
def short_description: ...
|
47
|
+
|
48
|
+
# Get a human-readable description of why case validation failed.
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# constraint = CaseConstraint.new(:self, :upper)
|
52
|
+
# constraint.satisfied?("Hello")
|
53
|
+
# constraint.short_violation_description # => "not upper case"
|
54
|
+
#
|
55
|
+
# @return [String] A description of the case mismatch
|
56
|
+
def short_violation_description: ...
|
57
|
+
|
58
|
+
# Check if the string matches the expected case format.
|
59
|
+
#
|
60
|
+
# @return [Boolean] true if the string matches the expected case
|
61
|
+
def satisfies_constraint?: ...
|
62
|
+
|
63
|
+
# Validate that the expectation is a valid case format.
|
64
|
+
#
|
65
|
+
# @param expectation [Object] The case format to validate
|
66
|
+
#
|
67
|
+
# @raise [ArgumentError] if the expectation is not a valid case format
|
68
|
+
# @return [void]
|
69
|
+
def validate_expectation!: ...
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Domainic
|
2
|
+
module Type
|
3
|
+
module Constraint
|
4
|
+
# A constraint for validating string character sets.
|
5
|
+
#
|
6
|
+
# This constraint verifies that string values contain only characters from
|
7
|
+
# specific character sets:
|
8
|
+
# - ascii: ASCII characters (0x00-0x7F)
|
9
|
+
# - alphanumeric: Letters and numbers
|
10
|
+
# - alpha: Letters only
|
11
|
+
# - numeric: Numbers only
|
12
|
+
# - printable: Visible characters and spaces
|
13
|
+
#
|
14
|
+
# @example ASCII validation
|
15
|
+
# constraint = CharacterSetConstraint.new(:self, :ascii)
|
16
|
+
# constraint.satisfied?("hello") # => true
|
17
|
+
# constraint.satisfied?("héllo") # => false
|
18
|
+
#
|
19
|
+
# @example Alphanumeric validation
|
20
|
+
# constraint = CharacterSetConstraint.new(:self, :alphanumeric)
|
21
|
+
# constraint.satisfied?("abc123") # => true
|
22
|
+
# constraint.satisfied?("abc-123") # => false
|
23
|
+
#
|
24
|
+
# @example Numeric validation
|
25
|
+
# constraint = CharacterSetConstraint.new(:self, :numeric)
|
26
|
+
# constraint.satisfied?("12345") # => true
|
27
|
+
# constraint.satisfied?("123.45") # => false
|
28
|
+
#
|
29
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
30
|
+
# @since 0.1.0
|
31
|
+
class CharacterSetConstraint
|
32
|
+
type expected = :ascii | :alphanumeric | :alpha | :numeric | :printable
|
33
|
+
|
34
|
+
include Behavior[expected, untyped, { }]
|
35
|
+
|
36
|
+
# Valid character set patterns
|
37
|
+
#
|
38
|
+
# @return [Hash{Symbol => Regexp}] Map of set names to validation patterns
|
39
|
+
VALID_SETS: Hash[expected, Regexp]
|
40
|
+
|
41
|
+
# Get a human-readable description of the character set requirement.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# constraint = CharacterSetConstraint.new(:self, :numeric)
|
45
|
+
# constraint.short_description # => "only numeric characters"
|
46
|
+
#
|
47
|
+
# @return [String] A description of the character set requirement
|
48
|
+
def short_description: ...
|
49
|
+
|
50
|
+
# Get a human-readable description of why character validation failed.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# constraint = CharacterSetConstraint.new(:self, :numeric)
|
54
|
+
# constraint.satisfied?("abc123")
|
55
|
+
# constraint.short_violation_description # => "non-numeric characters"
|
56
|
+
#
|
57
|
+
# @return [String] A description of the character set violation
|
58
|
+
def short_violation_description: ...
|
59
|
+
|
60
|
+
# Convert character set name to symbol.
|
61
|
+
#
|
62
|
+
# @param expectation [String, Symbol] The character set name
|
63
|
+
#
|
64
|
+
# @return [Symbol] The character set name as a symbol
|
65
|
+
def coerce_expectation: ...
|
66
|
+
|
67
|
+
# Check if the string contains only characters from the expected set.
|
68
|
+
#
|
69
|
+
# @return [Boolean] true if all characters match the expected set
|
70
|
+
def satisfies_constraint?: ...
|
71
|
+
|
72
|
+
# Validate that the expectation is a valid character set.
|
73
|
+
#
|
74
|
+
# @param expectation [Object] The character set to validate
|
75
|
+
#
|
76
|
+
# @raise [ArgumentError] if the expectation is not a valid character set
|
77
|
+
# @return [void]
|
78
|
+
def validate_expectation!: ...
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Domainic
|
2
|
+
module Type
|
3
|
+
module Constraint
|
4
|
+
# A constraint for validating that numeric values are divisible by a specified value.
|
5
|
+
#
|
6
|
+
# This constraint checks if one number is evenly divisible by another, allowing for
|
7
|
+
# a configurable tolerance to handle floating-point arithmetic imprecision. It can
|
8
|
+
# be used to validate properties like:
|
9
|
+
# - Even/odd numbers (divisible by 2)
|
10
|
+
# - Factors and multiples
|
11
|
+
# - Decimal place alignment (divisible by 0.1, 0.01, etc)
|
12
|
+
#
|
13
|
+
# @example Basic divisibility check
|
14
|
+
# constraint = DivisibilityConstraint.new(:self, 5)
|
15
|
+
# constraint.satisfied?(10) # => true
|
16
|
+
# constraint.satisfied?(7) # => false
|
17
|
+
#
|
18
|
+
# @example With floating point values
|
19
|
+
# constraint = DivisibilityConstraint.new(:self, 0.1)
|
20
|
+
# constraint.satisfied?(0.3) # => true
|
21
|
+
# constraint.satisfied?(0.35) # => false
|
22
|
+
#
|
23
|
+
# @example Custom tolerance
|
24
|
+
# constraint = DivisibilityConstraint.new(:self)
|
25
|
+
# constraint.expecting(3)
|
26
|
+
# constraint.with_options(tolerance: 1e-5)
|
27
|
+
#
|
28
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
29
|
+
# @since 0.1.0
|
30
|
+
class DivisibilityConstraint
|
31
|
+
type options = { ?tolerance: Numeric }
|
32
|
+
|
33
|
+
include Behavior[Numeric, Numeric, options]
|
34
|
+
|
35
|
+
# Default tolerance for floating-point arithmetic comparisons.
|
36
|
+
#
|
37
|
+
# @return [Float] The default tolerance value
|
38
|
+
DEFAULT_TOLERANCE: Float
|
39
|
+
|
40
|
+
# Get a human-readable description of the divisibility requirement.
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# constraint = DivisibilityConstraint.new(:self, 5)
|
44
|
+
# constraint.short_description # => "divisible by 5"
|
45
|
+
#
|
46
|
+
# @return [String] Description of the divisibility requirement
|
47
|
+
def short_description: ...
|
48
|
+
|
49
|
+
# Get a human-readable description of why divisibility validation failed.
|
50
|
+
#
|
51
|
+
# @example With non-numeric value
|
52
|
+
# constraint = DivisibilityConstraint.new(:self, 5)
|
53
|
+
# constraint.satisfied?("not a number")
|
54
|
+
# constraint.short_violation_description # => "not Numeric"
|
55
|
+
#
|
56
|
+
# @example With non-divisible value
|
57
|
+
# constraint = DivisibilityConstraint.new(:self, 5)
|
58
|
+
# constraint.satisfied?(7)
|
59
|
+
# constraint.short_violation_description # => "not divisible by 5"
|
60
|
+
#
|
61
|
+
# @return [String] Description of the validation failure
|
62
|
+
def short_violation_description: ...
|
63
|
+
|
64
|
+
# Check if the actual value is evenly divisible by the expected value.
|
65
|
+
#
|
66
|
+
# This method handles both integer and floating-point values, using the
|
67
|
+
# configured tolerance to account for floating-point arithmetic imprecision.
|
68
|
+
#
|
69
|
+
# @return [Boolean] true if the value is evenly divisible
|
70
|
+
def satisfies_constraint?: ...
|
71
|
+
|
72
|
+
# Validate that the expected value is a non-zero number.
|
73
|
+
#
|
74
|
+
# @param expectation [Object] The value to validate
|
75
|
+
#
|
76
|
+
# @raise [ArgumentError] if the value is not a non-zero number
|
77
|
+
# @return [void]
|
78
|
+
def validate_expectation!: ...
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Parse a value into a float, returning nil if parsing fails.
|
83
|
+
#
|
84
|
+
# @param value [Numeric] The value to parse
|
85
|
+
#
|
86
|
+
# @return [Float, nil] The parsed float value or nil if parsing failed
|
87
|
+
def parse_value: (Numeric value) -> Float?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|