domainic-type 0.1.0.alpha.3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5342f3d3bb47776a4cb27404a0a244d8e0d2761f14de0fc1efe0638ac354c387
4
- data.tar.gz: e5c55975ec267fba4623239df150111b6bb13eda48448ff4e026007e50342578
3
+ metadata.gz: e864e12054b6928f12f7465eace3657aaa736e264a1655e7838ec4f2e658ebed
4
+ data.tar.gz: ac7c71c1bd2c2f992bb6aca1306291838a4b7eabc77326130db553de8a9dc18f
5
5
  SHA512:
6
- metadata.gz: c1563338aa961165140ef8d72d22c04d9f46e5c08fd95c02b037d32b74f21570b136c7b07094093b4dd47c59b5be6b7cfaa003114ced0e4aad963a15305088e5
7
- data.tar.gz: 01751a3ccf57ddea40d185aa6d04e12ea7a78bd94c0770aeb4af8dbd449d43ac0eec5eda64f91fe1828ff05ab0e2a53d8b24b78f4c13347c9fbaeae343be928b
6
+ metadata.gz: 6029d7d12cb562dd7f2688472f0da97bcb548c544f163cf5215736d82ee0927e6f2c4ff87cc0d1818eefe722d10ac0ee2dd4ebccabff45412793340928e63cb8
7
+ data.tar.gz: 7ba6d7e41e431f2dd401bffecd60701a9dfeab35eeea3bef9085e97941d1b8327ddfd7cbe5febc577a0e1b3cce49147f6b686b56f1f6251257836d58327d198d
data/.yardopts ADDED
@@ -0,0 +1,11 @@
1
+ --title Domainic::Type
2
+ --readme README.md
3
+ --no-private
4
+ --protected
5
+ --markup markdown
6
+ --markup-provider redcarpet
7
+ --embed-mixins
8
+ --tag rbs
9
+ --hide-tag rbs
10
+ --files CHANGELOG.md,LICENSE,docs/USAGE.md
11
+ 'lib/**/*.rb'
data/docs/USAGE.md CHANGED
@@ -9,6 +9,7 @@ introduction and installation instructions.
9
9
  * [Core Concepts](#core-concepts)
10
10
  * [Basic Type Validation](#basic-type-validation)
11
11
  * [Type Constraints](#type-constraints)
12
+ * [Custom Constraints](#custom-constraints)
12
13
  * [Error Messages](#error-messages)
13
14
  * [Built-in Types](#built-in-types)
14
15
  * [Simple Types](#simple-types)
@@ -131,6 +132,51 @@ numbers = _Array
131
132
  .containing(42) # Must include 42
132
133
  ```
133
134
 
135
+ ### Custom Constraints
136
+
137
+ All types in Domainic::Type support adding custom constraints through the `satisfies` method. This is useful when you
138
+ need validation logic that goes beyond what the built-in constraints provide:
139
+
140
+ ```ruby
141
+ # Basic value range validation that can't be expressed with built-in constraints
142
+ kelvin = _Float.satisfies(
143
+ ->(value) { value >= 0.0 },
144
+ description: 'being a valid Kelvin temperature',
145
+ violation_description: 'temperature below absolute zero'
146
+ )
147
+
148
+ kelvin.validate!(0.0) # => true
149
+ kelvin.validate!(-1.0)
150
+ # => TypeError: Expected Float(being a valid Kelvin temperature), got Float(temperature below absolute zero)
151
+
152
+ # Validating relationships between values in complex objects
153
+ booking = _Hash
154
+ .of(_Symbol => _DateTime)
155
+ .containing_keys(:check_in, :check_out)
156
+ .satisfies(
157
+ ->(dates) { dates[:check_out] > dates[:check_in] },
158
+ description: 'having valid stay duration',
159
+ violation_description: 'check-out not after check-in'
160
+ )
161
+
162
+ # Access different parts of objects with accessors
163
+ balanced_ledger = _Array
164
+ .of(_Hash)
165
+ .satisfies(
166
+ ->(entries) { entries.sum { |e| e[:amount] }.zero? },
167
+ accessor: :entries,
168
+ description: 'having balanced transactions',
169
+ violation_description: 'transactions do not sum to zero'
170
+ )
171
+ ```
172
+
173
+ The `satisfies` method accepts:
174
+
175
+ * A predicate function that returns true/false for validating values
176
+ * Optional description that explains what makes a value valid
177
+ * Optional violation_description that explains why validation failed
178
+ * Optional accessor to validate specific parts of complex objects
179
+
134
180
  ### Error Messages
135
181
 
136
182
  Error messages clearly indicate what validation failed and why. For compound constraints, the error message shows all
@@ -410,14 +456,23 @@ Available in nilable variant `_DateTime?`.
410
456
 
411
457
  #### _DateTimeString
412
458
 
413
- String-based datetime validation with format constraints:
459
+ String-based datetime validation with format constraints and full datetime behavior support:
414
460
 
415
461
  ```ruby
416
462
  _DateTimeString # Basic datetime string validation
463
+ # Format constraints
417
464
  .having_american_format # MM/DD/YYYY format
418
465
  .having_european_format # DD.MM.YYYY format
419
466
  .having_iso8601_format # ISO 8601 format
420
467
  .having_rfc2822_format # RFC 2822 format
468
+
469
+ # Inherits all datetime constraints
470
+ .being_after('2024-01-01') # Must be after date
471
+ .being_before('2024-12-31') # Must be before date
472
+ .being_between( # Must be in range
473
+ '2024-01-01',
474
+ '2024-12-31'
475
+ )
421
476
  ```
422
477
 
423
478
  Also available as `_DateString` and in nilable variants `_DateTimeString?`, `_DateString?`.
@@ -103,10 +103,14 @@ module Domainic
103
103
  Time.at(value).to_datetime
104
104
  when String
105
105
  DATETIME_PATTERNS.each do |pattern|
106
- return DateTime.strptime(value, 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)
107
110
  rescue ArgumentError
108
111
  next
109
112
  end
113
+ DateTime.parse(value) # Fallback to Ruby's built-in parser and allow it to raise.
110
114
  else
111
115
  DateTime.parse(value) # Fallback to Ruby's built-in parser and allow it to raise.
112
116
  end
@@ -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
@@ -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
@@ -125,6 +125,15 @@ module Domainic
125
125
  # @return [void]
126
126
  def initialize: (**untyped) -> void
127
127
 
128
+ # Add a custom constraint to this type.
129
+ #
130
+ # @param proc [Proc] the constraint to add
131
+ # @param accessor [Type::Accessor] the accessor to constrain
132
+ # @param options [Hash{Symbol => Object}] additional constraint options
133
+ #
134
+ # @return [self] for chaining constraints
135
+ def satisfies: (Proc proc, ?accessor: Type::accessor, **untyped options) -> Behavior
136
+
128
137
  # Convert the type to a String representation.
129
138
  #
130
139
  # @return [String] The type as a String
@@ -0,0 +1,56 @@
1
+ module Domainic
2
+ module Type
3
+ module Constraint
4
+ # A constraint for validating values using a custom predicate function.
5
+ #
6
+ # This constraint allows for custom validation logic through a Proc that returns
7
+ # a boolean value. It enables users to create arbitrary validation rules when
8
+ # the built-in constraints don't cover their specific needs.
9
+ #
10
+ # @example Basic usage
11
+ # constraint = PredicateConstraint.new(:self, ->(x) { x > 0 })
12
+ # constraint.satisfied?(1) # => true
13
+ # constraint.satisfied?(-1) # => false
14
+ #
15
+ # @example With custom violation description
16
+ # constraint = PredicateConstraint.new(:self, ->(x) { x > 0 }, violation_description: 'not greater than zero')
17
+ # constraint.satisfied?(-1) # => false
18
+ # constraint.short_violation_description # => "not greater than zero"
19
+ #
20
+ # @author {https://aaronmallen.me Aaron Allen}
21
+ # @since 0.1.0
22
+ class PredicateConstraint
23
+ type expected = ^(untyped value) -> bool
24
+
25
+ type options = { ?violation_description: String }
26
+
27
+ include Behavior[expected, untyped, options]
28
+
29
+ # Get a description of what the constraint expects.
30
+ #
31
+ # @note This constraint type does not provide a description as predicates are arbitrary.
32
+ #
33
+ # @return [String] an empty string
34
+ def short_description: ...
35
+
36
+ # Get a description of why the predicate validation failed.
37
+ #
38
+ # @return [String] the custom violation description if provided
39
+ def short_violation_description: ...
40
+
41
+ # Check if the value satisfies the predicate function.
42
+ #
43
+ # @return [Boolean] true if the predicate returns true
44
+ def satisfies_constraint?: ...
45
+
46
+ # Validate that the expectation is a Proc.
47
+ #
48
+ # @param expectation [Object] the expectation to validate
49
+ #
50
+ # @raise [ArgumentError] if the expectation is not a Proc
51
+ # @return [void]
52
+ def validate_expectation!: ...
53
+ end
54
+ end
55
+ end
56
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: domainic-type
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha.3.3.0
4
+ version: 0.1.0.alpha.3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Allen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-27 00:00:00.000000000 Z
11
+ date: 2024-12-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Stop wrestling with complex type validations and unclear error messages.
14
14
  Domainic::Type brings type validation to Ruby that is both powerful and delightful
@@ -21,6 +21,7 @@ executables: []
21
21
  extensions: []
22
22
  extra_rdoc_files: []
23
23
  files:
24
+ - ".yardopts"
24
25
  - CHANGELOG.md
25
26
  - LICENSE
26
27
  - README.md
@@ -59,6 +60,7 @@ files:
59
60
  - lib/domainic/type/constraint/constraints/ordering_constraint.rb
60
61
  - lib/domainic/type/constraint/constraints/parity_constraint.rb
61
62
  - lib/domainic/type/constraint/constraints/polarity_constraint.rb
63
+ - lib/domainic/type/constraint/constraints/predicate_constraint.rb
62
64
  - lib/domainic/type/constraint/constraints/range_constraint.rb
63
65
  - lib/domainic/type/constraint/constraints/type_constraint.rb
64
66
  - lib/domainic/type/constraint/constraints/uniqueness_constraint.rb
@@ -125,6 +127,7 @@ files:
125
127
  - sig/domainic/type/constraint/constraints/ordering_constraint.rbs
126
128
  - sig/domainic/type/constraint/constraints/parity_constraint.rbs
127
129
  - sig/domainic/type/constraint/constraints/polarity_constraint.rbs
130
+ - sig/domainic/type/constraint/constraints/predicate_constraint.rbs
128
131
  - sig/domainic/type/constraint/constraints/range_constraint.rbs
129
132
  - sig/domainic/type/constraint/constraints/type_constraint.rbs
130
133
  - sig/domainic/type/constraint/constraints/uniqueness_constraint.rbs
@@ -159,15 +162,16 @@ files:
159
162
  - sig/domainic/type/types/specification/union_type.rbs
160
163
  - sig/domainic/type/types/specification/void_type.rbs
161
164
  - sig/manifest.yaml
162
- homepage: https://github.com/domainic/domainic/tree/domainic-type-v0.1.0-alpha.3.3.0/domainic-type
165
+ homepage: https://github.com/domainic/domainic/tree/domainic-type-v0.1.0-alpha.3.4.0/domainic-type
163
166
  licenses:
164
167
  - MIT
165
168
  metadata:
166
169
  bug_tracker_uri: https://github.com/domainic/domainic/issues
167
- changelog_uri: https://github.com/domainic/domainic/releases/tag/domainic-type-v0.1.0-alpha.3.3.0
168
- homepage_uri: https://github.com/domainic/domainic/tree/domainic-type-v0.1.0-alpha.3.3.0/domainic-type
170
+ changelog_uri: https://github.com/domainic/domainic/releases/tag/domainic-type-v0.1.0-alpha.3.4.0
171
+ documentation_uri: https://rubydoc.info/gems/domainic-type/0.1.0.alpha.3.4.0
172
+ homepage_uri: https://github.com/domainic/domainic/tree/domainic-type-v0.1.0-alpha.3.4.0/domainic-type
169
173
  rubygems_mfa_required: 'true'
170
- source_code_uri: https://github.com/domainic/domainic/tree/domainic-type-v0.1.0-alpha.3.3.0/domainic-type
174
+ source_code_uri: https://github.com/domainic/domainic/tree/domainic-type-v0.1.0-alpha.3.4.0/domainic-type
171
175
  post_install_message:
172
176
  rdoc_options: []
173
177
  require_paths: