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