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,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
|