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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE +1 -1
  4. data/README.md +28 -4
  5. data/lib/domainic/type/accessors.rb +41 -0
  6. data/lib/domainic/type/behavior/enumerable_behavior.rb +262 -0
  7. data/lib/domainic/type/behavior/numeric_behavior.rb +340 -0
  8. data/lib/domainic/type/behavior/sizable_behavior.rb +246 -0
  9. data/lib/domainic/type/behavior/string_behavior.rb +379 -0
  10. data/lib/domainic/type/behavior.rb +239 -0
  11. data/lib/domainic/type/config/registry.yml +101 -0
  12. data/lib/domainic/type/constraint/behavior.rb +342 -0
  13. data/lib/domainic/type/constraint/constraints/all_constraint.rb +81 -0
  14. data/lib/domainic/type/constraint/constraints/and_constraint.rb +105 -0
  15. data/lib/domainic/type/constraint/constraints/any_constraint.rb +83 -0
  16. data/lib/domainic/type/constraint/constraints/case_constraint.rb +104 -0
  17. data/lib/domainic/type/constraint/constraints/character_set_constraint.rb +111 -0
  18. data/lib/domainic/type/constraint/constraints/divisibility_constraint.rb +126 -0
  19. data/lib/domainic/type/constraint/constraints/emptiness_constraint.rb +69 -0
  20. data/lib/domainic/type/constraint/constraints/equality_constraint.rb +75 -0
  21. data/lib/domainic/type/constraint/constraints/finiteness_constraint.rb +123 -0
  22. data/lib/domainic/type/constraint/constraints/inclusion_constraint.rb +74 -0
  23. data/lib/domainic/type/constraint/constraints/match_pattern_constraint.rb +87 -0
  24. data/lib/domainic/type/constraint/constraints/method_presence_constraint.rb +72 -0
  25. data/lib/domainic/type/constraint/constraints/none_constraint.rb +83 -0
  26. data/lib/domainic/type/constraint/constraints/nor_constraint.rb +105 -0
  27. data/lib/domainic/type/constraint/constraints/not_constraint.rb +76 -0
  28. data/lib/domainic/type/constraint/constraints/or_constraint.rb +106 -0
  29. data/lib/domainic/type/constraint/constraints/ordering_constraint.rb +75 -0
  30. data/lib/domainic/type/constraint/constraints/parity_constraint.rb +102 -0
  31. data/lib/domainic/type/constraint/constraints/polarity_constraint.rb +147 -0
  32. data/lib/domainic/type/constraint/constraints/range_constraint.rb +135 -0
  33. data/lib/domainic/type/constraint/constraints/type_constraint.rb +110 -0
  34. data/lib/domainic/type/constraint/constraints/uniqueness_constraint.rb +69 -0
  35. data/lib/domainic/type/constraint/resolver.rb +172 -0
  36. data/lib/domainic/type/constraint/set.rb +266 -0
  37. data/lib/domainic/type/definitions.rb +364 -0
  38. data/lib/domainic/type/types/core/array_type.rb +48 -0
  39. data/lib/domainic/type/types/core/float_type.rb +39 -0
  40. data/lib/domainic/type/types/core/hash_type.rb +143 -0
  41. data/lib/domainic/type/types/core/integer_type.rb +38 -0
  42. data/lib/domainic/type/types/core/string_type.rb +51 -0
  43. data/lib/domainic/type/types/core/symbol_type.rb +51 -0
  44. data/lib/domainic/type/types/specification/anything_type.rb +22 -0
  45. data/lib/domainic/type/types/specification/duck_type.rb +55 -0
  46. data/lib/domainic/type/types/specification/enum_type.rb +26 -0
  47. data/lib/domainic/type/types/specification/union_type.rb +26 -0
  48. data/lib/domainic/type/types/specification/void_type.rb +12 -0
  49. data/lib/domainic/type.rb +7 -0
  50. data/lib/domainic-type.rb +3 -0
  51. data/sig/domainic/type/accessors.rbs +22 -0
  52. data/sig/domainic/type/behavior/enumerable_behavior.rbs +238 -0
  53. data/sig/domainic/type/behavior/numeric_behavior.rbs +299 -0
  54. data/sig/domainic/type/behavior/sizable_behavior.rbs +218 -0
  55. data/sig/domainic/type/behavior/string_behavior.rbs +315 -0
  56. data/sig/domainic/type/behavior.rbs +153 -0
  57. data/sig/domainic/type/constraint/behavior.rbs +258 -0
  58. data/sig/domainic/type/constraint/constraints/all_constraint.rbs +55 -0
  59. data/sig/domainic/type/constraint/constraints/and_constraint.rbs +72 -0
  60. data/sig/domainic/type/constraint/constraints/any_constraint.rbs +57 -0
  61. data/sig/domainic/type/constraint/constraints/case_constraint.rbs +73 -0
  62. data/sig/domainic/type/constraint/constraints/character_set_constraint.rbs +82 -0
  63. data/sig/domainic/type/constraint/constraints/divisibility_constraint.rbs +91 -0
  64. data/sig/domainic/type/constraint/constraints/emptiness_constraint.rbs +54 -0
  65. data/sig/domainic/type/constraint/constraints/equality_constraint.rbs +60 -0
  66. data/sig/domainic/type/constraint/constraints/finiteness_constraint.rbs +82 -0
  67. data/sig/domainic/type/constraint/constraints/inclusion_constraint.rbs +59 -0
  68. data/sig/domainic/type/constraint/constraints/match_pattern_constraint.rbs +66 -0
  69. data/sig/domainic/type/constraint/constraints/method_presence_constraint.rbs +51 -0
  70. data/sig/domainic/type/constraint/constraints/none_constraint.rbs +57 -0
  71. data/sig/domainic/type/constraint/constraints/nor_constraint.rbs +72 -0
  72. data/sig/domainic/type/constraint/constraints/not_constraint.rbs +56 -0
  73. data/sig/domainic/type/constraint/constraints/or_constraint.rbs +74 -0
  74. data/sig/domainic/type/constraint/constraints/ordering_constraint.rbs +60 -0
  75. data/sig/domainic/type/constraint/constraints/parity_constraint.rbs +71 -0
  76. data/sig/domainic/type/constraint/constraints/polarity_constraint.rbs +101 -0
  77. data/sig/domainic/type/constraint/constraints/range_constraint.rbs +88 -0
  78. data/sig/domainic/type/constraint/constraints/type_constraint.rbs +86 -0
  79. data/sig/domainic/type/constraint/constraints/uniqueness_constraint.rbs +54 -0
  80. data/sig/domainic/type/constraint/resolver.rbs +117 -0
  81. data/sig/domainic/type/constraint/set.rbs +159 -0
  82. data/sig/domainic/type/definitions.rbs +304 -0
  83. data/sig/domainic/type/types/core/array_type.rbs +42 -0
  84. data/sig/domainic/type/types/core/float_type.rbs +33 -0
  85. data/sig/domainic/type/types/core/hash_type.rbs +107 -0
  86. data/sig/domainic/type/types/core/integer_type.rbs +32 -0
  87. data/sig/domainic/type/types/core/string_type.rbs +45 -0
  88. data/sig/domainic/type/types/core/symbol_type.rbs +45 -0
  89. data/sig/domainic/type/types/specification/anything_type.rbs +14 -0
  90. data/sig/domainic/type/types/specification/duck_type.rbs +41 -0
  91. data/sig/domainic/type/types/specification/enum_type.rbs +14 -0
  92. data/sig/domainic/type/types/specification/union_type.rbs +14 -0
  93. data/sig/domainic/type/types/specification/void_type.rbs +8 -0
  94. data/sig/domainic/type.rbs +5 -0
  95. data/sig/domainic-type.rbs +1 -0
  96. data/sig/manifest.yaml +2 -0
  97. metadata +108 -71
@@ -0,0 +1,101 @@
1
+ constraints:
2
+ all:
3
+ constant: Domainic::Type::Constraint::AllConstraint
4
+ require_path: domainic/type/constraint/constraints/all_constraint
5
+ and:
6
+ constant: Domainic::Type::Constraint::AndConstraint
7
+ require_path: domainic/type/constraint/constraints/and_constraint
8
+ any:
9
+ constant: Domainic::Type::Constraint::AnyConstraint
10
+ require_path: domainic/type/constraint/constraints/any_constraint
11
+ case:
12
+ constant: Domainic::Type::Constraint::CaseConstraint
13
+ require_path: domainic/type/constraint/constraints/case_constraint
14
+ character_set:
15
+ constant: Domainic::Type::Constraint::CharacterSetConstraint
16
+ require_path: domainic/type/constraint/constraints/character_set_constraint
17
+ divisibility:
18
+ constant: Domainic::Type::Constraint::DivisibilityConstraint
19
+ require_path: domainic/type/constraint/constraints/divisibility_constraint
20
+ emptiness:
21
+ constant: Domainic::Type::Constraint::EmptinessConstraint
22
+ require_path: domainic/type/constraint/constraints/emptiness_constraint
23
+ equality:
24
+ constant: Domainic::Type::Constraint::EqualityConstraint
25
+ require_path: domainic/type/constraint/constraints/equality_constraint
26
+ finiteness:
27
+ constant: Domainic::Type::Constraint::FinitenessConstraint
28
+ require_path: domainic/type/constraint/constraints/finiteness_constraint
29
+ inclusion:
30
+ constant: Domainic::Type::Constraint::InclusionConstraint
31
+ require_path: domainic/type/constraint/constraints/inclusion_constraint
32
+ match_pattern:
33
+ constant: Domainic::Type::Constraint::MatchPatternConstraint
34
+ require_path: domainic/type/constraint/constraints/match_pattern_constraint
35
+ method_presence:
36
+ constant: Domainic::Type::Constraint::MethodPresenceConstraint
37
+ require_path: domainic/type/constraint/constraints/method_presence_constraint
38
+ none:
39
+ constant: Domainic::Type::Constraint::NoneConstraint
40
+ require_path: domainic/type/constraint/constraints/none_constraint
41
+ nor:
42
+ constant: Domainic::Type::Constraint::NorConstraint
43
+ require_path: domainic/type/constraint/constraints/nor_constraint
44
+ not:
45
+ constant: Domainic::Type::Constraint::NotConstraint
46
+ require_path: domainic/type/constraint/constraints/not_constraint
47
+ or:
48
+ constant: Domainic::Type::Constraint::OrConstraint
49
+ require_path: domainic/type/constraint/constraints/or_constraint
50
+ ordering:
51
+ constant: Domainic::Type::Constraint::OrderingConstraint
52
+ require_path: domainic/type/constraint/constraints/ordering_constraint
53
+ parity:
54
+ constant: Domainic::Type::Constraint::ParityConstraint
55
+ require_path: domainic/type/constraint/constraints/parity_constraint
56
+ polarity:
57
+ constant: Domainic::Type::Constraint::PolarityConstraint
58
+ require_path: domainic/type/constraint/constraints/polarity_constraint
59
+ range:
60
+ constant: Domainic::Type::Constraint::RangeConstraint
61
+ require_path: domainic/type/constraint/constraints/range_constraint
62
+ type:
63
+ constant: Domainic::Type::Constraint::TypeConstraint
64
+ require_path: domainic/type/constraint/constraints/type_constraint
65
+ uniqueness:
66
+ constant: Domainic::Type::Constraint::UniquenessConstraint
67
+ require_path: domainic/type/constraint/constraints/uniqueness_constraint
68
+ types:
69
+ array:
70
+ constant: Domainic::Type::ArrayType
71
+ require_path: domainic/type/types/core/array_type
72
+ anything:
73
+ constant: Domainic::Type::AnythingType
74
+ require_path: domainic/type/types/specification/anything_type
75
+ duck:
76
+ constant: Domainic::Type::DuckType
77
+ require_path: domainic/type/types/specification/duck_type
78
+ enum:
79
+ constant: Domainic::Type::EnumType
80
+ require_path: domainic/type/types/specification/enum_type
81
+ float:
82
+ constant: Domainic::Type::FloatType
83
+ require_path: domainic/type/types/core/float_type
84
+ hash:
85
+ constant: Domainic::Type::HashType
86
+ require_path: domainic/type/types/core/hash_type
87
+ integer:
88
+ constant: Domainic::Type::IntegerType
89
+ require_path: domainic/type/types/core/integer_type
90
+ string:
91
+ constant: Domainic::Type::StringType
92
+ require_path: domainic/type/types/core/string_type
93
+ symbol:
94
+ constant: Domainic::Type::SymbolType
95
+ require_path: domainic/type/types/core/symbol_type
96
+ union:
97
+ constant: Domainic::Type::UnionType
98
+ require_path: domainic/type/types/specification/union_type
99
+ void:
100
+ constant: Domainic::Type::VoidType
101
+ require_path: domainic/type/types/specification/void_type
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/type/accessors'
4
+
5
+ module Domainic
6
+ module Type
7
+ module Constraint
8
+ # A module providing core functionality for implementing type constraints.
9
+ #
10
+ # The Behavior module serves as the foundation for all type constraints in the Domainic::Type system.
11
+ # It provides a flexible interface for defining how values should be constrained, supporting both
12
+ # simple type checking and complex validation rules.
13
+ #
14
+ # Key features include:
15
+ # - Flexible value access through configurable accessors
16
+ # - Support for custom validation logic
17
+ # - Coercion hooks for both actual and expected values
18
+ # - Detailed failure reporting
19
+ # - Type-aware error messages
20
+ #
21
+ # @abstract Implementing classes must override {#satisfies_constraint?} to define their specific
22
+ # constraint logic.
23
+ #
24
+ # @example Implementing a basic numeric constraint
25
+ # class GreaterThanConstraint
26
+ # include Domainic::Type::Constraint::Behavior
27
+ #
28
+ # def short_description
29
+ # "greater than #{@expected}"
30
+ # end
31
+ #
32
+ # def short_violation_description
33
+ # @actual.to_s
34
+ # end
35
+ #
36
+ # protected
37
+ #
38
+ # def satisfies_constraint?
39
+ # @actual > @expected
40
+ # end
41
+ #
42
+ # def validate_expectation!(expectation)
43
+ # raise ArgumentError, 'Expected value must be numeric' unless expectation.is_a?(Numeric)
44
+ # end
45
+ # end
46
+ #
47
+ # @author {https://aaronmallen.me Aaron Allen}
48
+ # @since 0.1.0
49
+ # @rbs generic Expected < Object -- The type of @expected
50
+ # @rbs generic Actual < Object -- The type of @actual
51
+ # @rbs generic Options < Hash[Symbol, untyped] -- The type of @options
52
+ module Behavior
53
+ # @rbs!
54
+ # type options = { ?abort_on_failure: bool, ?coerce_with: Array[Proc] | Proc }
55
+
56
+ # @rbs @accessor: Type::accessor
57
+ # @rbs @actual: Actual
58
+ # @rbs @expected: Expected
59
+ # @rbs @options: options
60
+ # @rbs @quantifier_description: (String | Symbol)?
61
+ # @rbs @result: bool?
62
+
63
+ attr_reader :quantifier_description #: (String | Symbol)?
64
+
65
+ # Initialize a new constraint instance.
66
+ #
67
+ # @param accessor [Symbol] The accessor to use to retrieve the value being constrained
68
+ # @param quantifier_description [String, Symbol, nil] The description of how the constraint applies
69
+ # to elements, such as "all", "any", or "none" for collection constraints, or a specific type name
70
+ # for type constraints. Used to form natural language descriptions like "having elements of String"
71
+ # or "containing any of [1, 2, 3]"
72
+ #
73
+ # @raise [ArgumentError] if the accessor is not included in {VALID_ACCESSORS}
74
+ # @return [Behavior] A new instance of the constraint.
75
+ # @rbs (Type::accessor accessor, ?(String | Symbol)? quantifier_description) -> void
76
+ def initialize(accessor, quantifier_description = nil)
77
+ validate_accessor!(accessor)
78
+
79
+ @accessor = accessor.to_sym
80
+ @options = {}
81
+ @quantifier_description = quantifier_description
82
+ end
83
+
84
+ # Whether to abort further validation on an unsatisfied constraint.
85
+ #
86
+ # When this is true it tells the type to stop validating the value against the remaining constraints.
87
+ # This is particularly useful for fundamental type constraints where subsequent validations would
88
+ # be meaningless if the basic type check fails.
89
+ #
90
+ # @return [Boolean] Whether to abort on failure.
91
+ # @rbs () -> bool
92
+ def abort_on_failure?
93
+ @options.fetch(:abort_on_failure, false)
94
+ end
95
+
96
+ # Set the expected value to compare against.
97
+ #
98
+ # @param expectation [Object] The expected value to compare against.
99
+ #
100
+ # @raise [ArgumentError] if the expectation is invalid according to {#validate_expectation!}
101
+ # @return [self] The constraint instance.
102
+ # @rbs (untyped expectation) -> self
103
+ def expecting(expectation)
104
+ expectation = coerce_expectation(expectation)
105
+ validate_expectation!(expectation)
106
+
107
+ # @type var expectation: Expected
108
+ @expected = expectation
109
+ self
110
+ end
111
+
112
+ # Whether the constraint is a failure.
113
+ #
114
+ # @return [Boolean] `true` if the constraint is a failure, `false` otherwise.
115
+ # @rbs () -> bool
116
+ def failure?
117
+ @result == false
118
+ end
119
+ alias failed? failure?
120
+
121
+ # The full description of the constraint.
122
+ #
123
+ # @return [String, nil] The full description of the constraint.
124
+ # @rbs () -> String?
125
+ def full_description
126
+ full_description_for(short_description)
127
+ end
128
+
129
+ # The full description of the violations that caused the constraint to be unsatisfied.
130
+ #
131
+ # @return [String, nil] The full description of the constraint when it fails.
132
+ # @rbs () -> String?
133
+ def full_violation_description
134
+ full_description_for(short_violation_description)
135
+ end
136
+
137
+ # Whether the constraint is satisfied.
138
+ #
139
+ # This method orchestrates the constraint validation process by:
140
+ # 1. Accessing the value using the configured accessor
141
+ # 2. Coercing the actual value if needed
142
+ # 3. Checking if the constraint is satisfied
143
+ # 4. Handling any errors that occur during validation
144
+ #
145
+ # @param value [Object] The value to validate against the constraint.
146
+ #
147
+ # @return [Boolean] Whether the constraint is satisfied.
148
+ # @rbs (Actual value) -> bool
149
+ def satisfied?(value)
150
+ @result = nil
151
+ constrained = @accessor == :self ? value : value.public_send(@accessor)
152
+ @actual = coerce_actual(coerce_actual_for_type(constrained))
153
+ @result = satisfies_constraint? #: bool
154
+ rescue StandardError
155
+ @result = false #: bool
156
+ end
157
+
158
+ # The short description of the constraint.
159
+ #
160
+ # This is used to help compose a error message when the constraint is not satisfied.
161
+ # Implementing classes should override this to provide meaningful descriptions of their
162
+ # constraint behavior.
163
+ #
164
+ # @return [String] The description of the constraint.
165
+ # @rbs () -> String
166
+ def short_description
167
+ @expected.to_s
168
+ end
169
+
170
+ # The short description of the violations that caused the constraint to be unsatisfied.
171
+ #
172
+ # This is used to help compose a error message when the constraint is not satisfied.
173
+ # Implementing classes can override this to provide more specific failure messages.
174
+ #
175
+ # @return [String] The description of the constraint when it fails.
176
+ # @rbs () -> String
177
+ def short_violation_description
178
+ @actual.to_s
179
+ end
180
+
181
+ # Whether the constraint is a success.
182
+ #
183
+ # @return [Boolean] `true` if the constraint is a success, `false` otherwise.
184
+ # @rbs () -> bool
185
+ def successful?
186
+ @result == true
187
+ end
188
+ alias success? successful?
189
+
190
+ # Merge additional options into the constraint.
191
+ #
192
+ # @param options [Hash{String, Symbol => Object}] Additional options
193
+ # @option options [Boolean] :abort_on_failure (false) Whether to {#abort_on_failure?}
194
+ # @option options [Array<Proc>, Proc] :coerce_with Coercers to run on the value before validating the
195
+ # constraint.
196
+ #
197
+ # @return [self] The constraint instance.
198
+ # @rbs (?(options & Options) options) -> self
199
+ def with_options(options = {})
200
+ @options.merge!(options.transform_keys(&:to_sym))
201
+ self
202
+ end
203
+
204
+ protected
205
+
206
+ # Coerce the value being validated into the expected type.
207
+ #
208
+ # This hook allows implementing classes to transform the actual value before validation.
209
+ # This is particularly useful when the constraint needs to handle multiple input formats
210
+ # or needs to normalize values before comparison.
211
+ #
212
+ # @example Coerce input into an array
213
+ # def coerce_actual(actual)
214
+ # Array(actual)
215
+ # end
216
+ #
217
+ # @param actual [Object] The actual value to coerce.
218
+ #
219
+ # @return [Object] The coerced value.
220
+ # @rbs (untyped actual) -> Actual
221
+ def coerce_actual(actual)
222
+ actual
223
+ end
224
+
225
+ # Coerce actual values using type-provided coercion procs
226
+ #
227
+ # This method processes the actual value through any type-level coercion procs
228
+ # that were provided via options. This runs after the constraint's own coercion
229
+ # but before validation.
230
+ #
231
+ # @param actual [Object] The actual value to coerce
232
+ #
233
+ # @return [Object] The coerced value
234
+ # @rbs (untyped actual) -> untyped
235
+ def coerce_actual_for_type(actual)
236
+ coercers = @options[:coerce_with]
237
+ return actual if coercers.nil?
238
+
239
+ Array(coercers).reduce(actual) do |accumulator, proc|
240
+ # @type var proc: Proc
241
+ proc.call(accumulator)
242
+ end
243
+ end
244
+
245
+ # Coerce the expected value into the expected type.
246
+ #
247
+ # This hook allows implementing classes to transform or normalize the expected value
248
+ # when it's set. This is useful for handling different formats of expected values
249
+ # or combining multiple expectations.
250
+ #
251
+ # @example Coerce a range specification
252
+ # def coerce_expectation(expectation)
253
+ # case expectation
254
+ # when Range then { minimum: expectation.begin, maximum: expectation.end }
255
+ # when Hash then @expected.merge(expectation)
256
+ # else expectation
257
+ # end
258
+ # end
259
+ #
260
+ # @param expectation [Object] The expected value to coerce.
261
+ #
262
+ # @return [Object] The coerced value.
263
+ # @rbs (untyped expectation) -> Expected
264
+ def coerce_expectation(expectation)
265
+ expectation
266
+ end
267
+
268
+ # The primary implementation of the constraint.
269
+ #
270
+ # This is the core method that all constraints must implement to define their specific
271
+ # validation logic. It is called by {#satisfied?} after the value has been accessed
272
+ # and coerced.
273
+ #
274
+ # The implementing class has access to two instance variables:
275
+ # - @actual: The actual value being validated (after coercion)
276
+ # - @expected: The expected value to validate against (after coercion)
277
+ #
278
+ # @example Implementing a greater than constraint
279
+ # def satisfies_constraint?
280
+ # @actual > @expected
281
+ # end
282
+ #
283
+ # @raise [NotImplementedError] if the including class doesn't implement this method
284
+ # @return [Boolean] Whether the constraint is satisfied.
285
+ # @rbs () -> bool
286
+ def satisfies_constraint?
287
+ raise NotImplementedError
288
+ end
289
+
290
+ # Validate the expected value.
291
+ #
292
+ # This hook allows implementing classes to validate the expected value when it's set.
293
+ # Override this method when the constraint requires specific types or formats for
294
+ # the expected value.
295
+ #
296
+ # @example Validate numeric expectation
297
+ # def validate_expectation!(expectation)
298
+ # return if expectation.is_a?(Numeric)
299
+ #
300
+ # raise ArgumentError, "Expected value must be numeric, got #{expectation.class}"
301
+ # end
302
+ #
303
+ # @param expectation [Object] The expected value to validate.
304
+ #
305
+ # @return [void]
306
+ # @rbs (untyped expectation) -> void
307
+ def validate_expectation!(expectation); end
308
+
309
+ private
310
+
311
+ # Generate the full description for the corresponding short description.
312
+ #
313
+ # @param description [String] The short description to expand.
314
+ #
315
+ # @return [String] The full description.
316
+ # @rbs (String description) -> String?
317
+ def full_description_for(description)
318
+ return if quantifier_description.to_s.include?('not_described')
319
+
320
+ if quantifier_description.is_a?(Symbol)
321
+ "#{quantifier_description.to_s.split('_').join(' ')} #{description}"
322
+ else
323
+ "#{quantifier_description} #{description}"
324
+ end.strip
325
+ end
326
+
327
+ # Validate the accessor.
328
+ #
329
+ # @param accessor [Symbol] The accessor to validate.
330
+ #
331
+ # @raise [ArgumentError] if the accessor is not included in {VALID_ACCESSORS}.
332
+ # @return [void]
333
+ # @rbs (Type::accessor accessor) -> void
334
+ def validate_accessor!(accessor)
335
+ return if Type::ACCESSORS.include?(accessor)
336
+
337
+ raise ArgumentError, "Invalid accessor: #{accessor} must be one of #{Type::ACCESSORS.sort.join(', ')}"
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/type/constraint/behavior'
4
+
5
+ module Domainic
6
+ module Type
7
+ module Constraint
8
+ # A constraint that ensures all elements in an enumerable satisfy a given constraint.
9
+ #
10
+ # The AllConstraint allows applying a constraint to every element within an enumerable value,
11
+ # making it possible to validate collections where each element must meet certain criteria.
12
+ #
13
+ # Key features:
14
+ # - Validates each element against the expected constraint
15
+ # - Short-circuits on first failing element
16
+ # - Provides clear error messages about failing elements
17
+ # - Handles empty collections appropriately
18
+ #
19
+ # @example Validating array of strings
20
+ # string_constraint = StringConstraint.new(:self)
21
+ # all_strings = AllConstraint.new(:self, string_constraint)
22
+ #
23
+ # all_strings.satisfied?(['a', 'b', 'c']) # => true
24
+ # all_strings.satisfied?(['a', 1, 'c']) # => false
25
+ #
26
+ # @author {https://aaronmallen.me Aaron Allen}
27
+ # @since 0.1.0
28
+ class AllConstraint
29
+ include Behavior #[Behavior[untyped, untyped, untyped], Enumerable, {}]
30
+
31
+ # Get a description of what the constraint expects.
32
+ #
33
+ # @return [String] the constraint description
34
+ # @rbs override
35
+ def short_description
36
+ @expected.short_description
37
+ end
38
+
39
+ # The description of the violations that caused the constraint to be unsatisfied.
40
+ #
41
+ # This is used to help compose a error message when the constraint is not satisfied.
42
+ # Implementing classes can override this to provide more specific failure messages.
43
+ #
44
+ # @return [String] The description of the constraint when it fails.
45
+ # @rbs override
46
+ def short_violation_description
47
+ return 'not Enumerable' unless @actual.is_a?(Enumerable) # steep:ignore NoMethod
48
+
49
+ @actual.filter_map do |element|
50
+ next if @expected.satisfied?(element)
51
+
52
+ @expected.short_violation_description
53
+ end.uniq.join(', ')
54
+ end
55
+
56
+ protected
57
+
58
+ # Check if all elements satisfy the expected constraint.
59
+ #
60
+ # @return [Boolean] whether the constraint is satisfied
61
+ # @rbs override
62
+ def satisfies_constraint?
63
+ @actual.all? { |element| @expected.satisfied?(element) }
64
+ end
65
+
66
+ # Validate that the expectation is a valid constraint.
67
+ #
68
+ # @param expectation [Object] the expectation to validate
69
+ #
70
+ # @raise [ArgumentError] if the expectation is not a valid constraint
71
+ # @return [void]
72
+ # @rbs override
73
+ def validate_expectation!(expectation)
74
+ return if expectation.is_a?(Behavior)
75
+
76
+ raise ArgumentError, "expected a Domainic::Type::Constraint, got #{expectation.class}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/type/constraint/behavior'
4
+
5
+ module Domainic
6
+ module Type
7
+ module Constraint
8
+ # A constraint that combines multiple constraints with logical AND behavior.
9
+ #
10
+ # The AndConstraint validates that a value satisfies all of its provided constraints,
11
+ # implementing logical AND behavior. This enables validation rules like "must be both
12
+ # a string and non-empty" or "must be numeric and positive".
13
+ #
14
+ # Key features:
15
+ # - Combines multiple constraints with AND logic
16
+ # - Short-circuits on first failing constraint
17
+ # - Provides clear error messages for failing validations
18
+ # - Supports incremental constraint addition
19
+ #
20
+ # @example Validating a value is both a String and non-empty
21
+ # string_constraint = StringConstraint.new(:self)
22
+ # non_empty = LengthConstraint.new(:length, minimum: 1)
23
+ # string_and_non_empty = AndConstraint.new(:self, [string_constraint, non_empty])
24
+ #
25
+ # string_and_non_empty.satisfied?("test") # => true
26
+ # string_and_non_empty.satisfied?("") # => false
27
+ # string_and_non_empty.satisfied?(123) # => false
28
+ #
29
+ # @author {https://aaronmallen.me Aaron Allen}
30
+ # @since 0.1.0
31
+ class AndConstraint
32
+ include Behavior #[Array[Behavior[untyped, untyped, untyped]], untyped, {}]
33
+
34
+ # Get a description of what the constraint expects.
35
+ #
36
+ # @return [String] a description combining all constraint descriptions with 'and'
37
+ # @rbs override
38
+ def short_description
39
+ descriptions = @expected.map(&:short_description)
40
+ return descriptions.first if descriptions.size == 1
41
+ return '' if descriptions.empty?
42
+
43
+ *first, last = descriptions
44
+ "#{first.join(', ')} and #{last}"
45
+ end
46
+
47
+ # @rbs! def expecting: (Behavior[untyped, untyped, untyped]) -> self
48
+
49
+ # The description of the violations that caused the constraint to be unsatisfied.
50
+ #
51
+ # This method provides detailed feedback about which constraints failed,
52
+ # listing all violations that prevented validation from succeeding.
53
+ #
54
+ # @return [String] The combined violation descriptions from all constraints
55
+ # @rbs override
56
+ def short_violation_description
57
+ violations = @expected.reject { |constraint| constraint.satisfied?(@actual) }
58
+ descriptions = violations.map(&:short_violation_description)
59
+ return descriptions.first if descriptions.size == 1
60
+
61
+ *first, last = descriptions
62
+ "#{first.join(', ')} nor #{last}"
63
+ end
64
+
65
+ protected
66
+
67
+ # Coerce the expectation into an array and append new constraints.
68
+ #
69
+ # This enables both initializing with an array of constraints and adding
70
+ # new constraints incrementally via expecting().
71
+ #
72
+ # @param expectation [Behavior] the constraint to add
73
+ #
74
+ # @return [Array<Behavior>] the updated array of constraints
75
+ # @rbs (untyped expectation) -> Array[Behavior[untyped, untyped, untyped]]
76
+ def coerce_expectation(expectation)
77
+ expectation.is_a?(Array) ? (@expected || []).concat(expectation) : (@expected || []) << expectation
78
+ end
79
+
80
+ # Check if the value satisfies all expected constraints.
81
+ #
82
+ # Short-circuits on the first failing constraint for efficiency.
83
+ #
84
+ # @return [Boolean] whether all constraints are satisfied
85
+ # @rbs override
86
+ def satisfies_constraint?
87
+ @expected.all? { |constraint| constraint.satisfied?(@actual) }
88
+ end
89
+
90
+ # Validate that the expectation is an array of valid constraints.
91
+ #
92
+ # @param expectation [Object] the expectation to validate
93
+ #
94
+ # @raise [ArgumentError] if the expectation is not valid
95
+ # @return [void]
96
+ # @rbs override
97
+ def validate_expectation!(expectation)
98
+ return if expectation.is_a?(Array) && expectation.all?(Behavior)
99
+
100
+ raise ArgumentError, 'Expectation must be a Domainic::Type::Constraint'
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end