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,74 @@
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 for validating that a collection includes a specific value.
9
+ #
10
+ # This constraint verifies that a collection contains an expected value by using
11
+ # the collection's #include? method. It works with any object that responds to
12
+ # #include?, such as Arrays, Sets, Strings, and Ranges.
13
+ #
14
+ # @example Array inclusion
15
+ # constraint = InclusionConstraint.new(:self, 2)
16
+ # constraint.satisfied?([1, 2, 3]) # => true
17
+ # constraint.satisfied?([1, 3, 4]) # => false
18
+ #
19
+ # @example String inclusion
20
+ # constraint = InclusionConstraint.new(:self, 'b')
21
+ # constraint.satisfied?('abc') # => true
22
+ # constraint.satisfied?('ac') # => false
23
+ #
24
+ # @example Range inclusion
25
+ # constraint = InclusionConstraint.new(:self, 5)
26
+ # constraint.satisfied?(1..10) # => true
27
+ # constraint.satisfied?(11..20) # => false
28
+ #
29
+ # @author {https://aaronmallen.me Aaron Allen}
30
+ # @since 0.1.0
31
+ class InclusionConstraint
32
+ include Behavior #[untyped, Enumerable[untyped], {}]
33
+
34
+ # Get a human-readable description of the inclusion requirement.
35
+ #
36
+ # @example
37
+ # constraint = InclusionConstraint.new(:self, 42)
38
+ # constraint.short_description # => "including 42"
39
+ #
40
+ # @return [String] A description of the inclusion requirement
41
+ # @rbs override
42
+ def short_description
43
+ "including #{@expected.inspect}"
44
+ end
45
+
46
+ # Get a human-readable description of why inclusion validation failed.
47
+ #
48
+ # @example
49
+ # constraint = InclusionConstraint.new(:self, 42)
50
+ # constraint.satisfied?([1, 2, 3])
51
+ # constraint.short_violation_description # => 'excluding 42'
52
+ #
53
+ # @return [String] A description of which value was missing
54
+ # @rbs override
55
+ def short_violation_description
56
+ "excluding #{@expected.inspect}"
57
+ end
58
+
59
+ protected
60
+
61
+ # Check if the collection includes the expected value.
62
+ #
63
+ # Uses the collection's #include? method to verify that the expected
64
+ # value is present in the collection being validated.
65
+ #
66
+ # @return [Boolean] true if the collection includes the expected value
67
+ # @rbs override
68
+ def satisfies_constraint?
69
+ @actual.include?(@expected)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,87 @@
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 for validating that strings match a given pattern.
9
+ #
10
+ # This constraint verifies that a string value matches a specified regular expression
11
+ # pattern. It supports both Regexp objects and string patterns that are converted to
12
+ # regular expressions.
13
+ #
14
+ # @example Basic pattern matching
15
+ # constraint = MatchPatternConstraint.new(:self, /\A\d+\z/)
16
+ # constraint.satisfied?("123") # => true
17
+ # constraint.satisfied?("abc") # => false
18
+ #
19
+ # @example String pattern conversion
20
+ # constraint = MatchPatternConstraint.new(:self, '\A\w+@\w+\.\w+\z')
21
+ # constraint.satisfied?("test@example.com") # => true
22
+ # constraint.satisfied?("invalid-email") # => false
23
+ #
24
+ # @author {https://aaronmallen.me Aaron Allen}
25
+ # @since 0.1.0
26
+ class MatchPatternConstraint
27
+ include Behavior #[Regexp, untyped, {}]
28
+
29
+ # Get a human-readable description of the pattern requirement.
30
+ #
31
+ # @example
32
+ # constraint = MatchPatternConstraint.new(:self, /\d+/)
33
+ # constraint.short_description # => "matches /\\d+/"
34
+ #
35
+ # @return [String] A description of the pattern requirement
36
+ # @rbs override
37
+ def short_description
38
+ "matching #{@expected.inspect}"
39
+ end
40
+
41
+ # Get a human-readable description of why pattern validation failed.
42
+ #
43
+ # @example
44
+ # constraint = MatchPatternConstraint.new(:self, /\d+/)
45
+ # constraint.satisfied?("abc")
46
+ # constraint.short_violation_description # => "does not match /\\d+/"
47
+ #
48
+ # @return [String] A description of the pattern mismatch
49
+ # @rbs override
50
+ def short_violation_description
51
+ "does not match #{@expected.inspect}"
52
+ end
53
+
54
+ protected
55
+
56
+ # Coerce string patterns into Regexp objects.
57
+ #
58
+ # @param expectation [String, Regexp] The pattern to coerce
59
+ #
60
+ # @return [Regexp] The coerced pattern
61
+ # @rbs override
62
+ def coerce_expectation(expectation)
63
+ expectation.is_a?(String) ? Regexp.new(expectation) : expectation #: Expected
64
+ end
65
+
66
+ # Check if the value matches the expected pattern.
67
+ #
68
+ # @return [Boolean] true if the value matches the pattern
69
+ # @rbs override
70
+ def satisfies_constraint?
71
+ @expected.match?(@actual)
72
+ end
73
+
74
+ # Validate that the expectation is a valid regular expression.
75
+ #
76
+ # @param expectation [Object] The pattern to validate
77
+ #
78
+ # @raise [ArgumentError] if the expectation is not a Regexp
79
+ # @return [void]
80
+ # @rbs override
81
+ def validate_expectation!(expectation)
82
+ raise ArgumentError, 'expectation must be a Regexp' unless expectation.is_a?(Regexp)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,72 @@
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 validates whether an object responds to a specified method
9
+ #
10
+ # This constraint checks if an object implements a particular interface by verifying
11
+ # it responds to a given method name. The method name must be provided as a Symbol.
12
+ #
13
+ # @example
14
+ # constraint = MethodPresenceConstraint.new(:to_s)
15
+ # constraint.satisfied_by?(Object.new) # => true
16
+ # constraint.satisfied_by?(BasicObject.new) # => false
17
+ #
18
+ # @author {https://aaronmallen.me Aaron Allen}
19
+ # @since 0.1.0
20
+ class MethodPresenceConstraint
21
+ include Behavior #[Symbol, untyped, {}]
22
+
23
+ # Get a short description of what this constraint expects
24
+ #
25
+ # @return [String] description of the expected method
26
+ # @rbs override
27
+ def short_description
28
+ "responding to #{@expected}"
29
+ end
30
+
31
+ # Get a short description of why the constraint was violated
32
+ #
33
+ # @return [String] description of the missing method
34
+ # @rbs override
35
+ def short_violation_description
36
+ "not responding to #{@expected}"
37
+ end
38
+
39
+ protected
40
+
41
+ # Coerce the expectation into a symbol
42
+ #
43
+ # @param expectation [Symbol] the method name to check
44
+ #
45
+ # @return [Symbol] coerced method name
46
+ # @rbs override
47
+ def coerce_expectation(expectation)
48
+ expectation.to_sym
49
+ end
50
+
51
+ # Check if the actual value satisfies the constraint
52
+ #
53
+ # @return [Boolean] true if the object responds to the expected method
54
+ # @rbs override
55
+ def satisfies_constraint?
56
+ @actual.respond_to?(@expected)
57
+ end
58
+
59
+ # Validate that the expectation is a Symbol
60
+ #
61
+ # @param expectation [Object] the expectation to validate
62
+ #
63
+ # @raise [ArgumentError] if the expectation is not a Symbol
64
+ # @return [void]
65
+ # @rbs override
66
+ def validate_expectation!(expectation)
67
+ raise ArgumentError, 'Expectation must be a Symbol' unless expectation.is_a?(Symbol)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,83 @@
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 no element in an enumerable satisfies a given constraint.
9
+ #
10
+ # The NoneConstraint validates that none of the elements in an enumerable value meet
11
+ # the provided constraint. This enables validation rules like "must not contain any
12
+ # strings" or "must have no negative numbers".
13
+ #
14
+ # Key features:
15
+ # - Validates enumerable elements against a constraint
16
+ # - Short-circuits on first violating element
17
+ # - Provides clear error messages for violating elements
18
+ # - Handles non-enumerable values gracefully
19
+ #
20
+ # @example Validating an array contains no strings
21
+ # string_constraint = StringConstraint.new(:self)
22
+ # no_strings = NoneConstraint.new(:self, string_constraint)
23
+ #
24
+ # no_strings.satisfied?([1, 2, 3]) # => true
25
+ # no_strings.satisfied?(['a', 1, 'c']) # => false
26
+ # no_strings.satisfied?(nil) # => false (not enumerable)
27
+ #
28
+ # @author {https://aaronmallen.me Aaron Allen}
29
+ # @since 0.1.0
30
+ class NoneConstraint
31
+ include Behavior #[Behavior[untyped, untyped, untyped], Enumerable, {}]
32
+
33
+ # Get a description of what the constraint expects.
34
+ #
35
+ # @return [String] a description negating the expected constraint description
36
+ # @rbs override
37
+ def short_description
38
+ "not #{@expected.short_description}"
39
+ end
40
+
41
+ # The description of the violations that caused the constraint to be unsatisfied.
42
+ #
43
+ # This method provides detailed feedback when any constraints are satisfied,
44
+ # listing all the ways in which the value failed validation.
45
+ #
46
+ # @return [String] The combined violation descriptions from all violating elements
47
+ # @rbs override
48
+ def short_violation_description
49
+ return 'not Enumerable' unless @actual.is_a?(Enumerable) # steep:ignore NoMethod
50
+
51
+ @actual.filter_map do |element|
52
+ next unless @expected.satisfied?(element)
53
+
54
+ @expected.short_description
55
+ end.uniq.join(', ')
56
+ end
57
+
58
+ protected
59
+
60
+ # Check if the value satisfies none of the expected constraints.
61
+ #
62
+ # @return [Boolean] whether no constraints are satisfied
63
+ # @rbs override
64
+ def satisfies_constraint?
65
+ @actual.none? { |element| @expected.satisfied?(element) }
66
+ end
67
+
68
+ # Validate that the expectation is a valid constraint.
69
+ #
70
+ # @param expectation [Object] the expectation to validate
71
+ #
72
+ # @raise [ArgumentError] if the expectation is not a valid Domainic::Type::Constraint
73
+ # @return [void]
74
+ # @rbs override
75
+ def validate_expectation!(expectation)
76
+ return if expectation.is_a?(Behavior)
77
+
78
+ raise ArgumentError, "expected a Domainic::Type::Constraint, got #{expectation.class}"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ 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 ensures none of the provided constraints are satisfied.
9
+ #
10
+ # The NorConstraint validates that none of the elements in an enumerable value meet
11
+ # any of the provided constraints. This enables validation rules like "must not contain
12
+ # any strings or negative numbers".
13
+ #
14
+ # Key features:
15
+ # - Validates enumerable elements against multiple constraints with NOR logic
16
+ # - Short-circuits on first satisfying constraint for performance
17
+ # - Provides clear error messages for violating constraints
18
+ # - Handles non-enumerable values gracefully
19
+ # - Supports incremental constraint addition
20
+ #
21
+ # @example Validating an array contains no strings or negative numbers
22
+ # string_constraint = StringConstraint.new(:self)
23
+ # negative_number_constraint = NegativeNumberConstraint.new(:self)
24
+ # no_strings_or_negatives = NorConstraint.new(:self, [string_constraint, negative_number_constraint])
25
+ #
26
+ # no_strings_or_negatives.satisfied?([1, 2, 3]) # => true
27
+ # no_strings_or_negatives.satisfied?(['a', 1, 'c']) # => false
28
+ # no_strings_or_negatives.satisfied?([1, -2, 3]) # => false
29
+ # no_strings_or_negatives.satisfied?(nil) # => false (not enumerable)
30
+ #
31
+ # @author {https://aaronmallen.me Aaron Allen}
32
+ # @since 0.1.0
33
+ class NorConstraint
34
+ include Behavior #[Array[Behavior[untyped, untyped, untyped]], untyped, {}]
35
+
36
+ # Get a description of what the constraint expects.
37
+ #
38
+ # @return [String] a description combining all constraint descriptions with 'nor'
39
+ # @rbs override
40
+ def short_description
41
+ descriptions = @expected.map(&:short_description)
42
+ return descriptions.first if descriptions.size == 1
43
+
44
+ *first, last = descriptions
45
+ "#{first.join(', ')} nor #{last}"
46
+ end
47
+
48
+ # The description of the violations that caused the constraint to be unsatisfied.
49
+ #
50
+ # This method provides detailed feedback about which constraints were satisfied,
51
+ # listing all violations that caused the validation to fail.
52
+ #
53
+ # @return [String] The combined violation descriptions from all satisfied constraints
54
+ # @rbs override
55
+ def short_violation_description
56
+ violations = @expected.select { |constraint| constraint.satisfied?(@actual) }
57
+ descriptions = violations.map(&:short_violation_description)
58
+ return descriptions.first if descriptions.size == 1
59
+ return '' if descriptions.empty?
60
+
61
+ *first, last = descriptions
62
+ "#{first.join(', ')} and #{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 none of the values satisfy any of the expected constraints.
81
+ #
82
+ # Short-circuits on the first satisfying constraint for efficiency.
83
+ #
84
+ # @return [Boolean] whether none of the constraints are satisfied
85
+ # @rbs override
86
+ def satisfies_constraint?
87
+ @expected.none? { |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 a valid Domainic::Type::Constraint
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
@@ -0,0 +1,76 @@
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 negates another constraint's validation logic.
9
+ #
10
+ # The NotConstraint inverts the validation result of its inner constraint,
11
+ # allowing for negative validation rules. This enables expressing conditions
12
+ # like "not equal to", "not included in", etc.
13
+ #
14
+ # Key features:
15
+ # - Inverts any constraint's validation logic
16
+ # - Maintains clear error messages with proper negation
17
+ # - Can be composed with other logical constraints
18
+ # - Preserves the original constraint's type safety
19
+ #
20
+ # @example Validating a value is not a specific type
21
+ # string_constraint = StringConstraint.new(:self)
22
+ # not_string = NotConstraint.new(:self, string_constraint)
23
+ #
24
+ # not_string.satisfied?(123) # => true
25
+ # not_string.satisfied?("test") # => false
26
+ #
27
+ # @author {https://aaronmallen.me Aaron Allen}
28
+ # @since 0.1.0
29
+ class NotConstraint
30
+ include Behavior #[Behavior[untyped, untyped, untyped], {}, {}]
31
+
32
+ # Get a description of what the constraint expects.
33
+ #
34
+ # @return [String] the negated constraint description
35
+ # @rbs override
36
+ def short_description
37
+ "not #{@expected.short_description}"
38
+ end
39
+
40
+ # The description of the violations that caused the constraint to be unsatisfied.
41
+ #
42
+ # This is used to help compose a error message when the constraint is not satisfied.
43
+ # Implementing classes can override this to provide more specific failure messages.
44
+ #
45
+ # @return [String] The description of the constraint when it fails.
46
+ # @rbs override
47
+ def short_violation_description
48
+ @expected.short_description
49
+ end
50
+
51
+ protected
52
+
53
+ # Check if the value does not satisfy the expected constraint.
54
+ #
55
+ # @return [Boolean] whether the constraint is satisfied
56
+ # @rbs override
57
+ def satisfies_constraint?
58
+ !@expected.satisfied?(@actual)
59
+ end
60
+
61
+ # Validate that the expectation is a valid constraint.
62
+ #
63
+ # @param expectation [Object] the expectation to validate
64
+ #
65
+ # @raise [ArgumentError] if the expectation is not a valid constraint
66
+ # @return [void]
67
+ # @rbs override
68
+ def validate_expectation!(expectation)
69
+ return if expectation.is_a?(Behavior)
70
+
71
+ raise ArgumentError, "expected a Domainic::Type::Constraint, got #{expectation.class}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,106 @@
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 OR behavior.
9
+ #
10
+ # The OrConstraint validates that a value satisfies at least one of its provided constraints,
11
+ # implementing logical OR behavior. This enables validation rules like "must be either a
12
+ # string or a symbol" or "must be nil or a positive number".
13
+ #
14
+ # Key features:
15
+ # - Combines multiple constraints with OR logic
16
+ # - Short-circuits on first passing constraint
17
+ # - Provides clear error messages listing all failed attempts
18
+ # - Supports incremental constraint addition
19
+ #
20
+ # @example Validating a value is either a String or Symbol
21
+ # string_constraint = StringConstraint.new(:self)
22
+ # symbol_constraint = SymbolConstraint.new(:self)
23
+ # string_or_symbol = OrConstraint.new(:self, [string_constraint, symbol_constraint])
24
+ #
25
+ # string_or_symbol.satisfied?("test") # => true
26
+ # string_or_symbol.satisfied?(:test) # => true
27
+ # string_or_symbol.satisfied?(123) # => false
28
+ #
29
+ # @author {https://aaronmallen.me Aaron Allen}
30
+ # @since 0.1.0
31
+ class OrConstraint
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 'or'
37
+ # @rbs override
38
+ def short_description
39
+ descriptions = @expected.map(&:short_description)
40
+ return descriptions.first if descriptions.size == 1
41
+
42
+ *first, last = descriptions
43
+ "#{first.join(', ')} or #{last}"
44
+ end
45
+
46
+ # @rbs! def expecting: (Behavior[untyped, untyped, untyped]) -> self
47
+
48
+ # The description of the violations that caused the constraint to be unsatisfied.
49
+ #
50
+ # This method provides detailed feedback when no constraints are satisfied,
51
+ # listing all the ways in which the value failed validation. Uses 'and' in the
52
+ # message because all constraints failed (e.g., "was not a string AND was not a
53
+ # symbol" rather than "was not a string OR was not a symbol").
54
+ #
55
+ # @return [String] The combined violation descriptions from all constraints
56
+ # @rbs override
57
+ def short_violation_description
58
+ violations = @expected.reject { |constraint| constraint.satisfied?(@actual) }
59
+ descriptions = violations.map(&:short_violation_description)
60
+ return descriptions.first if descriptions.size == 1
61
+
62
+ *first, last = descriptions
63
+ "#{first.join(', ')} and #{last}"
64
+ end
65
+
66
+ protected
67
+
68
+ # Coerce the expectation into an array and append new constraints.
69
+ #
70
+ # This enables both initializing with an array of constraints and adding
71
+ # new constraints incrementally via expecting().
72
+ #
73
+ # @param expectation [Behavior] the constraint to add
74
+ #
75
+ # @return [Array<Behavior>] the updated array of constraints
76
+ # @rbs (untyped expectation) -> Array[Behavior[untyped, untyped, untyped]]
77
+ def coerce_expectation(expectation)
78
+ expectation.is_a?(Array) ? (@expected || []).concat(expectation) : (@expected || []) << expectation
79
+ end
80
+
81
+ # Check if the value satisfies any of the expected constraints.
82
+ #
83
+ # Short-circuits on the first satisfied constraint for efficiency.
84
+ #
85
+ # @return [Boolean] whether any constraint is satisfied
86
+ # @rbs override
87
+ def satisfies_constraint?
88
+ @expected.any? { |constraint| constraint.satisfied?(@actual) }
89
+ end
90
+
91
+ # Validate that the expectation is an array of valid constraints.
92
+ #
93
+ # @param expectation [Object] the expectation to validate
94
+ #
95
+ # @raise [ArgumentError] if the expectation is not valid
96
+ # @return [void]
97
+ # @rbs override
98
+ def validate_expectation!(expectation)
99
+ return if expectation.is_a?(Array) && expectation.all?(Behavior)
100
+
101
+ raise ArgumentError, 'Expectation must be a Domainic::Type::Constraint'
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,75 @@
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 for validating that collection elements are in sorted order.
9
+ #
10
+ # This constraint ensures that elements in a collection are in ascending order
11
+ # by comparing the collection to its sorted version. The constraint works with
12
+ # any collection whose elements implement the Comparable module and respond to
13
+ # the <=> operator.
14
+ #
15
+ # @example Basic ordering validation
16
+ # constraint = OrderingConstraint.new(:self)
17
+ # constraint.satisfied?([1, 2, 3]) # => true
18
+ # constraint.satisfied?([1, 3, 2]) # => false
19
+ #
20
+ # @example With string elements
21
+ # constraint = OrderingConstraint.new(:self)
22
+ # constraint.satisfied?(['a', 'b', 'c']) # => true
23
+ # constraint.satisfied?(['c', 'a', 'b']) # => false
24
+ #
25
+ # @example With mixed types that can be compared
26
+ # constraint = OrderingConstraint.new(:self)
27
+ # constraint.satisfied?([1, 1.5, 2]) # => true
28
+ # constraint.satisfied?([2, 1, 1.5]) # => false
29
+ #
30
+ # @author {https://aaronmallen.me Aaron Allen}
31
+ # @since 0.1.0
32
+ class OrderingConstraint
33
+ include Behavior #[nil, untyped, {}]
34
+
35
+ # Get a human-readable description of the ordering requirement.
36
+ #
37
+ # @example
38
+ # constraint = OrderingConstraint.new(:self)
39
+ # constraint.description # => "ordered"
40
+ #
41
+ # @return [String] A description of the ordering requirement
42
+ # @rbs override
43
+ def short_description
44
+ 'ordered'
45
+ end
46
+
47
+ # Get a human-readable description of why ordering validation failed.
48
+ #
49
+ # @example
50
+ # constraint = OrderingConstraint.new(:self)
51
+ # constraint.satisfied?([3, 1, 2])
52
+ # constraint.short_violation_description # => "not ordered"
53
+ #
54
+ # @return [String] A description of the ordering failure
55
+ # @rbs override
56
+ def short_violation_description
57
+ 'not ordered'
58
+ end
59
+
60
+ protected
61
+
62
+ # Check if the collection elements are in sorted order.
63
+ #
64
+ # Compares the collection to its sorted version to determine if the
65
+ # elements are already in ascending order.
66
+ #
67
+ # @return [Boolean] true if the elements are in sorted order
68
+ # @rbs override
69
+ def satisfies_constraint?
70
+ @actual.sort == @actual
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end