domainic-attributer 0.1.0 → 0.2.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.md +32 -1
  4. data/README.md +42 -355
  5. data/docs/USAGE.md +723 -0
  6. data/lib/domainic/attributer/attribute/callback.rb +21 -9
  7. data/lib/domainic/attributer/attribute/coercer.rb +28 -13
  8. data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +16 -13
  9. data/lib/domainic/attributer/attribute/signature.rb +43 -32
  10. data/lib/domainic/attributer/attribute/validator.rb +46 -16
  11. data/lib/domainic/attributer/attribute.rb +28 -18
  12. data/lib/domainic/attributer/attribute_set.rb +21 -19
  13. data/lib/domainic/attributer/class_methods.rb +136 -83
  14. data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +64 -22
  15. data/lib/domainic/attributer/dsl/attribute_builder.rb +515 -26
  16. data/lib/domainic/attributer/dsl/initializer.rb +23 -18
  17. data/lib/domainic/attributer/dsl/method_injector.rb +16 -14
  18. data/lib/domainic/attributer/errors/aggregate_error.rb +36 -0
  19. data/lib/domainic/attributer/errors/callback_execution_error.rb +30 -0
  20. data/lib/domainic/attributer/errors/coercion_execution_error.rb +37 -0
  21. data/lib/domainic/attributer/errors/error.rb +19 -0
  22. data/lib/domainic/attributer/errors/validation_execution_error.rb +30 -0
  23. data/lib/domainic/attributer/instance_methods.rb +11 -8
  24. data/lib/domainic/attributer/undefined.rb +9 -7
  25. data/lib/domainic/attributer.rb +88 -27
  26. data/sig/domainic/attributer/attribute/callback.rbs +10 -7
  27. data/sig/domainic/attributer/attribute/coercer.rbs +14 -11
  28. data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +14 -12
  29. data/sig/domainic/attributer/attribute/signature.rbs +43 -32
  30. data/sig/domainic/attributer/attribute/validator.rbs +28 -13
  31. data/sig/domainic/attributer/attribute.rbs +27 -17
  32. data/sig/domainic/attributer/attribute_set.rbs +21 -19
  33. data/sig/domainic/attributer/class_methods.rbs +133 -80
  34. data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +62 -22
  35. data/sig/domainic/attributer/dsl/attribute_builder.rbs +515 -26
  36. data/sig/domainic/attributer/dsl/initializer.rbs +21 -19
  37. data/sig/domainic/attributer/dsl/method_injector.rbs +16 -14
  38. data/sig/domainic/attributer/errors/aggregate_error.rbs +28 -0
  39. data/sig/domainic/attributer/errors/callback_execution_error.rbs +23 -0
  40. data/sig/domainic/attributer/errors/coercion_execution_error.rbs +29 -0
  41. data/sig/domainic/attributer/errors/error.rbs +17 -0
  42. data/sig/domainic/attributer/errors/validation_execution_error.rbs +23 -0
  43. data/sig/domainic/attributer/instance_methods.rbs +11 -8
  44. data/sig/domainic/attributer/undefined.rbs +5 -3
  45. data/sig/domainic/attributer.rbs +88 -27
  46. metadata +19 -6
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'domainic/attributer/attribute/mixin/belongs_to_attribute'
4
+ require 'domainic/attributer/errors/callback_execution_error'
4
5
 
5
6
  module Domainic
6
7
  module Attributer
7
8
  class Attribute
8
- # A class responsible for managing change callbacks for an attribute.
9
+ # A class responsible for managing change callbacks for an attribute
9
10
  #
10
11
  # This class handles the execution of callbacks that are triggered when an
11
12
  # attribute's value changes. Each callback must be a Proc that accepts two
12
- # arguments: the old value and the new value.
13
+ # arguments: the old value, and the new value
13
14
  #
15
+ # @api private
16
+ # @!visibility private
14
17
  # @author {https://aaronmallen.me Aaron Allen}
15
18
  # @since 0.1.0
16
19
  class Callback
@@ -21,12 +24,12 @@ module Domainic
21
24
 
22
25
  include BelongsToAttribute
23
26
 
24
- # Initialize a new Callback instance.
27
+ # Initialize a new Callback instance
25
28
  #
26
- # @param attribute [Attribute] the attribute this Callback belongs to
29
+ # @param attribute [Attribute] the {Attribute} this instance belongs to
27
30
  # @param handlers [Array<Proc>] the handlers to use for processing
28
31
  #
29
- # @return [Callback] the new instance of Callback
32
+ # @return [Callback] the new Callback instance
30
33
  # @rbs (Attribute attribute, Array[handler] | handler handlers) -> void
31
34
  def initialize(attribute, handlers = [])
32
35
  super
@@ -36,21 +39,30 @@ module Domainic
36
39
  end.uniq
37
40
  end
38
41
 
39
- # Execute all callbacks for a value change.
42
+ # Execute all callbacks for a value change
40
43
  #
41
44
  # @param instance [Object] the instance on which to execute callbacks
42
45
  # @param old_value [Object] the previous value
43
46
  # @param new_value [Object] the new value
44
47
  #
48
+ # @raise [CallbackExecutionError] if any callback handlers raises an error
45
49
  # @return [void]
46
50
  # @rbs (untyped instance, untyped old_value, untyped new_value) -> void
47
51
  def call(instance, old_value, new_value)
48
- @handlers.each { |handler| instance.instance_exec(old_value, new_value, &handler) }
52
+ errors = []
53
+
54
+ @handlers.each do |handler|
55
+ instance.instance_exec(old_value, new_value, &handler)
56
+ rescue StandardError => e
57
+ errors << e
58
+ end
59
+
60
+ raise CallbackExecutionError, errors unless errors.empty?
49
61
  end
50
62
 
51
63
  private
52
64
 
53
- # Validate that a callback handler is a valid Proc.
65
+ # Validate that a callback handler is a valid Proc
54
66
  #
55
67
  # @param handler [Object] the handler to validate
56
68
  #
@@ -60,7 +72,7 @@ module Domainic
60
72
  def validate_handler!(handler)
61
73
  return if handler.is_a?(Proc)
62
74
 
63
- raise TypeError, "`#{attribute_method_name}`: invalid handler: #{handler.inspect}. Must be a Proc."
75
+ raise TypeError, "`#{attribute_method_name}`: invalid handler: #{handler.inspect}. Must be a Proc"
64
76
  end
65
77
  end
66
78
  end
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'domainic/attributer/attribute/mixin/belongs_to_attribute'
4
+ require 'domainic/attributer/errors/coercion_execution_error'
5
+ require 'domainic/attributer/undefined'
4
6
 
5
7
  module Domainic
6
8
  module Attributer
7
9
  class Attribute
8
- # A class responsible for coercing attribute values.
10
+ # A class responsible for coercing attribute values
9
11
  #
10
12
  # This class manages the coercion of values assigned to an attribute. Coercion can be
11
- # handled by either a Proc that accepts a single value argument, or by referencing an
12
- # instance method via Symbol.
13
+ # handled by either a {Proc} that accepts a single value argument, or by referencing an
14
+ # instance method via {Symbol}
13
15
  #
16
+ # @api private
17
+ # @!visibility private
14
18
  # @author {https://aaronmallen.me Aaron Allen}
15
19
  # @since 0.1.0
16
20
  class Coercer
@@ -23,12 +27,12 @@ module Domainic
23
27
 
24
28
  # @rbs @handlers: Array[handler]
25
29
 
26
- # Initialize a new Coercer instance.
30
+ # Initialize a new {Coercer} instance
27
31
  #
28
- # @param attribute [Attribute] the attribute this Coercer belongs to
32
+ # @param attribute [Attribute] the {Attribute} this instance belongs to
29
33
  # @param handlers [Array<Proc, Symbol>] the handlers to use for processing
30
34
  #
31
- # @return [Coercer] the new instance of Coercer
35
+ # @return [Coercer] the new Coercer instance
32
36
  # @rbs (Attribute attribute, Array[handler] | handler handlers) -> void
33
37
  def initialize(attribute, handlers = [])
34
38
  super
@@ -38,20 +42,31 @@ module Domainic
38
42
  end.uniq
39
43
  end
40
44
 
41
- # Process a value through all coercion handlers.
45
+ # Process a value through all coercion handlers
42
46
  #
43
47
  # @param instance [Object] the instance on which to perform coercion
44
48
  # @param value [Object] the value to coerce
45
49
  #
46
- # @return [Object] the coerced value
47
- # @rbs (untyped instance, untyped value) -> untyped
50
+ # @raise [CoercionExecutionError] if a coercion handler raises an error
51
+ # @return [Object, nil] the coerced value
52
+ # @rbs (untyped instance, untyped? value) -> untyped?
48
53
  def call(instance, value)
49
- @handlers.reduce(value) { |accumulator, handler| coerce_value(instance, handler, accumulator) }
54
+ return value if value == Undefined
55
+ return value if value.nil? && !@attribute.signature.nilable?
56
+
57
+ @handlers.reduce(value) do |accumulator, handler|
58
+ coerce_value(instance, handler, accumulator)
59
+ rescue StandardError => e
60
+ raise CoercionExecutionError.new(
61
+ "Failed to coerce #{accumulator.inspect} with #{handler.inspect}: #{e.message}",
62
+ handler
63
+ )
64
+ end
50
65
  end
51
66
 
52
67
  private
53
68
 
54
- # Process a value through a single coercion handler.
69
+ # Process a value through a single coercion handler
55
70
  #
56
71
  # @param instance [Object] the instance on which to perform coercion
57
72
  # @param handler [Proc, Symbol] the coercion handler
@@ -67,12 +82,12 @@ module Domainic
67
82
  when Symbol
68
83
  instance.send(handler, value)
69
84
  else
70
- # We should never get here because we validate the handlers in the initializer.
85
+ # We should never get here because we validate the handlers in the initializer
71
86
  raise TypeError, "`#{attribute_method_name}`: invalid coercer: #{handler}. "
72
87
  end
73
88
  end
74
89
 
75
- # Validate that a coercion handler is valid.
90
+ # Validate that a coercion handler is valid
76
91
  #
77
92
  # @param handler [Object] the handler to validate
78
93
  #
@@ -1,35 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'domainic/attributer/attribute'
4
+
3
5
  module Domainic
4
6
  module Attributer
5
7
  class Attribute
6
- # A mixin providing common functionality for classes that belong to an Attribute.
8
+ # A mixin providing common functionality for classes that belong to an Attribute
7
9
  #
8
10
  # This module provides initialization and duplication behavior for classes that are owned
9
11
  # by and work in conjunction with an Attribute instance. These classes typically handle
10
- # specific aspects of attribute processing such as coercion, validation, or callbacks.
12
+ # specific aspects of attribute processing such as coercion, validation, or callbacks
11
13
  #
14
+ # @api private
15
+ # @!visibility private
12
16
  # @author {https://aaronmallen.me Aaron Allen}
13
17
  # @since 0.1.0
14
18
  module BelongsToAttribute
15
19
  # @rbs @attribute: Attribute
16
20
 
17
- # Initialize a new instance that belongs to an attribute.
21
+ # Initialize a new instance that belongs to an {Attribute}
18
22
  #
19
- # @param attribute [Attribute] the attribute this instance belongs to
23
+ # @param attribute [Attribute] the {Attribute} this instance belongs to
20
24
  #
21
- # @return [void]
25
+ # @return [BelongsToAttribute] the new BelongsToAttribute instance
22
26
  # @rbs (Attribute attribute, *untyped, **untyped) -> void
23
27
  def initialize(attribute, ...)
24
28
  validate_attribute!(attribute)
25
29
  @attribute = attribute
26
30
  end
27
31
 
28
- # Create a duplicate instance associated with a new attribute.
32
+ # Create a duplicate instance associated with a new {Attribute}
29
33
  #
30
34
  # @param new_attribute [Attribute] the new attribute to associate with
31
35
  #
32
- # @return [BelongsToAttribute] a duplicate instance
36
+ # @return [BelongsToAttribute] duplicate instance with new {Attribute}
33
37
  # @rbs (Attribute attribute) -> BelongsToAttribute
34
38
  def dup_with_attribute(new_attribute)
35
39
  validate_attribute!(new_attribute)
@@ -39,24 +43,23 @@ module Domainic
39
43
 
40
44
  private
41
45
 
42
- # Generate a method name for error messages.
46
+ # Generate a method name for error messages
43
47
  #
44
- # @return [String] the formatted method name
48
+ # @return [String] formatted method name
45
49
  # @rbs () -> String
46
50
  def attribute_method_name
47
51
  "#{@attribute.base}##{@attribute.name}"
48
52
  end
49
53
 
50
- # Ensure that an attribute is a valid {Attribute} instance.
54
+ # Ensure that an {Attribute} is a valid {Attribute} instance
51
55
  #
52
- # @param attribute [Attribute] the attribute to validate
56
+ # @param attribute [Attribute] the {Attribute} to validate
53
57
  #
54
- # @raise [TypeError] if the attribute is not a valid {Attribute} instance
58
+ # @raise [TypeError] if the attribute is not a valid Attribute instance
55
59
  # @return [void]
56
60
  # @rbs (Attribute attribute) -> void
57
61
  def validate_attribute!(attribute)
58
62
  return if attribute.is_a?(Attribute)
59
- return if defined?(RSpec::Mocks::TestDouble) && attribute.is_a?(RSpec::Mocks::TestDouble)
60
63
 
61
64
  raise TypeError,
62
65
  "invalid attribute: #{attribute.inspect}. Must be an Domainic::Attributer::Attribute instance"
@@ -5,12 +5,14 @@ require 'domainic/attributer/attribute/mixin/belongs_to_attribute'
5
5
  module Domainic
6
6
  module Attributer
7
7
  class Attribute
8
- # A class responsible for managing attribute signature information.
8
+ # A class responsible for managing attribute signature information
9
9
  #
10
- # This class encapsulates the type and visibility configuration for an attribute.
10
+ # This class encapsulates the type and visibility configuration for an {Attribute}
11
11
  # It validates and manages whether an attribute is an argument or option, as well
12
- # as controlling read and write visibility (public, protected, or private).
12
+ # as controlling read and write visibility (public, protected, or private)
13
13
  #
14
+ # @api private
15
+ # @!visibility private
14
16
  # @author {https://aaronmallen.me Aaron Allen}
15
17
  # @since 0.1.0
16
18
  class Signature
@@ -37,10 +39,10 @@ module Domainic
37
39
 
38
40
  include BelongsToAttribute
39
41
 
40
- # @return [Hash{Symbol => Object}] Default options for a new Signature instance.
42
+ # @return [Hash{Symbol => Object}] Default options for a new Signature instance
41
43
  DEFAULT_OPTIONS = { nilable: true, read: :public, required: false, write: :public }.freeze #: default_options
42
44
 
43
- # Constants defining valid attribute types.
45
+ # Constants defining valid attribute types
44
46
  #
45
47
  # @author {https://aaronmallen.me Aaron Allen}
46
48
  # @since 0.1.0
@@ -55,7 +57,7 @@ module Domainic
55
57
  ALL = [ARGUMENT, OPTION].freeze #: Array[type_symbol]
56
58
  end
57
59
 
58
- # Constants defining valid visibility levels.
60
+ # Constants defining valid visibility levels
59
61
  #
60
62
  # @author {https://aaronmallen.me Aaron Allen}
61
63
  # @since 0.1.0
@@ -80,30 +82,39 @@ module Domainic
80
82
  # @rbs @type: type_symbol
81
83
  # @rbs @write_visibility: visibility_symbol
82
84
 
85
+ # Get the position of the attribute
86
+ #
83
87
  # @return [Integer, nil] the position of the attribute
84
88
  attr_reader :position #: Integer?
85
89
 
90
+ # Get the visibility level for reading the attribute
91
+ #
86
92
  # @return [Symbol] the visibility level for reading the attribute
87
93
  attr_reader :read_visibility #: visibility_symbol
88
94
 
95
+ # Get the attribute type
96
+ #
89
97
  # @return [Symbol] the type of the attribute
90
98
  attr_reader :type #: type_symbol
91
99
 
100
+ # Get the visibility level for writing the attribute
101
+ #
92
102
  # @return [Symbol] the visibility level for writing the attribute
93
103
  attr_reader :write_visibility #: visibility_symbol
94
104
 
95
- # Initialize a new Signature instance.
105
+ # Initialize a new {Signature} instance
96
106
  #
97
- # @param attribute [Attribute] the attribute this signature belongs to
107
+ # @param attribute [Attribute] the {Attribute} this instance belongs to
98
108
  # @param options [Hash{Symbol => Object}] the signature options
99
- # @option options [Boolean] nilable (true) whether the attribute is allowed to be nil.
109
+ # @option options [Boolean] nilable (true) whether the attribute is allowed to be nil
100
110
  # @option options [Integer, nil] position (nil) optional position for ordered attributes
101
111
  # @option options [Symbol] read (:public) the read visibility
102
112
  # @option options [Boolean] required (false) whether the attribute is required
103
113
  # @option options [Symbol] type the type of attribute
104
114
  # @option options [Symbol] write (:public) the write visibility
105
115
  #
106
- # @return [void]
116
+ # @raise [ArgumentError] if the configuration is invalid
117
+ # @return [Signature] the new Signature instance
107
118
  # @rbs (
108
119
  # Attribute attribute,
109
120
  # ?nilable: bool,
@@ -127,7 +138,7 @@ module Domainic
127
138
  @write_visibility = options.fetch(:write).to_sym
128
139
  end
129
140
 
130
- # Check if this signature is for an argument attribute.
141
+ # Check if this signature is for an argument attribute
131
142
  #
132
143
  # @return [Boolean] true if this is an argument attribute
133
144
  # @rbs () -> bool
@@ -135,15 +146,15 @@ module Domainic
135
146
  @type == TYPE::ARGUMENT
136
147
  end
137
148
 
138
- # Check if the attribute is allowed to be nil.
149
+ # Check if the attribute allows nil values
139
150
  #
140
- # @return [Boolean] true if the attribute is allowed to be nil
151
+ # @return [Boolean] true if the attribute allows nil values
141
152
  # @rbs () -> bool
142
153
  def nilable?
143
154
  @nilable
144
155
  end
145
156
 
146
- # Check if this signature is for an option attribute.
157
+ # Check if this signature is for an option attribute
147
158
  #
148
159
  # @return [Boolean] true if this is an option attribute
149
160
  # @rbs () -> bool
@@ -151,7 +162,7 @@ module Domainic
151
162
  @type == TYPE::OPTION
152
163
  end
153
164
 
154
- # Check if this signature is for an optional attribute.
165
+ # Check if this signature is for an optional attribute
155
166
  #
156
167
  # @return [Boolean] true if this is an optional attribute
157
168
  # @rbs () -> bool
@@ -159,7 +170,7 @@ module Domainic
159
170
  !required?
160
171
  end
161
172
 
162
- # Check if both read and write operations are private.
173
+ # Check if both read and write operations are private
163
174
  #
164
175
  # @return [Boolean] true if both read and write are private
165
176
  # @rbs () -> bool
@@ -167,7 +178,7 @@ module Domainic
167
178
  private_read? && private_write?
168
179
  end
169
180
 
170
- # Check if read operations are private.
181
+ # Check if read operations are private
171
182
  #
172
183
  # @return [Boolean] true if read operations are private
173
184
  # @rbs () -> bool
@@ -175,7 +186,7 @@ module Domainic
175
186
  [VISIBILITY::PRIVATE, VISIBILITY::PROTECTED].include?(@read_visibility)
176
187
  end
177
188
 
178
- # Check if write operations are private.
189
+ # Check if write operations are private
179
190
  #
180
191
  # @return [Boolean] true if write operations are private
181
192
  # @rbs () -> bool
@@ -183,7 +194,7 @@ module Domainic
183
194
  [VISIBILITY::PRIVATE, VISIBILITY::PROTECTED].include?(@write_visibility)
184
195
  end
185
196
 
186
- # Check if both read and write operations are protected.
197
+ # Check if both read and write operations are protected
187
198
  #
188
199
  # @return [Boolean] true if both read and write are protected
189
200
  # @rbs () -> bool
@@ -191,7 +202,7 @@ module Domainic
191
202
  protected_read? && protected_write?
192
203
  end
193
204
 
194
- # Check if read operations are protected.
205
+ # Check if read operations are protected
195
206
  #
196
207
  # @return [Boolean] true if read operations are protected
197
208
  # @rbs () -> bool
@@ -199,7 +210,7 @@ module Domainic
199
210
  @read_visibility == VISIBILITY::PROTECTED
200
211
  end
201
212
 
202
- # Check if write operations are protected.
213
+ # Check if write operations are protected
203
214
  #
204
215
  # @return [Boolean] true if write operations are protected
205
216
  # @rbs () -> bool
@@ -207,7 +218,7 @@ module Domainic
207
218
  @write_visibility == VISIBILITY::PROTECTED
208
219
  end
209
220
 
210
- # Check if both read and write operations are public.
221
+ # Check if both read and write operations are public
211
222
  #
212
223
  # @return [Boolean] true if both read and write are public
213
224
  # @rbs () -> bool
@@ -215,7 +226,7 @@ module Domainic
215
226
  public_read? && public_write?
216
227
  end
217
228
 
218
- # Check if read operations are public.
229
+ # Check if read operations are public
219
230
  #
220
231
  # @return [Boolean] true if read operations are public
221
232
  # @rbs () -> bool
@@ -223,7 +234,7 @@ module Domainic
223
234
  @read_visibility == VISIBILITY::PUBLIC
224
235
  end
225
236
 
226
- # Check if write operations are public.
237
+ # Check if write operations are public
227
238
  #
228
239
  # @return [Boolean] true if write operations are public
229
240
  # @rbs () -> bool
@@ -231,9 +242,9 @@ module Domainic
231
242
  @write_visibility == VISIBILITY::PUBLIC
232
243
  end
233
244
 
234
- # Check if the attribute is required.
245
+ # Check if this signature requires an attribute value
235
246
  #
236
- # @return [Boolean] true if the attribute is required
247
+ # @return [Boolean] true if this signature requires an attribute value
237
248
  # @rbs () -> bool
238
249
  def required?
239
250
  @required
@@ -241,7 +252,7 @@ module Domainic
241
252
 
242
253
  private
243
254
 
244
- # Get signature options as a hash.
255
+ # Get signature options as a hash
245
256
  #
246
257
  # @return [Hash] the signature options
247
258
  # @rbs () -> initialize_options
@@ -256,7 +267,7 @@ module Domainic
256
267
  }
257
268
  end
258
269
 
259
- # Validate that a value is a Boolean.
270
+ # Validate that a value is a boolean
260
271
  #
261
272
  # @param name [String, Symbol] the name of the attribute being validated
262
273
  # @param value [Boolean] the value to validate
@@ -270,7 +281,7 @@ module Domainic
270
281
  raise ArgumentError, "`#{attribute_method_name}`: invalid #{name}: #{value}. Must be `true` or `false`."
271
282
  end
272
283
 
273
- # Validate all initialization options.
284
+ # Validate all initialization options
274
285
  #
275
286
  # @param options [Hash{Symbol => Object}] the options to validate
276
287
  # @option options [Boolean] nilable the nilable flag to validate
@@ -291,7 +302,7 @@ module Domainic
291
302
  validate_type!(options[:type])
292
303
  end
293
304
 
294
- # Validate that a position value is valid.
305
+ # Validate that a position value is valid
295
306
  #
296
307
  # @param position [Integer, nil] the position to validate
297
308
  #
@@ -304,7 +315,7 @@ module Domainic
304
315
  raise ArgumentError, "`#{attribute_method_name}`: invalid position: #{position}. Must be Integer or nil."
305
316
  end
306
317
 
307
- # Validate that a type value is valid.
318
+ # Validate that a type value is valid
308
319
  #
309
320
  # @param type [Symbol] the type to validate
310
321
  #
@@ -318,7 +329,7 @@ module Domainic
318
329
  "`#{attribute_method_name}`: invalid type: #{type}. Must be one of #{TYPE::ALL.join(', ')}"
319
330
  end
320
331
 
321
- # Validate that visibility values are valid.
332
+ # Validate that visibility values are valid
322
333
  #
323
334
  # @param type [Symbol] which visibility setting to validate
324
335
  # @param value [Symbol] the visibility value to validate
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'domainic/attributer/attribute/mixin/belongs_to_attribute'
4
+ require 'domainic/attributer/errors/error'
5
+ require 'domainic/attributer/errors/validation_execution_error'
6
+ require 'domainic/attributer/undefined'
4
7
 
5
8
  module Domainic
6
9
  module Attributer
7
10
  class Attribute
8
- # A class responsible for validating attribute values.
11
+ # A class responsible for validating attribute values
9
12
  #
10
13
  # This class manages the validation of values assigned to an attribute. Validation
11
- # can be performed either by a Proc that accepts a single value argument and returns
12
- # a boolean, or by any object that responds to the `===` operator.
14
+ # can be performed either by a {Proc} that accepts a single value argument and returns
15
+ # a boolean, or by any object that responds to the `===` operator
13
16
  #
17
+ # @api private
18
+ # @!visibility private
14
19
  # @author {https://aaronmallen.me Aaron Allen}
15
20
  # @since 0.1.0
16
21
  class Validator
@@ -37,12 +42,12 @@ module Domainic
37
42
 
38
43
  # @rbs @handlers: Array[handler]
39
44
 
40
- # Initialize a new Validator instance.
45
+ # Initialize a new Validator instance
41
46
  #
42
- # @param attribute [Attribute] the attribute this Validator belongs to
47
+ # @param attribute [Attribute] the {Attribute} this instance belongs to
43
48
  # @param handlers [Array<Class, Module, Object, Proc>] the handlers to use for processing
44
49
  #
45
- # @return [Validator] the new instance of Validator
50
+ # @return [Validator] the new Validator instance
46
51
  # @rbs (Attribute attribute, Array[handler] | handler handlers) -> void
47
52
  def initialize(attribute, handlers = [])
48
53
  super
@@ -52,28 +57,28 @@ module Domainic
52
57
  end.uniq
53
58
  end
54
59
 
55
- # Validate a value using all configured validators.
60
+ # Validate a value using all configured validators
56
61
  #
57
62
  # @param instance [Object] the instance on which to perform validation
58
63
  # @param value [Object] the value to validate
59
64
  #
60
65
  # @raise [ArgumentError] if the value fails validation
66
+ # @raise [ValidationExecutionError] if errors occur during validation execution
61
67
  # @return [void]
62
68
  # @rbs (untyped instance, untyped value) -> void
63
69
  def call(instance, value)
64
70
  return if value == Undefined && handle_undefined!
65
71
  return if value.nil? && handle_nil!
66
- return if @handlers.all? { |handler| validate_value!(handler, instance, value) }
67
72
 
68
- raise ArgumentError, "`#{attribute_method_name}`: has invalid value: #{value.inspect}"
73
+ run_validations!(instance, value)
69
74
  end
70
75
 
71
76
  private
72
77
 
73
- # Handle a `nil` value.
78
+ # Handle a nil value
74
79
  #
75
80
  # @raise [ArgumentError] if the attribute is not nilable
76
- # @return [true] if the attribute is nilable
81
+ # @return [Boolean] true if the attribute is nilable
77
82
  # @rbs () -> bool
78
83
  def handle_nil!
79
84
  return true if @attribute.signature.nilable?
@@ -81,10 +86,10 @@ module Domainic
81
86
  raise ArgumentError, "`#{attribute_method_name}`: cannot be nil"
82
87
  end
83
88
 
84
- # Handle an {Undefined} value.
89
+ # Handle an {Undefined} value
85
90
  #
86
91
  # @raise [ArgumentError] if the attribute is required
87
- # @return [true] if the attribute is optional
92
+ # @return [Boolean] true if the attribute is optional
88
93
  # @rbs () -> bool
89
94
  def handle_undefined!
90
95
  return true if @attribute.signature.optional?
@@ -92,7 +97,30 @@ module Domainic
92
97
  raise ArgumentError, "`#{attribute_method_name}`: is required"
93
98
  end
94
99
 
95
- # Validate that a validation handler is valid.
100
+ # Run all configured validations
101
+ #
102
+ # @param instance [Object] the instance on which to perform validation
103
+ # @param value [Object] the value to validate
104
+ #
105
+ # @raise [ArgumentError] if the value fails validation
106
+ # @raise [ValidationExecutionError] if errors occur during validation execution
107
+ # @return [void]
108
+ # @rbs (untyped instance, untyped value) -> void
109
+ def run_validations!(instance, value)
110
+ errors = []
111
+ is_valid = true
112
+
113
+ @handlers.each do |handler|
114
+ is_valid = validate_value!(handler, instance, value)
115
+ rescue StandardError => e
116
+ errors << e
117
+ end
118
+
119
+ raise ValidationExecutionError, errors unless errors.empty?
120
+ raise ArgumentError, "`#{attribute_method_name}`: has invalid value: #{value.inspect}" unless is_valid
121
+ end
122
+
123
+ # Validate that a validation handler is valid
96
124
  #
97
125
  # @param handler [Object] the handler to validate
98
126
  #
@@ -106,11 +134,13 @@ module Domainic
106
134
  'or an object responding to `#===`.'
107
135
  end
108
136
 
109
- # Validate a value using a single handler.
137
+ # Validate a value using a single handler
110
138
  #
111
139
  # @param handler [Object] the handler to use for validation
112
140
  # @param instance [Object] the instance on which to perform validation
113
141
  # @param value [Object] the value to validate
142
+ #
143
+ # @return [Boolean] true if validation succeeds
114
144
  # @rbs (handler handler, untyped instance, untyped value) -> bool
115
145
  def validate_value!(handler, instance, value)
116
146
  if handler.is_a?(Proc)
@@ -118,7 +148,7 @@ module Domainic
118
148
  elsif handler.respond_to?(:===)
119
149
  handler === value # rubocop:disable Style/CaseEquality
120
150
  else
121
- # We should never get here because we validate the handlers in the initializer.
151
+ # We should never get here because we validate the handlers in the initializer
122
152
  raise TypeError, "`#{attribute_method_name}`: invalid validator: #{handler.inspect}"
123
153
  end
124
154
  end