domainic-type 0.1.0.alpha.2.1.0 → 0.1.0.alpha.3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE +1 -1
  4. data/README.md +28 -4
  5. data/lib/domainic/type/accessors.rb +41 -0
  6. data/lib/domainic/type/behavior/enumerable_behavior.rb +262 -0
  7. data/lib/domainic/type/behavior/numeric_behavior.rb +340 -0
  8. data/lib/domainic/type/behavior/sizable_behavior.rb +246 -0
  9. data/lib/domainic/type/behavior/string_behavior.rb +379 -0
  10. data/lib/domainic/type/behavior.rb +239 -0
  11. data/lib/domainic/type/config/registry.yml +101 -0
  12. data/lib/domainic/type/constraint/behavior.rb +342 -0
  13. data/lib/domainic/type/constraint/constraints/all_constraint.rb +81 -0
  14. data/lib/domainic/type/constraint/constraints/and_constraint.rb +105 -0
  15. data/lib/domainic/type/constraint/constraints/any_constraint.rb +83 -0
  16. data/lib/domainic/type/constraint/constraints/case_constraint.rb +104 -0
  17. data/lib/domainic/type/constraint/constraints/character_set_constraint.rb +111 -0
  18. data/lib/domainic/type/constraint/constraints/divisibility_constraint.rb +126 -0
  19. data/lib/domainic/type/constraint/constraints/emptiness_constraint.rb +69 -0
  20. data/lib/domainic/type/constraint/constraints/equality_constraint.rb +75 -0
  21. data/lib/domainic/type/constraint/constraints/finiteness_constraint.rb +123 -0
  22. data/lib/domainic/type/constraint/constraints/inclusion_constraint.rb +74 -0
  23. data/lib/domainic/type/constraint/constraints/match_pattern_constraint.rb +87 -0
  24. data/lib/domainic/type/constraint/constraints/method_presence_constraint.rb +72 -0
  25. data/lib/domainic/type/constraint/constraints/none_constraint.rb +83 -0
  26. data/lib/domainic/type/constraint/constraints/nor_constraint.rb +105 -0
  27. data/lib/domainic/type/constraint/constraints/not_constraint.rb +76 -0
  28. data/lib/domainic/type/constraint/constraints/or_constraint.rb +106 -0
  29. data/lib/domainic/type/constraint/constraints/ordering_constraint.rb +75 -0
  30. data/lib/domainic/type/constraint/constraints/parity_constraint.rb +102 -0
  31. data/lib/domainic/type/constraint/constraints/polarity_constraint.rb +147 -0
  32. data/lib/domainic/type/constraint/constraints/range_constraint.rb +135 -0
  33. data/lib/domainic/type/constraint/constraints/type_constraint.rb +110 -0
  34. data/lib/domainic/type/constraint/constraints/uniqueness_constraint.rb +69 -0
  35. data/lib/domainic/type/constraint/resolver.rb +172 -0
  36. data/lib/domainic/type/constraint/set.rb +266 -0
  37. data/lib/domainic/type/definitions.rb +364 -0
  38. data/lib/domainic/type/types/core/array_type.rb +48 -0
  39. data/lib/domainic/type/types/core/float_type.rb +39 -0
  40. data/lib/domainic/type/types/core/hash_type.rb +143 -0
  41. data/lib/domainic/type/types/core/integer_type.rb +38 -0
  42. data/lib/domainic/type/types/core/string_type.rb +51 -0
  43. data/lib/domainic/type/types/core/symbol_type.rb +51 -0
  44. data/lib/domainic/type/types/specification/anything_type.rb +22 -0
  45. data/lib/domainic/type/types/specification/duck_type.rb +55 -0
  46. data/lib/domainic/type/types/specification/enum_type.rb +26 -0
  47. data/lib/domainic/type/types/specification/union_type.rb +26 -0
  48. data/lib/domainic/type/types/specification/void_type.rb +12 -0
  49. data/lib/domainic/type.rb +7 -0
  50. data/lib/domainic-type.rb +3 -0
  51. data/sig/domainic/type/accessors.rbs +22 -0
  52. data/sig/domainic/type/behavior/enumerable_behavior.rbs +238 -0
  53. data/sig/domainic/type/behavior/numeric_behavior.rbs +299 -0
  54. data/sig/domainic/type/behavior/sizable_behavior.rbs +218 -0
  55. data/sig/domainic/type/behavior/string_behavior.rbs +315 -0
  56. data/sig/domainic/type/behavior.rbs +153 -0
  57. data/sig/domainic/type/constraint/behavior.rbs +258 -0
  58. data/sig/domainic/type/constraint/constraints/all_constraint.rbs +55 -0
  59. data/sig/domainic/type/constraint/constraints/and_constraint.rbs +72 -0
  60. data/sig/domainic/type/constraint/constraints/any_constraint.rbs +57 -0
  61. data/sig/domainic/type/constraint/constraints/case_constraint.rbs +73 -0
  62. data/sig/domainic/type/constraint/constraints/character_set_constraint.rbs +82 -0
  63. data/sig/domainic/type/constraint/constraints/divisibility_constraint.rbs +91 -0
  64. data/sig/domainic/type/constraint/constraints/emptiness_constraint.rbs +54 -0
  65. data/sig/domainic/type/constraint/constraints/equality_constraint.rbs +60 -0
  66. data/sig/domainic/type/constraint/constraints/finiteness_constraint.rbs +82 -0
  67. data/sig/domainic/type/constraint/constraints/inclusion_constraint.rbs +59 -0
  68. data/sig/domainic/type/constraint/constraints/match_pattern_constraint.rbs +66 -0
  69. data/sig/domainic/type/constraint/constraints/method_presence_constraint.rbs +51 -0
  70. data/sig/domainic/type/constraint/constraints/none_constraint.rbs +57 -0
  71. data/sig/domainic/type/constraint/constraints/nor_constraint.rbs +72 -0
  72. data/sig/domainic/type/constraint/constraints/not_constraint.rbs +56 -0
  73. data/sig/domainic/type/constraint/constraints/or_constraint.rbs +74 -0
  74. data/sig/domainic/type/constraint/constraints/ordering_constraint.rbs +60 -0
  75. data/sig/domainic/type/constraint/constraints/parity_constraint.rbs +71 -0
  76. data/sig/domainic/type/constraint/constraints/polarity_constraint.rbs +101 -0
  77. data/sig/domainic/type/constraint/constraints/range_constraint.rbs +88 -0
  78. data/sig/domainic/type/constraint/constraints/type_constraint.rbs +86 -0
  79. data/sig/domainic/type/constraint/constraints/uniqueness_constraint.rbs +54 -0
  80. data/sig/domainic/type/constraint/resolver.rbs +117 -0
  81. data/sig/domainic/type/constraint/set.rbs +159 -0
  82. data/sig/domainic/type/definitions.rbs +304 -0
  83. data/sig/domainic/type/types/core/array_type.rbs +42 -0
  84. data/sig/domainic/type/types/core/float_type.rbs +33 -0
  85. data/sig/domainic/type/types/core/hash_type.rbs +107 -0
  86. data/sig/domainic/type/types/core/integer_type.rbs +32 -0
  87. data/sig/domainic/type/types/core/string_type.rbs +45 -0
  88. data/sig/domainic/type/types/core/symbol_type.rbs +45 -0
  89. data/sig/domainic/type/types/specification/anything_type.rbs +14 -0
  90. data/sig/domainic/type/types/specification/duck_type.rbs +41 -0
  91. data/sig/domainic/type/types/specification/enum_type.rbs +14 -0
  92. data/sig/domainic/type/types/specification/union_type.rbs +14 -0
  93. data/sig/domainic/type/types/specification/void_type.rbs +8 -0
  94. data/sig/domainic/type.rbs +5 -0
  95. data/sig/domainic-type.rbs +1 -0
  96. data/sig/manifest.yaml +2 -0
  97. metadata +108 -71
@@ -0,0 +1,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