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,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 any element in an enumerable satisfies a given constraint.
|
9
|
+
#
|
10
|
+
# The AnyConstraint validates that at least one element in an enumerable value meets
|
11
|
+
# the provided constraint. This enables validation rules like "must contain at least
|
12
|
+
# one string" or "must have any positive number".
|
13
|
+
#
|
14
|
+
# Key features:
|
15
|
+
# - Validates enumerable elements against a constraint
|
16
|
+
# - Short-circuits on first satisfying element
|
17
|
+
# - Provides clear error messages for failing elements
|
18
|
+
# - Handles non-enumerable values gracefully
|
19
|
+
#
|
20
|
+
# @example Validating an array contains any string
|
21
|
+
# string_constraint = StringConstraint.new(:self)
|
22
|
+
# any_string = AnyConstraint.new(:self, string_constraint)
|
23
|
+
#
|
24
|
+
# any_string.satisfied?(['a', 1, 'c']) # => true
|
25
|
+
# any_string.satisfied?([1, 2, 3]) # => false
|
26
|
+
# any_string.satisfied?(nil) # => false (not enumerable)
|
27
|
+
#
|
28
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
29
|
+
# @since 0.1.0
|
30
|
+
class AnyConstraint
|
31
|
+
include Behavior #[Behavior[untyped, untyped, untyped], Enumerable, {}]
|
32
|
+
|
33
|
+
# Get a description of what the constraint expects.
|
34
|
+
#
|
35
|
+
# @return [String] a description combining all constraint descriptions
|
36
|
+
# @rbs override
|
37
|
+
def short_description
|
38
|
+
@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 no constraints are satisfied,
|
44
|
+
# listing all the ways in which the value failed validation.
|
45
|
+
#
|
46
|
+
# @return [String] The combined violation descriptions from all constraints
|
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 if @expected.satisfied?(element)
|
53
|
+
|
54
|
+
@expected.short_violation_description
|
55
|
+
end.uniq.join(', ')
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
# Check if the value satisfies any of the expected constraints.
|
61
|
+
#
|
62
|
+
# @return [Boolean] whether any constraint is satisfied
|
63
|
+
# @rbs override
|
64
|
+
def satisfies_constraint?
|
65
|
+
@actual.any? { |element| @expected.satisfied?(element) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# Validate that the expectation is an array of valid constraints.
|
69
|
+
#
|
70
|
+
# @param expectation [Object] the expectation to validate
|
71
|
+
#
|
72
|
+
# @raise [ArgumentError] if the expectation is not valid
|
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,104 @@
|
|
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 string case formatting.
|
9
|
+
#
|
10
|
+
# This constraint verifies that string values conform to specific case formats:
|
11
|
+
# - upper: all characters are uppercase
|
12
|
+
# - lower: all characters are lowercase
|
13
|
+
# - mixed: contains both uppercase and lowercase characters
|
14
|
+
# - title: words are capitalized (first letter uppercase, rest lowercase)
|
15
|
+
#
|
16
|
+
# @example Uppercase validation
|
17
|
+
# constraint = CaseConstraint.new(:self, :upper)
|
18
|
+
# constraint.satisfied?("HELLO") # => true
|
19
|
+
# constraint.satisfied?("Hello") # => false
|
20
|
+
#
|
21
|
+
# @example Title case validation
|
22
|
+
# constraint = CaseConstraint.new(:self, :title)
|
23
|
+
# constraint.satisfied?("Hello World") # => true
|
24
|
+
# constraint.satisfied?("hello world") # => false
|
25
|
+
#
|
26
|
+
# @example Mixed case validation
|
27
|
+
# constraint = CaseConstraint.new(:self, :mixed)
|
28
|
+
# constraint.satisfied?("helloWORLD") # => true
|
29
|
+
# constraint.satisfied?("HELLO") # => false
|
30
|
+
#
|
31
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
32
|
+
# @since 0.1.0
|
33
|
+
class CaseConstraint
|
34
|
+
# @rbs!
|
35
|
+
# type expected = :upper | :lower | :mixed | :title
|
36
|
+
|
37
|
+
include Behavior #[expected, untyped, {}]
|
38
|
+
|
39
|
+
# Valid case format options
|
40
|
+
#
|
41
|
+
# @return [Array<Symbol>] List of valid case formats
|
42
|
+
VALID_CASES = %i[upper lower mixed title].freeze #: Array[expected]
|
43
|
+
|
44
|
+
# Get a human-readable description of the case requirement.
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# constraint = CaseConstraint.new(:self, :upper)
|
48
|
+
# constraint.short_description # => "upper case"
|
49
|
+
#
|
50
|
+
# @return [String] A description of the case requirement
|
51
|
+
# @rbs override
|
52
|
+
def short_description
|
53
|
+
"#{@expected} case"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get a human-readable description of why case validation failed.
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# constraint = CaseConstraint.new(:self, :upper)
|
60
|
+
# constraint.satisfied?("Hello")
|
61
|
+
# constraint.short_violation_description # => "not upper case"
|
62
|
+
#
|
63
|
+
# @return [String] A description of the case mismatch
|
64
|
+
# @rbs override
|
65
|
+
def short_violation_description
|
66
|
+
"not #{@expected} case"
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
# Check if the string matches the expected case format.
|
72
|
+
#
|
73
|
+
# @return [Boolean] true if the string matches the expected case
|
74
|
+
# @rbs override
|
75
|
+
def satisfies_constraint?
|
76
|
+
case @expected
|
77
|
+
when :upper
|
78
|
+
@actual == @actual.upcase
|
79
|
+
when :lower
|
80
|
+
@actual == @actual.downcase
|
81
|
+
when :title
|
82
|
+
# Use map and join to preserve original whitespace
|
83
|
+
@actual == @actual.scan(/[^\s]+/).map(&:capitalize).join(
|
84
|
+
@actual.match(/\s+/).to_s
|
85
|
+
)
|
86
|
+
when :mixed
|
87
|
+
!(@actual == @actual.upcase || @actual == @actual.downcase)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Validate that the expectation is a valid case format.
|
92
|
+
#
|
93
|
+
# @param expectation [Object] The case format to validate
|
94
|
+
#
|
95
|
+
# @raise [ArgumentError] if the expectation is not a valid case format
|
96
|
+
# @return [void]
|
97
|
+
# @rbs override
|
98
|
+
def validate_expectation!(expectation)
|
99
|
+
raise ArgumentError, "case must be one of: #{VALID_CASES.join(', ')}" unless VALID_CASES.include?(expectation)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,111 @@
|
|
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 string character sets.
|
9
|
+
#
|
10
|
+
# This constraint verifies that string values contain only characters from
|
11
|
+
# specific character sets:
|
12
|
+
# - ascii: ASCII characters (0x00-0x7F)
|
13
|
+
# - alphanumeric: Letters and numbers
|
14
|
+
# - alpha: Letters only
|
15
|
+
# - numeric: Numbers only
|
16
|
+
# - printable: Visible characters and spaces
|
17
|
+
#
|
18
|
+
# @example ASCII validation
|
19
|
+
# constraint = CharacterSetConstraint.new(:self, :ascii)
|
20
|
+
# constraint.satisfied?("hello") # => true
|
21
|
+
# constraint.satisfied?("héllo") # => false
|
22
|
+
#
|
23
|
+
# @example Alphanumeric validation
|
24
|
+
# constraint = CharacterSetConstraint.new(:self, :alphanumeric)
|
25
|
+
# constraint.satisfied?("abc123") # => true
|
26
|
+
# constraint.satisfied?("abc-123") # => false
|
27
|
+
#
|
28
|
+
# @example Numeric validation
|
29
|
+
# constraint = CharacterSetConstraint.new(:self, :numeric)
|
30
|
+
# constraint.satisfied?("12345") # => true
|
31
|
+
# constraint.satisfied?("123.45") # => false
|
32
|
+
#
|
33
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
34
|
+
# @since 0.1.0
|
35
|
+
class CharacterSetConstraint
|
36
|
+
# @rbs!
|
37
|
+
# type expected = :ascii | :alphanumeric | :alpha | :numeric | :printable
|
38
|
+
|
39
|
+
include Behavior #[expected, untyped, {}]
|
40
|
+
|
41
|
+
# Valid character set patterns
|
42
|
+
#
|
43
|
+
# @return [Hash{Symbol => Regexp}] Map of set names to validation patterns
|
44
|
+
VALID_SETS = {
|
45
|
+
ascii: /\A[\x00-\x7F]*\z/, # Any ASCII character
|
46
|
+
alphanumeric: /\A[[:alnum:]]*\z/, # Letters and numbers
|
47
|
+
alpha: /\A[[:alpha:]]*\z/, # Letters only
|
48
|
+
numeric: /\A[[:digit:]]*\z/, # Numbers only
|
49
|
+
printable: /\A[[:print:]]*\z/ # Visible chars and spaces
|
50
|
+
}.freeze #: Hash[expected, Regexp]
|
51
|
+
|
52
|
+
# Get a human-readable description of the character set requirement.
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# constraint = CharacterSetConstraint.new(:self, :numeric)
|
56
|
+
# constraint.short_description # => "only numeric characters"
|
57
|
+
#
|
58
|
+
# @return [String] A description of the character set requirement
|
59
|
+
# @rbs override
|
60
|
+
def short_description
|
61
|
+
"only #{@expected} characters"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get a human-readable description of why character validation failed.
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# constraint = CharacterSetConstraint.new(:self, :numeric)
|
68
|
+
# constraint.satisfied?("abc123")
|
69
|
+
# constraint.short_violation_description # => "non-numeric characters"
|
70
|
+
#
|
71
|
+
# @return [String] A description of the character set violation
|
72
|
+
# @rbs override
|
73
|
+
def short_violation_description
|
74
|
+
"non-#{@expected} characters"
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
# Convert character set name to symbol.
|
80
|
+
#
|
81
|
+
# @param expectation [String, Symbol] The character set name
|
82
|
+
#
|
83
|
+
# @return [Symbol] The character set name as a symbol
|
84
|
+
# @rbs override
|
85
|
+
def coerce_expectation(expectation)
|
86
|
+
expectation.to_sym
|
87
|
+
end
|
88
|
+
|
89
|
+
# Check if the string contains only characters from the expected set.
|
90
|
+
#
|
91
|
+
# @return [Boolean] true if all characters match the expected set
|
92
|
+
# @rbs override
|
93
|
+
def satisfies_constraint?
|
94
|
+
pattern = VALID_SETS.fetch(@expected)
|
95
|
+
pattern.match?(@actual)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Validate that the expectation is a valid character set.
|
99
|
+
#
|
100
|
+
# @param expectation [Object] The character set to validate
|
101
|
+
#
|
102
|
+
# @raise [ArgumentError] if the expectation is not a valid character set
|
103
|
+
# @return [void]
|
104
|
+
# @rbs override
|
105
|
+
def validate_expectation!(expectation)
|
106
|
+
raise ArgumentError, "set must be one of: #{VALID_SETS.keys.join(', ')}" unless VALID_SETS.key?(expectation)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,126 @@
|
|
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 are divisible by a specified value.
|
9
|
+
#
|
10
|
+
# This constraint checks if one number is evenly divisible by another, allowing for
|
11
|
+
# a configurable tolerance to handle floating-point arithmetic imprecision. It can
|
12
|
+
# be used to validate properties like:
|
13
|
+
# - Even/odd numbers (divisible by 2)
|
14
|
+
# - Factors and multiples
|
15
|
+
# - Decimal place alignment (divisible by 0.1, 0.01, etc)
|
16
|
+
#
|
17
|
+
# @example Basic divisibility check
|
18
|
+
# constraint = DivisibilityConstraint.new(:self, 5)
|
19
|
+
# constraint.satisfied?(10) # => true
|
20
|
+
# constraint.satisfied?(7) # => false
|
21
|
+
#
|
22
|
+
# @example With floating point values
|
23
|
+
# constraint = DivisibilityConstraint.new(:self, 0.1)
|
24
|
+
# constraint.satisfied?(0.3) # => true
|
25
|
+
# constraint.satisfied?(0.35) # => false
|
26
|
+
#
|
27
|
+
# @example Custom tolerance
|
28
|
+
# constraint = DivisibilityConstraint.new(:self)
|
29
|
+
# constraint.expecting(3)
|
30
|
+
# constraint.with_options(tolerance: 1e-5)
|
31
|
+
#
|
32
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
33
|
+
# @since 0.1.0
|
34
|
+
class DivisibilityConstraint
|
35
|
+
# @rbs!
|
36
|
+
# type options = { ?tolerance: Numeric }
|
37
|
+
|
38
|
+
include Behavior #[Numeric, Numeric, options]
|
39
|
+
|
40
|
+
# Default tolerance for floating-point arithmetic comparisons.
|
41
|
+
#
|
42
|
+
# @return [Float] The default tolerance value
|
43
|
+
DEFAULT_TOLERANCE = 1e-10 #: Float
|
44
|
+
|
45
|
+
# Get a human-readable description of the divisibility requirement.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# constraint = DivisibilityConstraint.new(:self, 5)
|
49
|
+
# constraint.short_description # => "divisible by 5"
|
50
|
+
#
|
51
|
+
# @return [String] Description of the divisibility requirement
|
52
|
+
# @rbs override
|
53
|
+
def short_description
|
54
|
+
"divisible by #{@expected.inspect}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get a human-readable description of why divisibility validation failed.
|
58
|
+
#
|
59
|
+
# @example With non-numeric value
|
60
|
+
# constraint = DivisibilityConstraint.new(:self, 5)
|
61
|
+
# constraint.satisfied?("not a number")
|
62
|
+
# constraint.short_violation_description # => "not Numeric"
|
63
|
+
#
|
64
|
+
# @example With non-divisible value
|
65
|
+
# constraint = DivisibilityConstraint.new(:self, 5)
|
66
|
+
# constraint.satisfied?(7)
|
67
|
+
# constraint.short_violation_description # => "not divisible by 5"
|
68
|
+
#
|
69
|
+
# @return [String] Description of the validation failure
|
70
|
+
# @rbs override
|
71
|
+
def short_violation_description
|
72
|
+
return 'not Numeric' unless @actual.is_a?(Numeric)
|
73
|
+
|
74
|
+
"not divisible by #{@expected.inspect}"
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
# Check if the actual value is evenly divisible by the expected value.
|
80
|
+
#
|
81
|
+
# This method handles both integer and floating-point values, using the
|
82
|
+
# configured tolerance to account for floating-point arithmetic imprecision.
|
83
|
+
#
|
84
|
+
# @return [Boolean] true if the value is evenly divisible
|
85
|
+
# @rbs override
|
86
|
+
def satisfies_constraint?
|
87
|
+
actual = parse_value(@actual)
|
88
|
+
expected = parse_value(@expected)
|
89
|
+
|
90
|
+
return false if actual.nil? || expected.nil?
|
91
|
+
return false if expected.zero?
|
92
|
+
|
93
|
+
tolerance = @options.fetch(:tolerance, DEFAULT_TOLERANCE)
|
94
|
+
# @type var tolerance: Numeric
|
95
|
+
((actual / expected) - (actual / expected).round).abs < tolerance
|
96
|
+
end
|
97
|
+
|
98
|
+
# Validate that the expected value is a non-zero number.
|
99
|
+
#
|
100
|
+
# @param expectation [Object] The value to validate
|
101
|
+
#
|
102
|
+
# @raise [ArgumentError] if the value is not a non-zero number
|
103
|
+
# @return [void]
|
104
|
+
# @rbs override
|
105
|
+
def validate_expectation!(expectation)
|
106
|
+
raise ArgumentError, 'Expectation must be Numeric' unless expectation.is_a?(Numeric)
|
107
|
+
raise ArgumentError, 'Expectation must be non-zero' if expectation.zero?
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
# Parse a value into a float, returning nil if parsing fails.
|
113
|
+
#
|
114
|
+
# @param value [Numeric] The value to parse
|
115
|
+
#
|
116
|
+
# @return [Float, nil] The parsed float value or nil if parsing failed
|
117
|
+
# @rbs (Numeric value) -> Float?
|
118
|
+
def parse_value(value)
|
119
|
+
Float(value)
|
120
|
+
rescue ArgumentError, TypeError
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
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 a collection is empty.
|
9
|
+
#
|
10
|
+
# This constraint ensures that an enumerable collection contains no elements
|
11
|
+
# by checking the collection's #empty? method. It works with any object that
|
12
|
+
# includes the Enumerable module and responds to #empty?.
|
13
|
+
#
|
14
|
+
# @example Basic emptiness validation
|
15
|
+
# constraint = EmptinessConstraint.new(:self)
|
16
|
+
# constraint.satisfied?([]) # => true
|
17
|
+
# constraint.satisfied?([1, 2, 3]) # => false
|
18
|
+
#
|
19
|
+
# @example With different collection types
|
20
|
+
# constraint = EmptinessConstraint.new(:self)
|
21
|
+
# constraint.satisfied?(Set.new) # => true
|
22
|
+
# constraint.satisfied?(['a', 'b']) # => false
|
23
|
+
#
|
24
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
25
|
+
# @since 0.1.0
|
26
|
+
class EmptinessConstraint
|
27
|
+
include Behavior #[nil, untyped, {}]
|
28
|
+
|
29
|
+
# Get a human-readable description of the emptiness requirement.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# constraint = EmptinessConstraint.new(:self)
|
33
|
+
# constraint.description # => "empty"
|
34
|
+
#
|
35
|
+
# @return [String] A description of the emptiness requirement
|
36
|
+
# @rbs override
|
37
|
+
def short_description
|
38
|
+
'empty'
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get a human-readable description of why emptiness validation failed.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# constraint = EmptinessConstraint.new(:self)
|
45
|
+
# constraint.satisfied?([1, 2, 3])
|
46
|
+
# constraint.short_violation_description # => "not empty"
|
47
|
+
#
|
48
|
+
# @return [String] A description of the emptiness failure
|
49
|
+
# @rbs override
|
50
|
+
def short_violation_description
|
51
|
+
'not empty'
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
# Check if the collection is empty.
|
57
|
+
#
|
58
|
+
# Uses the collection's #empty? method to determine if it contains
|
59
|
+
# any elements.
|
60
|
+
#
|
61
|
+
# @return [Boolean] true if the collection is empty
|
62
|
+
# @rbs override
|
63
|
+
def satisfies_constraint?
|
64
|
+
@actual.empty?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
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 values are equal to an expected value.
|
9
|
+
#
|
10
|
+
# This constraint uses Ruby's standard equality operator (==) to compare values,
|
11
|
+
# allowing for type-specific equality definitions while maintaining consistent
|
12
|
+
# behavior across different value types.
|
13
|
+
#
|
14
|
+
# @example Basic equality validation
|
15
|
+
# constraint = EqualityConstraint.new(:self, 42)
|
16
|
+
# constraint.satisfied?(42) # => true
|
17
|
+
# constraint.satisfied?(41) # => false
|
18
|
+
#
|
19
|
+
# @example Complex object comparison
|
20
|
+
# point = Struct.new(:x, :y).new(1, 2)
|
21
|
+
# constraint = EqualityConstraint.new(:self, point)
|
22
|
+
# constraint.satisfied?(Struct.new(:x, :y).new(1, 2)) # => true
|
23
|
+
# constraint.satisfied?(Struct.new(:x, :y).new(2, 1)) # => false
|
24
|
+
#
|
25
|
+
# @example Array comparison
|
26
|
+
# constraint = EqualityConstraint.new(:self, [1, 2, 3])
|
27
|
+
# constraint.satisfied?([1, 2, 3]) # => true
|
28
|
+
# constraint.satisfied?([3, 2, 1]) # => false
|
29
|
+
#
|
30
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
31
|
+
# @since 0.1.0
|
32
|
+
class EqualityConstraint
|
33
|
+
include Behavior #[untyped, untyped, {}]
|
34
|
+
|
35
|
+
# Get a human-readable description of the equality requirement.
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# constraint = EqualityConstraint.new(:self, 42)
|
39
|
+
# constraint.description # => "equal to 42"
|
40
|
+
#
|
41
|
+
# @return [String] A description of the expected value
|
42
|
+
# @rbs override
|
43
|
+
def short_description
|
44
|
+
"equal to #{@expected.inspect}"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get a human-readable description of why equality validation failed.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# constraint = EqualityConstraint.new(:self, 42)
|
51
|
+
# constraint.satisfied?(41)
|
52
|
+
# constraint.short_violation_description # => "not equal to 42"
|
53
|
+
#
|
54
|
+
# @return [String] A description of the equality failure
|
55
|
+
# @rbs override
|
56
|
+
def short_violation_description
|
57
|
+
"not equal to #{@expected.inspect}"
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
# Check if the actual value equals the expected value.
|
63
|
+
#
|
64
|
+
# Uses Ruby's standard equality operator (==) for comparison, allowing
|
65
|
+
# objects to define their own equality behavior.
|
66
|
+
#
|
67
|
+
# @return [Boolean] true if the values are equal
|
68
|
+
# @rbs override
|
69
|
+
def satisfies_constraint?
|
70
|
+
@actual == @expected
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,123 @@
|
|
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 are finite or infinite.
|
9
|
+
#
|
10
|
+
# This constraint verifies whether a numeric value is finite or infinite by calling
|
11
|
+
# Ruby's standard finite?/infinite? methods. It's useful for ensuring numbers remain
|
12
|
+
# within computable bounds and handling edge cases in numerical computations.
|
13
|
+
#
|
14
|
+
# @example Basic finiteness check
|
15
|
+
# constraint = FinitenessConstraint.new(:self).expecting(:finite)
|
16
|
+
# constraint.satisfied?(42) # => true
|
17
|
+
# constraint.satisfied?(Float::INFINITY) # => false
|
18
|
+
#
|
19
|
+
# @example Checking for infinity
|
20
|
+
# constraint = FinitenessConstraint.new(:self).expecting(:infinite)
|
21
|
+
# constraint.satisfied?(Float::INFINITY) # => true
|
22
|
+
# constraint.satisfied?(42) # => false
|
23
|
+
#
|
24
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
25
|
+
# @since 0.1.0
|
26
|
+
class FinitenessConstraint
|
27
|
+
# @rbs!
|
28
|
+
# type expected = :finite | :finite? | :infinite | :infinite?
|
29
|
+
|
30
|
+
include Behavior #[expected, Numeric, {}]
|
31
|
+
|
32
|
+
# Get a human-readable description of the finiteness requirement.
|
33
|
+
#
|
34
|
+
# @return [String] Description of the finiteness 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 finiteness validation failed.
|
41
|
+
#
|
42
|
+
# @return [String] Description of the validation failure
|
43
|
+
# @rbs override
|
44
|
+
def short_violation_description
|
45
|
+
if @expected == :finite?
|
46
|
+
'infinite'
|
47
|
+
elsif @expected == :infinite?
|
48
|
+
'finite'
|
49
|
+
else
|
50
|
+
''
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
# Coerce the expectation into the correct method name format.
|
57
|
+
#
|
58
|
+
# Ensures the expectation ends with a question mark to match Ruby's
|
59
|
+
# standard method naming for predicate methods.
|
60
|
+
#
|
61
|
+
# @param expectation [Symbol, String] The finiteness check to perform
|
62
|
+
#
|
63
|
+
# @return [Symbol] The coerced method name
|
64
|
+
# @rbs override
|
65
|
+
def coerce_expectation(expectation)
|
66
|
+
expectation.to_s.end_with?('?') ? expectation.to_sym : :"#{expectation}?" #: expected
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if the value satisfies the finiteness constraint.
|
70
|
+
#
|
71
|
+
# @return [Boolean] true if the value matches the finiteness requirement
|
72
|
+
# @rbs override
|
73
|
+
def satisfies_constraint?
|
74
|
+
interpret_result(@actual.public_send(@expected))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Validate that the expectation is a valid finiteness check.
|
78
|
+
#
|
79
|
+
# @param expectation [Object] The value to validate
|
80
|
+
#
|
81
|
+
# @raise [ArgumentError] if the expectation is not :finite, :finite?, :infinite, or :infinite?
|
82
|
+
# @return [void]
|
83
|
+
# @rbs override
|
84
|
+
def validate_expectation!(expectation)
|
85
|
+
return if %i[finite? infinite?].include?(expectation)
|
86
|
+
|
87
|
+
raise ArgumentError, "Invalid expectation: #{expectation}. Must be one of :finite, :finite?, :infinite, " \
|
88
|
+
'or :infinite?'
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Interpret the result from Ruby's finite?/infinite? methods.
|
94
|
+
#
|
95
|
+
# Handles the different return values from Ruby's finiteness checking methods:
|
96
|
+
# - finite? returns true/false
|
97
|
+
# - infinite? returns nil (for finite numbers), 1 (for +∞), or -1 (for -∞)
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# interpret_result(true) # => true
|
101
|
+
# interpret_result(false) # => false
|
102
|
+
# interpret_result(nil) # => false
|
103
|
+
# interpret_result(1) # => true
|
104
|
+
# interpret_result(-1) # => true
|
105
|
+
#
|
106
|
+
# @param result [Boolean, Integer, nil] The result from finite? or infinite?
|
107
|
+
#
|
108
|
+
# @return [Boolean] true if the result indicates the desired finiteness state
|
109
|
+
# @rbs ((bool | Numeric)? result) -> bool
|
110
|
+
def interpret_result(result)
|
111
|
+
case result
|
112
|
+
when true, false
|
113
|
+
result
|
114
|
+
when nil
|
115
|
+
false
|
116
|
+
else
|
117
|
+
true
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|