domainic-type 0.1.0.alpha.3.2.0 → 0.1.0.alpha.3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +11 -0
- data/README.md +66 -10
- data/docs/USAGE.md +787 -0
- data/lib/domainic/type/accessors.rb +3 -2
- data/lib/domainic/type/behavior/date_time_behavior.rb +121 -37
- data/lib/domainic/type/behavior.rb +16 -0
- data/lib/domainic/type/config/registry.yml +24 -0
- data/lib/domainic/type/constraint/constraints/nor_constraint.rb +1 -1
- data/lib/domainic/type/constraint/constraints/predicate_constraint.rb +76 -0
- data/lib/domainic/type/definitions.rb +212 -0
- data/lib/domainic/type/types/core/complex_type.rb +122 -0
- data/lib/domainic/type/types/core/range_type.rb +47 -0
- data/lib/domainic/type/types/core/rational_type.rb +38 -0
- data/lib/domainic/type/types/core_extended/big_decimal_type.rb +34 -0
- data/lib/domainic/type/types/core_extended/set_type.rb +34 -0
- data/lib/domainic/type/types/datetime/date_time_string_type.rb +156 -0
- data/lib/domainic/type/types/datetime/timestamp_type.rb +50 -0
- data/sig/domainic/type/accessors.rbs +2 -2
- data/sig/domainic/type/behavior/date_time_behavior.rbs +35 -23
- data/sig/domainic/type/behavior.rbs +9 -0
- data/sig/domainic/type/constraint/constraints/predicate_constraint.rbs +56 -0
- data/sig/domainic/type/definitions.rbs +165 -0
- data/sig/domainic/type/types/core/complex_type.rbs +96 -0
- data/sig/domainic/type/types/core/range_type.rbs +41 -0
- data/sig/domainic/type/types/core/rational_type.rbs +32 -0
- data/sig/domainic/type/types/core_extended/big_decimal_type.rbs +27 -0
- data/sig/domainic/type/types/core_extended/set_type.rbs +27 -0
- data/sig/domainic/type/types/datetime/date_time_string_type.rbs +124 -0
- data/sig/domainic/type/types/datetime/timestamp_type.rbs +44 -0
- metadata +25 -6
@@ -4,12 +4,12 @@ module Domainic
|
|
4
4
|
module Type
|
5
5
|
# @rbs!
|
6
6
|
# type accessor = :abs | :begin | :chars | :class | :count | :end | :entries | :first | :keys | :last | :length |
|
7
|
-
# :self | :size | :values
|
7
|
+
# :real | :self | :size | :values
|
8
8
|
|
9
9
|
# A list of valid access methods that can be used to retrieve values for constraint validation.
|
10
10
|
# These methods represent common Ruby interfaces for accessing collection sizes, ranges, and values.
|
11
11
|
#
|
12
|
-
# - :abs
|
12
|
+
# - :abs, :real - For absolute values
|
13
13
|
# - :begin, :end - For Range-like objects
|
14
14
|
# - :class - For type checking
|
15
15
|
# - :count, :length, :size - For measuring collections
|
@@ -27,6 +27,7 @@ module Domainic
|
|
27
27
|
entries
|
28
28
|
chars
|
29
29
|
abs
|
30
|
+
real
|
30
31
|
count
|
31
32
|
size
|
32
33
|
length
|
@@ -28,13 +28,93 @@ module Domainic
|
|
28
28
|
# @author {https://aaronmallen.me Aaron Allen}
|
29
29
|
# @since 0.1.0
|
30
30
|
module DateTimeBehavior
|
31
|
+
# The supported date/time patterns for parsing date/time strings
|
32
|
+
#
|
33
|
+
# @note This list is ordered from most specific to least specific to ensure that the most specific patterns are
|
34
|
+
# tried first. This is important because some patterns are more lenient than others and may match a wider range
|
35
|
+
# of input strings.
|
36
|
+
#
|
37
|
+
# @return [Array<String>] the supported date/time patterns
|
38
|
+
DATETIME_PATTERNS = [
|
39
|
+
# ISO 8601 variants (most specific first)
|
40
|
+
'%Y-%m-%dT%H:%M:%S.%N%:z', # 2024-01-01T12:00:00.000+00:00
|
41
|
+
'%Y-%m-%dT%H:%M:%S%:z', # 2024-01-01T12:00:00+00:00
|
42
|
+
'%Y-%m-%dT%H:%M:%S.%N', # 2024-01-01T12:00:00.000
|
43
|
+
'%Y-%m-%dT%H:%M:%S', # 2024-01-01T12:00:00
|
44
|
+
'%Y%m%dT%H%M%S%z', # 20240101T120000+0000 (basic format)
|
45
|
+
|
46
|
+
# RFC formats
|
47
|
+
'%a, %d %b %Y %H:%M:%S %z', # Thu, 31 Jan 2024 13:30:00 +0000
|
48
|
+
'%d %b %Y %H:%M:%S %z', # 31 Jan 2024 13:30:00 +0000
|
49
|
+
|
50
|
+
# Common datetime formats with timezone
|
51
|
+
'%Y-%m-%d %H:%M:%S %z', # 2024-01-01 12:00:00 +0000
|
52
|
+
'%d/%m/%Y %H:%M:%S %z', # 31/01/2024 12:00:00 +0000
|
53
|
+
|
54
|
+
# Full date + time formats (24h)
|
55
|
+
'%Y-%m-%d %H:%M:%S', # 2024-01-01 12:00:00
|
56
|
+
'%Y-%m-%d %H:%M', # 2024-01-01 12:00
|
57
|
+
'%d/%m/%Y %H:%M:%S', # 31/01/2024 12:00:00
|
58
|
+
'%d/%m/%Y %H:%M', # 31/01/2024 12:00
|
59
|
+
'%d-%m-%Y %H:%M:%S', # 31-01-2024 12:00:00
|
60
|
+
'%d-%m-%Y %H:%M', # 31-01-2024 12:00
|
61
|
+
'%Y/%m/%d %H:%M:%S', # 2024/01/31 12:00:00
|
62
|
+
'%Y/%m/%d %H:%M', # 2024/01/31 12:00
|
63
|
+
|
64
|
+
# Full date + time formats (12h)
|
65
|
+
'%Y-%m-%d %I:%M:%S %p', # 2024-01-01 01:30:00 PM
|
66
|
+
'%Y-%m-%d %I:%M %p', # 2024-01-01 01:30 PM
|
67
|
+
'%d/%m/%Y %I:%M:%S %p', # 31/01/2024 01:30:00 PM
|
68
|
+
'%d/%m/%Y %I:%M %p', # 31/01/2024 01:30 PM
|
69
|
+
|
70
|
+
# Full month name formats
|
71
|
+
'%B %d, %Y %H:%M:%S', # January 31, 2024 12:00:00
|
72
|
+
'%B %d, %Y %H:%M', # January 31, 2024 12:00
|
73
|
+
'%d %B %Y %H:%M:%S', # 31 January 2024 12:00:00
|
74
|
+
'%d %B %Y %H:%M', # 31 January 2024 12:00
|
75
|
+
|
76
|
+
# Abbreviated month name formats
|
77
|
+
'%b %d, %Y %H:%M:%S', # Jan 31, 2024 12:00:00
|
78
|
+
'%b %d, %Y %H:%M', # Jan 31, 2024 12:00
|
79
|
+
'%d %b %Y %H:%M:%S', # 31 Jan 2024 12:00:00
|
80
|
+
'%d %b %Y %H:%M', # 31 Jan 2024 12:00
|
81
|
+
|
82
|
+
# Date-only formats (in order of specificity)
|
83
|
+
'%Y-%m-%d', # 2024-01-31
|
84
|
+
'%Y%m%d', # 20240131
|
85
|
+
'%B %d, %Y', # January 31, 2024
|
86
|
+
'%d %B %Y', # 31 January 2024
|
87
|
+
'%b %d, %Y', # Jan 31, 2024
|
88
|
+
'%d %b %Y', # 31 Jan 2024
|
89
|
+
'%d/%m/%Y', # 31/01/2024
|
90
|
+
'%d-%m-%Y', # 31-01-2024
|
91
|
+
'%Y/%m/%d', # 2024/01/31
|
92
|
+
'%m/%d/%Y' # 01/31/2024 (US format - last to avoid ambiguity)
|
93
|
+
].freeze #: Array[String]
|
94
|
+
|
95
|
+
# Coerce a value to a Date, DateTime, or Time object
|
96
|
+
#
|
97
|
+
# @return [Proc] a lambda that coerces a value to a Date, DateTime, or Time object
|
31
98
|
TO_DATETIME_COERCER = lambda { |value|
|
32
|
-
|
99
|
+
case value
|
100
|
+
when Date, DateTime, Time
|
33
101
|
value
|
102
|
+
when Integer
|
103
|
+
Time.at(value).to_datetime
|
104
|
+
when String
|
105
|
+
DATETIME_PATTERNS.each do |pattern|
|
106
|
+
result = DateTime.strptime(value, pattern)
|
107
|
+
# Validate the parsing preserved the original values by reformatting
|
108
|
+
# the result with the same pattern and comparing
|
109
|
+
return result if value == result.strftime(pattern)
|
110
|
+
rescue ArgumentError
|
111
|
+
next
|
112
|
+
end
|
113
|
+
DateTime.parse(value) # Fallback to Ruby's built-in parser and allow it to raise.
|
34
114
|
else
|
35
|
-
DateTime.parse(value.
|
115
|
+
DateTime.parse(value) # Fallback to Ruby's built-in parser and allow it to raise.
|
36
116
|
end
|
37
|
-
} #:
|
117
|
+
} #: ^(Date | DateTime | Integer | String | Time value) -> (Date | DateTime | Time)
|
38
118
|
|
39
119
|
class << self
|
40
120
|
private
|
@@ -44,39 +124,39 @@ module Domainic
|
|
44
124
|
# @note this in my opinion is better than polluting the namespace of the including class even with a private
|
45
125
|
# method. This way, the method is only available within the module itself. See {#being_between}.
|
46
126
|
#
|
47
|
-
# @param after [Date, DateTime, String, Time, nil] minimum size value from positional args
|
48
|
-
# @param before [Date, DateTime, String, Time, nil] maximum size value from positional args
|
127
|
+
# @param after [Date, DateTime, Integer, String, Time, nil] minimum size value from positional args
|
128
|
+
# @param before [Date, DateTime, Integer, String, Time, nil] maximum size value from positional args
|
49
129
|
# @param options [Hash] keyword arguments containing after/before values
|
50
130
|
#
|
51
131
|
# @raise [ArgumentError] if minimum or maximum value can't be determined
|
52
|
-
# @return [Array<Date, DateTime, String, Time, nil>] parsed [after, before] values
|
132
|
+
# @return [Array<Date, DateTime, Integer, String, Time, nil>] parsed [after, before] values
|
53
133
|
# @rbs (
|
54
|
-
# (Date | DateTime | String | Time)? after,
|
55
|
-
# (Date | DateTime | String | Time)? before,
|
56
|
-
# Hash[Symbol, (Date | DateTime | String | Time)?] options
|
57
|
-
# ) -> Array[(Date | DateTime | String | Time)?]
|
134
|
+
# (Date | DateTime | Integer | String | Time)? after,
|
135
|
+
# (Date | DateTime | Integer | String | Time)? before,
|
136
|
+
# Hash[Symbol, (Date | DateTime | Integer | String | Time)?] options
|
137
|
+
# ) -> Array[(Date | DateTime | Integer | String | Time)?]
|
58
138
|
def parse_being_between_arguments!(after, before, options)
|
59
139
|
after ||= options[:after]
|
60
140
|
before ||= options[:before]
|
61
141
|
raise_being_between_argument_error!(caller, after, before, options) if after.nil? || before.nil?
|
62
142
|
|
63
|
-
[after, before] #: Array[(Date | DateTime | String | Time)?]
|
143
|
+
[after, before] #: Array[(Date | DateTime | Integer | String | Time)?]
|
64
144
|
end
|
65
145
|
|
66
146
|
# Raise appropriate ArgumentError for being_between
|
67
147
|
#
|
68
148
|
# @param original_caller [Array<String>] caller stack for error
|
69
|
-
# @param after [Date, DateTime, String, Time, nil] after value from positional args
|
70
|
-
# @param before [Date, DateTime, String, Time, nil] before value from positional args
|
149
|
+
# @param after [Date, DateTime, Integer, String, Time, nil] after value from positional args
|
150
|
+
# @param before [Date, DateTime, Integer, String, Time, nil] before value from positional args
|
71
151
|
# @param options [Hash] keyword arguments containing after/before values
|
72
152
|
#
|
73
153
|
# @raise [ArgumentError] with appropriate message
|
74
154
|
# @return [void]
|
75
155
|
# @rbs (
|
76
156
|
# Array[String] original_caller,
|
77
|
-
# (Date | DateTime | String | Time)? after,
|
78
|
-
# (Date | DateTime | String | Time)? before,
|
79
|
-
# Hash[Symbol, (Date | DateTime | String | Time)?] options
|
157
|
+
# (Date | DateTime | Integer | String | Time)? after,
|
158
|
+
# (Date | DateTime | Integer | String | Time)? before,
|
159
|
+
# Hash[Symbol, (Date | DateTime | Integer | String | Time)?] options
|
80
160
|
# ) -> void
|
81
161
|
def raise_being_between_argument_error!(original_caller, after, before, options)
|
82
162
|
message = if options.empty?
|
@@ -93,66 +173,70 @@ module Domainic
|
|
93
173
|
|
94
174
|
# Constrain the value to be chronologically after a given date/time
|
95
175
|
#
|
96
|
-
# @param other [Date, DateTime, String, Time] the date/time to compare against
|
176
|
+
# @param other [Date, DateTime, Integer, String, Time] the date/time to compare against
|
97
177
|
# @return [self] self for method chaining
|
98
|
-
# @rbs (Date | DateTime | String | Time other) -> Behavior
|
178
|
+
# @rbs (Date | DateTime | Integer | String | Time other) -> Behavior
|
99
179
|
def being_after(other)
|
100
180
|
# @type self: Object & Behavior
|
101
|
-
constrain :self, :range, { minimum: other },
|
181
|
+
constrain :self, :range, { minimum: TO_DATETIME_COERCER.call(other) },
|
102
182
|
coerce_with: TO_DATETIME_COERCER, description: 'being', inclusive: false
|
103
183
|
end
|
104
184
|
alias after being_after
|
105
185
|
|
106
186
|
# Constrain the value to be chronologically before a given date/time
|
107
187
|
#
|
108
|
-
# @param other [Date, DateTime, String, Time] the date/time to compare against
|
188
|
+
# @param other [Date, DateTime, Integer, String, Time] the date/time to compare against
|
109
189
|
# @return [self] self for method chaining
|
110
|
-
# @rbs (Date | DateTime | String | Time other) -> Behavior
|
190
|
+
# @rbs (Date | DateTime | Integer | String | Time other) -> Behavior
|
111
191
|
def being_before(other)
|
112
192
|
# @type self: Object & Behavior
|
113
|
-
constrain :self, :range, { maximum: other },
|
193
|
+
constrain :self, :range, { maximum: TO_DATETIME_COERCER.call(other) },
|
114
194
|
coerce_with: TO_DATETIME_COERCER, description: 'being', inclusive: false
|
115
195
|
end
|
116
196
|
alias before being_before
|
117
197
|
|
118
198
|
# Constrain the value to be chronologically between two date/times
|
119
199
|
#
|
120
|
-
# @param after [Date, DateTime, String, Time] the earliest allowed date/time
|
121
|
-
# @param before [Date, DateTime, String, Time] the latest allowed date/time
|
200
|
+
# @param after [Date, DateTime, Integer, String, Time] the earliest allowed date/time
|
201
|
+
# @param before [Date, DateTime, Integer, String, Time] the latest allowed date/time
|
122
202
|
# @param options [Hash] alternative way to specify after/before via keywords
|
123
|
-
# @option options [Date, DateTime, String, Time] :after earliest allowed date/time
|
124
|
-
# @option options [Date, DateTime, String, Time] :before latest allowed date/time
|
203
|
+
# @option options [Date, DateTime, Integer, String, Time] :after earliest allowed date/time
|
204
|
+
# @option options [Date, DateTime, Integer, String, Time] :before latest allowed date/time
|
125
205
|
# @return [self] self for method chaining
|
126
|
-
# @rbs (
|
206
|
+
# @rbs (
|
207
|
+
# Date | DateTime | Integer | String | Time after,
|
208
|
+
# Date | DateTime | Integer | String | Time before
|
209
|
+
# ) -> Behavior
|
127
210
|
def being_between(after = nil, before = nil, **options)
|
128
211
|
# @type self: Object & Behavior
|
129
212
|
after, before =
|
130
213
|
DateTimeBehavior.send(:parse_being_between_arguments!, after, before, options.transform_keys(&:to_sym))
|
131
|
-
constrain :self, :range,
|
214
|
+
constrain :self, :range,
|
215
|
+
{ minimum: TO_DATETIME_COERCER.call(after), maximum: TO_DATETIME_COERCER.call(before) },
|
132
216
|
coerce_with: TO_DATETIME_COERCER, description: 'being', inclusive: false
|
133
217
|
end
|
134
218
|
alias between being_between
|
135
219
|
|
136
220
|
# Constrain the value to be exactly equal to a given date/time
|
137
221
|
#
|
138
|
-
# @param other [Date, DateTime, String, Time] the date/time to compare against
|
222
|
+
# @param other [Date, DateTime, Integer, String, Time] the date/time to compare against
|
139
223
|
# @return [self] self for method chaining
|
140
|
-
# @rbs (Date | DateTime | String | Time other) -> Behavior
|
224
|
+
# @rbs (Date | DateTime | Integer | String | Time other) -> Behavior
|
141
225
|
def being_equal_to(other)
|
142
226
|
# @type self: Object & Behavior
|
143
|
-
constrain :self, :equality, other,
|
227
|
+
constrain :self, :equality, TO_DATETIME_COERCER.call(other),
|
144
228
|
coerce_with: TO_DATETIME_COERCER, description: 'being'
|
145
229
|
end
|
146
230
|
alias at being_equal_to
|
147
231
|
|
148
232
|
# Constrain the value to be chronologically on or after a given date/time
|
149
233
|
#
|
150
|
-
# @param other [Date, DateTime, String, Time] the date/time to compare against
|
234
|
+
# @param other [Date, DateTime, Integer, String, Time] the date/time to compare against
|
151
235
|
# @return [self] self for method chaining
|
152
|
-
# @rbs (Date | DateTime | String | Time other) -> Behavior
|
236
|
+
# @rbs (Date | DateTime | Integer | String | Time other) -> Behavior
|
153
237
|
def being_on_or_after(other)
|
154
238
|
# @type self: Object & Behavior
|
155
|
-
constrain :self, :range, { minimum: other },
|
239
|
+
constrain :self, :range, { minimum: TO_DATETIME_COERCER.call(other) },
|
156
240
|
coerce_with: TO_DATETIME_COERCER, description: 'being'
|
157
241
|
end
|
158
242
|
alias at_or_after being_on_or_after
|
@@ -161,12 +245,12 @@ module Domainic
|
|
161
245
|
|
162
246
|
# Constrain the value to be chronologically on or before a given date/time
|
163
247
|
#
|
164
|
-
# @param other [Date, DateTime, String, Time] the date/time to compare against
|
248
|
+
# @param other [Date, DateTime, Integer, String, Time] the date/time to compare against
|
165
249
|
# @return [self] self for method chaining
|
166
|
-
# @rbs (Date | DateTime | String | Time other) -> Behavior
|
250
|
+
# @rbs (Date | DateTime | Integer | String | Time other) -> Behavior
|
167
251
|
def being_on_or_before(other)
|
168
252
|
# @type self: Object & Behavior
|
169
|
-
constrain :self, :range, { maximum: other },
|
253
|
+
constrain :self, :range, { maximum: TO_DATETIME_COERCER.call(other) },
|
170
254
|
coerce_with: TO_DATETIME_COERCER, description: 'being'
|
171
255
|
end
|
172
256
|
alias at_or_before being_on_or_before
|
@@ -186,6 +186,22 @@ module Domainic
|
|
186
186
|
end
|
187
187
|
end
|
188
188
|
|
189
|
+
# Add a custom constraint to this type.
|
190
|
+
#
|
191
|
+
# @param proc [Proc] the constraint to add
|
192
|
+
# @param accessor [Type::Accessor] the accessor to constrain
|
193
|
+
# @param options [Hash{Symbol => Object}] additional constraint options
|
194
|
+
#
|
195
|
+
# @return [self] for chaining constraints
|
196
|
+
# @rbs (
|
197
|
+
# Proc proc,
|
198
|
+
# ?accessor: Type::accessor,
|
199
|
+
# **untyped options
|
200
|
+
# ) -> Behavior
|
201
|
+
def satisfies(proc, accessor: :self, **options)
|
202
|
+
constrain accessor, :predicate, proc, **options
|
203
|
+
end
|
204
|
+
|
189
205
|
# Convert the type to a String representation.
|
190
206
|
#
|
191
207
|
# @return [String] The type as a String
|
@@ -62,6 +62,9 @@ constraints:
|
|
62
62
|
polarity:
|
63
63
|
constant: Domainic::Type::Constraint::PolarityConstraint
|
64
64
|
require_path: domainic/type/constraint/constraints/polarity_constraint
|
65
|
+
predicate:
|
66
|
+
constant: Domainic::Type::Constraint::PredicateConstraint
|
67
|
+
require_path: domainic/type/constraint/constraints/predicate_constraint
|
65
68
|
range:
|
66
69
|
constant: Domainic::Type::Constraint::RangeConstraint
|
67
70
|
require_path: domainic/type/constraint/constraints/range_constraint
|
@@ -78,6 +81,12 @@ types:
|
|
78
81
|
anything:
|
79
82
|
constant: Domainic::Type::AnythingType
|
80
83
|
require_path: domainic/type/types/specification/anything_type
|
84
|
+
big_decimal:
|
85
|
+
constant: Domainic::Type::BigDecimalType
|
86
|
+
require_path: domainic/type/types/core_extended/big_decimal_type
|
87
|
+
complex:
|
88
|
+
constant: Domainic::Type::ComplexType
|
89
|
+
require_path: domainic/type/types/core/complex_type
|
81
90
|
cuid:
|
82
91
|
constant: Domainic::Type::CUIDType
|
83
92
|
require_path: domainic/type/types/identifier/cuid_type
|
@@ -87,6 +96,9 @@ types:
|
|
87
96
|
date_time:
|
88
97
|
constant: Domainic::Type::DateTimeType
|
89
98
|
require_path: domainic/type/types/datetime/date_time_type
|
99
|
+
date_time_string:
|
100
|
+
constant: Domainic::Type::DateTimeStringType
|
101
|
+
require_path: domainic/type/types/datetime/date_time_string_type
|
90
102
|
duck:
|
91
103
|
constant: Domainic::Type::DuckType
|
92
104
|
require_path: domainic/type/types/specification/duck_type
|
@@ -111,6 +123,15 @@ types:
|
|
111
123
|
integer:
|
112
124
|
constant: Domainic::Type::IntegerType
|
113
125
|
require_path: domainic/type/types/core/integer_type
|
126
|
+
range:
|
127
|
+
constant: Domainic::Type::RangeType
|
128
|
+
require_path: domainic/type/types/core/range_type
|
129
|
+
rational:
|
130
|
+
constant: Domainic::Type::RationalType
|
131
|
+
require_path: domainic/type/types/core/rational_type
|
132
|
+
set:
|
133
|
+
constant: Domainic::Type::SetType
|
134
|
+
require_path: domainic/type/types/core_extended/set_type
|
114
135
|
string:
|
115
136
|
constant: Domainic::Type::StringType
|
116
137
|
require_path: domainic/type/types/core/string_type
|
@@ -120,6 +141,9 @@ types:
|
|
120
141
|
time:
|
121
142
|
constant: Domainic::Type::TimeType
|
122
143
|
require_path: domainic/type/types/datetime/time_type
|
144
|
+
timestamp:
|
145
|
+
constant: Domainic::Type::TimestampType
|
146
|
+
require_path: domainic/type/types/datetime/timestamp_type
|
123
147
|
union:
|
124
148
|
constant: Domainic::Type::UnionType
|
125
149
|
require_path: domainic/type/types/specification/union_type
|
@@ -39,7 +39,7 @@ module Domainic
|
|
39
39
|
# @rbs override
|
40
40
|
def short_description
|
41
41
|
descriptions = @expected.map(&:short_description)
|
42
|
-
return descriptions.first if descriptions.size == 1
|
42
|
+
return "not #{descriptions.first}" if descriptions.size == 1
|
43
43
|
|
44
44
|
*first, last = descriptions
|
45
45
|
"#{first.join(', ')} nor #{last}"
|
@@ -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 for validating values using a custom predicate function.
|
9
|
+
#
|
10
|
+
# This constraint allows for custom validation logic through a Proc that returns
|
11
|
+
# a boolean value. It enables users to create arbitrary validation rules when
|
12
|
+
# the built-in constraints don't cover their specific needs.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# constraint = PredicateConstraint.new(:self, ->(x) { x > 0 })
|
16
|
+
# constraint.satisfied?(1) # => true
|
17
|
+
# constraint.satisfied?(-1) # => false
|
18
|
+
#
|
19
|
+
# @example With custom violation description
|
20
|
+
# constraint = PredicateConstraint.new(:self, ->(x) { x > 0 }, violation_description: 'not greater than zero')
|
21
|
+
# constraint.satisfied?(-1) # => false
|
22
|
+
# constraint.short_violation_description # => "not greater than zero"
|
23
|
+
#
|
24
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
25
|
+
# @since 0.1.0
|
26
|
+
class PredicateConstraint
|
27
|
+
# @rbs!
|
28
|
+
# type expected = ^(untyped value) -> bool
|
29
|
+
#
|
30
|
+
# type options = { ?violation_description: String}
|
31
|
+
|
32
|
+
include Behavior #[expected, untyped, options]
|
33
|
+
|
34
|
+
# Get a description of what the constraint expects.
|
35
|
+
#
|
36
|
+
# @note This constraint type does not provide a description as predicates are arbitrary.
|
37
|
+
#
|
38
|
+
# @return [String] an empty string
|
39
|
+
# @rbs override
|
40
|
+
def short_description = ''
|
41
|
+
|
42
|
+
# Get a description of why the predicate validation failed.
|
43
|
+
#
|
44
|
+
# @return [String] the custom violation description if provided
|
45
|
+
# @rbs override
|
46
|
+
def short_violation_description
|
47
|
+
# @type ivar @options: { ?violation_description: String }
|
48
|
+
@options.fetch(:violation_description, '')
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
# Check if the value satisfies the predicate function.
|
54
|
+
#
|
55
|
+
# @return [Boolean] true if the predicate returns true
|
56
|
+
# @rbs override
|
57
|
+
def satisfies_constraint?
|
58
|
+
@expected.call(@actual)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Validate that the expectation is a Proc.
|
62
|
+
#
|
63
|
+
# @param expectation [Object] the expectation to validate
|
64
|
+
#
|
65
|
+
# @raise [ArgumentError] if the expectation is not a Proc
|
66
|
+
# @return [void]
|
67
|
+
# @rbs override
|
68
|
+
def validate_expectation!(expectation)
|
69
|
+
return if expectation.is_a?(Proc)
|
70
|
+
|
71
|
+
raise ArgumentError, 'Expectation must be a Proc'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|