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,102 @@
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 numeric parity (even or odd).
9
+ #
10
+ # This constraint verifies a number's parity by calling Ruby's standard
11
+ # even? and odd? methods. It normalizes method names
12
+ # to ensure compatibility with Ruby's predicate method naming conventions.
13
+ #
14
+ # @example Basic polarity checks
15
+ # constraint = ParityConstraint.new(:self).expecting(:even)
16
+ # constraint.satisfied?(2) # => true
17
+ # constraint.satisfied?(-2) # => true
18
+ # constraint.satisfied?(1) # => false
19
+ #
20
+ # @author {https://aaronmallen.me Aaron Allen}
21
+ # @since 0.1.0
22
+ class ParityConstraint
23
+ # @rbs!
24
+ # type expected = :even | :even? | :odd | :odd?
25
+
26
+ include Behavior #[expected, Numeric, {}]
27
+
28
+ # Get a human-readable description of the parity requirement.
29
+ #
30
+ # @example
31
+ # constraint = ParityConstraint.new(:self).expecting(:even)
32
+ # constraint.short_description # => "even"
33
+ #
34
+ # @return [String] Description of the parity requirement
35
+ # @rbs override
36
+ def short_description
37
+ @expected.to_s.delete_suffix('?')
38
+ end
39
+
40
+ # Get a human-readable description of why parity validation failed.
41
+ #
42
+ # @example
43
+ # constraint = ParityConstraint.new(:self).expecting(:positive)
44
+ # constraint.satisfied?(0)
45
+ # constraint.short_violation_description # => "odd"
46
+ #
47
+ # @return [String] Description of the validation failure
48
+ # @rbs override
49
+ def short_violation_description
50
+ case @expected
51
+ when :even?
52
+ 'odd'
53
+ when :odd?
54
+ 'even'
55
+ else
56
+ ''
57
+ end
58
+ end
59
+
60
+ protected
61
+
62
+ # Coerce the expectation into the correct method name format.
63
+ #
64
+ # Ensures the expectation ends with a question mark to match Ruby's
65
+ # standard method naming for predicate methods.
66
+ #
67
+ # @example
68
+ # coerce_expectation(:even) # => :even?
69
+ # coerce_expectation(:even?) # => :eve?
70
+ #
71
+ # @param expectation [Symbol, String] The parity check to perform
72
+ #
73
+ # @return [Symbol] The coerced method name
74
+ # @rbs override
75
+ def coerce_expectation(expectation)
76
+ expectation.to_s.end_with?('?') ? expectation.to_sym : :"#{expectation}?" #: expected
77
+ end
78
+
79
+ # Check if the value satisfies the parity constraint.
80
+ #
81
+ # @return [Boolean] true if the value matches the parity requirement
82
+ # @rbs override
83
+ def satisfies_constraint?
84
+ @actual.public_send(@expected)
85
+ end
86
+
87
+ # Validate that the expectation is a valid parity check.
88
+ #
89
+ # @param expectation [Object] The value to validate
90
+ #
91
+ # @raise [ArgumentError] if the expectation is not :even, :even?, :odd, or :odd?
92
+ # @return [void]
93
+ # @rbs override
94
+ def validate_expectation!(expectation)
95
+ return if %i[even? odd?].include?(expectation)
96
+
97
+ raise ArgumentError, "Invalid expectation: #{expectation}. Must be one of :even, :even?, :odd, or :odd?"
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,147 @@
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 numeric polarity (positive, negative, zero, nonzero).
9
+ #
10
+ # This constraint verifies a number's polarity by calling Ruby's standard
11
+ # positive?, negative?, zero?, and nonzero? methods. It normalizes method names
12
+ # to ensure compatibility with Ruby's predicate method naming conventions.
13
+ #
14
+ # @example Basic polarity checks
15
+ # constraint = PolarityConstraint.new(:self).expecting(:positive)
16
+ # constraint.satisfied?(42) # => true
17
+ # constraint.satisfied?(-42) # => false
18
+ # constraint.satisfied?(0) # => false
19
+ #
20
+ # @example Zero checks
21
+ # constraint = PolarityConstraint.new(:self).expecting(:zero)
22
+ # constraint.satisfied?(0) # => true
23
+ # constraint.satisfied?(42) # => false
24
+ #
25
+ # @example Nonzero checks
26
+ # constraint = PolarityConstraint.new(:self).expecting(:nonzero)
27
+ # constraint.satisfied?(42) # => true
28
+ # constraint.satisfied?(0) # => false
29
+ #
30
+ # @author {https://aaronmallen.me Aaron Allen}
31
+ # @since 0.1.0
32
+ class PolarityConstraint
33
+ # @rbs!
34
+ # type expected = :negative | :negative? | :nonzero | :nonzero? | :positive | :positive? | :zero | :zero?
35
+
36
+ include Behavior #[expected, Numeric, {}]
37
+
38
+ # Get a human-readable description of the polarity requirement.
39
+ #
40
+ # @example
41
+ # constraint = PolarityConstraint.new(:self).expecting(:positive)
42
+ # constraint.short_description # => "positive"
43
+ #
44
+ # @return [String] Description of the polarity requirement
45
+ # @rbs override
46
+ def short_description
47
+ @expected.to_s.delete_suffix('?')
48
+ end
49
+
50
+ # Get a human-readable description of why polarity validation failed.
51
+ #
52
+ # @example
53
+ # constraint = PolarityConstraint.new(:self).expecting(:positive)
54
+ # constraint.satisfied?(0)
55
+ # constraint.short_violation_description # => "zero"
56
+ #
57
+ # @return [String] Description of the validation failure
58
+ # @rbs override
59
+ def short_violation_description # rubocop:disable Metrics/MethodLength
60
+ case @expected
61
+ when :positive?
62
+ @actual.zero? ? 'zero' : 'negative'
63
+ when :negative?
64
+ @actual.zero? ? 'zero' : 'positive'
65
+ when :zero?
66
+ 'nonzero'
67
+ when :nonzero?
68
+ 'zero'
69
+ else
70
+ ''
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ # Coerce the expectation into the correct method name format.
77
+ #
78
+ # Ensures the expectation ends with a question mark to match Ruby's
79
+ # standard method naming for predicate methods.
80
+ #
81
+ # @example
82
+ # coerce_expectation(:positive) # => :positive?
83
+ # coerce_expectation(:positive?) # => :positive?
84
+ #
85
+ # @param expectation [Symbol, String] The polarity check to perform
86
+ #
87
+ # @return [Symbol] The coerced method name
88
+ # @rbs override
89
+ def coerce_expectation(expectation)
90
+ expectation.to_s.end_with?('?') ? expectation.to_sym : :"#{expectation}?" #: expected
91
+ end
92
+
93
+ # Check if the value satisfies the polarity constraint.
94
+ #
95
+ # @return [Boolean] true if the value matches the polarity requirement
96
+ # @rbs override
97
+ def satisfies_constraint?
98
+ interpret_result(@actual.public_send(@expected))
99
+ end
100
+
101
+ # Validate that the expectation is a valid polarity check.
102
+ #
103
+ # @param expectation [Object] The value to validate
104
+ #
105
+ # @raise [ArgumentError] if the expectation is not :negative, :negative?, :nonzero,
106
+ # :nonzero?, :positive, :positive?, :zero, or :zero?
107
+ # @return [void]
108
+ # @rbs override
109
+ def validate_expectation!(expectation)
110
+ return if %i[negative? nonzero? positive? zero?].include?(expectation)
111
+
112
+ raise ArgumentError, "Invalid expectation: #{expectation}. Must be one of :negative, :negative?, :nonzero, " \
113
+ ':nonzero?, :positive, :positive?, :zero, or :zero?'
114
+ end
115
+
116
+ private
117
+
118
+ # Interpret the result from Ruby's polarity methods.
119
+ #
120
+ # Handles the different return values from Ruby's polarity checking methods:
121
+ # - positive?/negative?/zero? return true/false
122
+ # - nonzero? returns nil for zero, and self for nonzero
123
+ #
124
+ # @example
125
+ # interpret_result(true) # => true
126
+ # interpret_result(false) # => false
127
+ # interpret_result(nil) # => false
128
+ # interpret_result(42) # => true
129
+ #
130
+ # @param result [Boolean, Integer, nil] The result from the polarity check
131
+ #
132
+ # @return [Boolean] true if the result indicates the desired polarity state
133
+ # @rbs ((bool | Numeric)? result) -> bool
134
+ def interpret_result(result)
135
+ case result
136
+ when true, false
137
+ result
138
+ when nil
139
+ false
140
+ else
141
+ true
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,135 @@
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 numeric values fall within a specified range.
9
+ #
10
+ # This constraint allows for validating numeric values against minimum and maximum
11
+ # boundaries. It supports specifying either or both boundaries, allowing for
12
+ # open-ended ranges when appropriate.
13
+ #
14
+ # @example Validating with both minimum and maximum
15
+ # constraint = RangeConstraint.new(:self, { minimum: 1, maximum: 10 })
16
+ # constraint.satisfied?(5) # => true
17
+ # constraint.satisfied?(15) # => false
18
+ #
19
+ # @example Validating with only minimum
20
+ # constraint = RangeConstraint.new(:self, { minimum: 0 })
21
+ # constraint.satisfied?(10) # => true
22
+ # constraint.satisfied?(-1) # => false
23
+ #
24
+ # @example Validating with only maximum
25
+ # constraint = RangeConstraint.new(:self, { maximum: 100 })
26
+ # constraint.satisfied?(50) # => true
27
+ # constraint.satisfied?(150) # => false
28
+ #
29
+ # @author {https://aaronmallen.me Aaron Allen}
30
+ # @since 0.1.0
31
+ class RangeConstraint
32
+ # @rbs!
33
+ # type expected = { ?minimum: Numeric, ?maximum: Numeric }
34
+ #
35
+ # type options = { ?inclusive: bool }
36
+
37
+ include Behavior #[expected, Numeric, options]
38
+
39
+ # Get a human-readable description of the range constraint.
40
+ #
41
+ # @example With both bounds
42
+ # constraint = RangeConstraint.new(:self, { minimum: 1, maximum: 10 })
43
+ # constraint.description
44
+ # # => "greater than or equal to 1 and less than or equal to 10"
45
+ #
46
+ # @example With only minimum
47
+ # constraint = RangeConstraint.new(:self, { minimum: 0 })
48
+ # constraint.description # => "greater than or equal to 0"
49
+ #
50
+ # @example With only maximum
51
+ # constraint = RangeConstraint.new(:self, { maximum: 100 })
52
+ # constraint.description # => "less than or equal to 100"
53
+ #
54
+ # @return [String] A description of the range bounds
55
+ # @rbs override
56
+ def short_description
57
+ min, max = @expected.values_at(:minimum, :maximum)
58
+ min_description = "greater than or equal to #{min}"
59
+ max_description = "less than or equal to #{max}"
60
+
61
+ return "#{min_description} and #{max_description}" unless min.nil? || max.nil?
62
+ return min_description unless min.nil?
63
+
64
+ max_description
65
+ end
66
+
67
+ # The description of the violations that caused the constraint to be unsatisfied.
68
+ #
69
+ # This is used to help compose a error message when the constraint is not satisfied.
70
+ # Implementing classes can override this to provide more specific failure messages.
71
+ #
72
+ # @return [String] The description of the constraint when it fails.
73
+ # @rbs override
74
+ def short_violation_description
75
+ @actual.inspect
76
+ end
77
+
78
+ protected
79
+
80
+ # @rbs override
81
+ def coerce_expectation(expectation)
82
+ @expected.is_a?(Hash) && expectation.is_a?(Hash) ? @expected.merge(expectation) : expectation #: expected
83
+ end
84
+
85
+ # Check if the actual value falls within the specified range.
86
+ #
87
+ # Uses -Infinity and +Infinity as default bounds when minimum or maximum
88
+ # are not specified, respectively.
89
+ #
90
+ # @return [Boolean] true if the value is within range
91
+ # @rbs override
92
+ def satisfies_constraint?
93
+ min, max = @expected.values_at(:minimum, :maximum)
94
+ min_comparison, max_comparison = @options.fetch(:inclusive, true) ? %i[>= <=] : %i[> <]
95
+
96
+ @actual.send(min_comparison, (min || -Float::INFINITY)) &&
97
+ @actual.send(max_comparison, (max || Float::INFINITY))
98
+ end
99
+
100
+ # Validate that the expected value is a properly formatted range specification.
101
+ #
102
+ # @param expectation [Hash] The range specification to validate
103
+ #
104
+ # @raise [ArgumentError] if the specification is invalid
105
+ # @return [void]
106
+ # @rbs override
107
+ def validate_expectation!(expectation)
108
+ unless expectation.is_a?(Hash) && (expectation.key?(:minimum) || expectation.key?(:maximum))
109
+ raise ArgumentError, 'Expectation must be a Hash including :minimum and/or :maximum'
110
+ end
111
+
112
+ validate_minimum_and_maximum!(expectation)
113
+ end
114
+
115
+ # Validate the minimum and maximum values in a range specification.
116
+ #
117
+ # @param expectation [Hash] The range specification to validate
118
+ #
119
+ # @raise [ArgumentError] if the values are invalid
120
+ # @return [void]
121
+ # @rbs (untyped expectation) -> void
122
+ def validate_minimum_and_maximum!(expectation)
123
+ expectation.each_pair do |property, value|
124
+ raise ArgumentError, ":#{property} must be a Numeric" unless value.nil? || value.is_a?(Numeric)
125
+ end
126
+
127
+ min, max = expectation.values_at(:minimum, :maximum)
128
+ return if min.nil? || max.nil? || min <= max
129
+
130
+ raise ArgumentError, ':minimum must be less than or equal to :maximum'
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/type/behavior'
4
+ require 'domainic/type/constraint/behavior'
5
+
6
+ module Domainic
7
+ module Type
8
+ module Constraint
9
+ # A constraint for validating that values match a specific type.
10
+ #
11
+ # This constraint provides type checking functionality through Ruby's standard
12
+ # type system, supporting class inheritance checks, module inclusion checks,
13
+ # and custom type validation through the case equality operator (===).
14
+ #
15
+ # @example Basic class type validation
16
+ # constraint = TypeConstraint.new(:self, String)
17
+ # constraint.satisfied?("hello") # => true
18
+ # constraint.satisfied?(123) # => false
19
+ #
20
+ # @example Module type validation
21
+ # constraint = TypeConstraint.new(:self, Enumerable)
22
+ # constraint.satisfied?([1, 2, 3]) # => true
23
+ # constraint.satisfied?("string") # => false
24
+ #
25
+ # @example Custom type validation
26
+ # class EvenType
27
+ # def self.===(value)
28
+ # value.is_a?(Integer) && value.even?
29
+ # end
30
+ # end
31
+ #
32
+ # constraint = TypeConstraint.new(:self, EvenType)
33
+ # constraint.satisfied?(2) # => true
34
+ # constraint.satisfied?(3) # => false
35
+ #
36
+ # @example Nil type validation
37
+ # constraint = TypeConstraint.new(:self, nil)
38
+ # constraint.satisfied?(nil) # => true
39
+ # constraint.satisfied?(false) # => false
40
+ #
41
+ # @author {https://aaronmallen.me Aaron Allen}
42
+ # @since 0.1.0
43
+ class TypeConstraint
44
+ include Behavior #[Class | Module | Type::Behavior | nil, untyped, {}]
45
+
46
+ # Get a human-readable description of the expected type.
47
+ #
48
+ # @example
49
+ # constraint = TypeConstraint.new(:self, Float)
50
+ # constraint.description # => "Float"
51
+ #
52
+ # constraint = TypeConstraint.new(:self, Array)
53
+ # constraint.description # => "Array"
54
+ #
55
+ # @return [String] A description of the expected type
56
+ # @rbs override
57
+ def short_description
58
+ @expected.to_s
59
+ end
60
+
61
+ # The description of the violations that caused the constraint to be unsatisfied.
62
+ #
63
+ # This is used to help compose a error message when the constraint is not satisfied.
64
+ # Implementing classes can override this to provide more specific failure messages.
65
+ #
66
+ # @return [String] The description of the constraint when it fails.
67
+ # @rbs override
68
+ def short_violation_description
69
+ @actual.class.to_s
70
+ end
71
+
72
+ protected
73
+
74
+ # Coerce the expected type, converting nil to NilClass for type checking.
75
+ #
76
+ # @param expectation [Class, Module, nil] The type to coerce
77
+ #
78
+ # @return [Class, Module] The coerced type
79
+ # @rbs override
80
+ def coerce_expectation(expectation)
81
+ expectation.nil? ? NilClass : expectation
82
+ end
83
+
84
+ # Check if the actual value matches the expected type.
85
+ #
86
+ # The check is performed using both the case equality operator (===)
87
+ # and Ruby's is_a? method to provide maximum flexibility in type checking.
88
+ #
89
+ # @return [Boolean] true if the value matches the expected type
90
+ # @rbs override
91
+ def satisfies_constraint?
92
+ @expected === @actual || @actual.is_a?(@expected) # rubocop:disable Style/CaseEquality
93
+ end
94
+
95
+ # Validate that the expected type is a valid Ruby type.
96
+ #
97
+ # @param expectation [Object] The type to validate
98
+ #
99
+ # @raise [ArgumentError] if the expectation is not a valid type
100
+ # @return [void]
101
+ # @rbs override
102
+ def validate_expectation!(expectation)
103
+ return if [Class, Module, Type::Behavior].any? { |type| expectation.is_a?(type) }
104
+
105
+ raise ArgumentError, 'Expectation must be a Class, Module, or Domainic::Type'
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,69 @@
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 all elements in a collection are unique.
9
+ #
10
+ # This constraint ensures that an enumerable collection contains no duplicate
11
+ # elements by comparing the collection's size before and after removing duplicates.
12
+ # It works with any object that includes the Enumerable module and responds
13
+ # to #uniq and #count.
14
+ #
15
+ # @example Basic uniqueness validation
16
+ # constraint = UniquenessConstraint.new(:self)
17
+ # constraint.satisfied?([1, 2, 3]) # => true
18
+ # constraint.satisfied?([1, 2, 2, 3]) # => false
19
+ #
20
+ # @example With different collection types
21
+ # constraint = UniquenessConstraint.new(:self)
22
+ # constraint.satisfied?(Set[1, 2, 3]) # => true
23
+ # constraint.satisfied?(['a', 'b', 'b']) # => false
24
+ #
25
+ # @author {https://aaronmallen.me Aaron Allen}
26
+ # @since 0.1.0
27
+ class UniquenessConstraint
28
+ include Behavior #[nil, Enumerable, {}]
29
+
30
+ # Get a human-readable description of the uniqueness requirement.
31
+ #
32
+ # @example
33
+ # constraint = UniquenessConstraint.new(:self)
34
+ # constraint.description # => "unique"
35
+ #
36
+ # @return [String] A description of the uniqueness requirement
37
+ # @rbs override
38
+ def short_description
39
+ 'unique'
40
+ end
41
+
42
+ # The description of the violations that caused the constraint to be unsatisfied.
43
+ #
44
+ # This is used to help compose a error message when the constraint is not satisfied.
45
+ # Implementing classes can override this to provide more specific failure messages.
46
+ #
47
+ # @return [String] The description of the constraint when it fails.
48
+ # @rbs override
49
+ def short_violation_description
50
+ 'not unique'
51
+ end
52
+
53
+ protected
54
+
55
+ # Check if all elements in the collection are unique.
56
+ #
57
+ # Compares the size of the collection before and after removing duplicates.
58
+ # If the sizes are equal, all elements are unique. If they differ, duplicates
59
+ # were present.
60
+ #
61
+ # @return [Boolean] true if all elements are unique
62
+ # @rbs override
63
+ def satisfies_constraint?
64
+ @actual.uniq.count == @actual.count
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end