domainic-type 0.1.0.alpha.3.2.0 → 0.1.0.alpha.3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/README.md +66 -10
  4. data/docs/USAGE.md +787 -0
  5. data/lib/domainic/type/accessors.rb +3 -2
  6. data/lib/domainic/type/behavior/date_time_behavior.rb +121 -37
  7. data/lib/domainic/type/behavior.rb +16 -0
  8. data/lib/domainic/type/config/registry.yml +24 -0
  9. data/lib/domainic/type/constraint/constraints/nor_constraint.rb +1 -1
  10. data/lib/domainic/type/constraint/constraints/predicate_constraint.rb +76 -0
  11. data/lib/domainic/type/definitions.rb +212 -0
  12. data/lib/domainic/type/types/core/complex_type.rb +122 -0
  13. data/lib/domainic/type/types/core/range_type.rb +47 -0
  14. data/lib/domainic/type/types/core/rational_type.rb +38 -0
  15. data/lib/domainic/type/types/core_extended/big_decimal_type.rb +34 -0
  16. data/lib/domainic/type/types/core_extended/set_type.rb +34 -0
  17. data/lib/domainic/type/types/datetime/date_time_string_type.rb +156 -0
  18. data/lib/domainic/type/types/datetime/timestamp_type.rb +50 -0
  19. data/sig/domainic/type/accessors.rbs +2 -2
  20. data/sig/domainic/type/behavior/date_time_behavior.rbs +35 -23
  21. data/sig/domainic/type/behavior.rbs +9 -0
  22. data/sig/domainic/type/constraint/constraints/predicate_constraint.rbs +56 -0
  23. data/sig/domainic/type/definitions.rbs +165 -0
  24. data/sig/domainic/type/types/core/complex_type.rbs +96 -0
  25. data/sig/domainic/type/types/core/range_type.rbs +41 -0
  26. data/sig/domainic/type/types/core/rational_type.rbs +32 -0
  27. data/sig/domainic/type/types/core_extended/big_decimal_type.rbs +27 -0
  28. data/sig/domainic/type/types/core_extended/set_type.rbs +27 -0
  29. data/sig/domainic/type/types/datetime/date_time_string_type.rbs +124 -0
  30. data/sig/domainic/type/types/datetime/timestamp_type.rbs +44 -0
  31. 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 - For absolute values
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
- if [Date, DateTime, Time].any? { |type| value.is_a?(type) }
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.to_s)
115
+ DateTime.parse(value) # Fallback to Ruby's built-in parser and allow it to raise.
36
116
  end
37
- } #: Proc
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 (Date | DateTime | String | Time after, Date | DateTime | String | Time before) -> Behavior
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, { minimum: after, maximum: before },
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