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