domainic-attributer 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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