domainic-attributer 0.1.0 → 0.2.2

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.md +39 -1
  4. data/README.md +41 -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 +44 -33
  10. data/lib/domainic/attributer/attribute/validator.rb +47 -17
  11. data/lib/domainic/attributer/attribute.rb +29 -19
  12. data/lib/domainic/attributer/attribute_set.rb +23 -21
  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 +51 -32
  30. data/sig/domainic/attributer/attribute/validator.rbs +28 -13
  31. data/sig/domainic/attributer/attribute.rbs +41 -17
  32. data/sig/domainic/attributer/attribute_set.rbs +21 -19
  33. data/sig/domainic/attributer/class_methods.rbs +190 -83
  34. data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +56 -22
  35. data/sig/domainic/attributer/dsl/attribute_builder.rbs +521 -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 +19 -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 +21 -13
  47. data/sig/domainic/attributer/dsl.rbs +0 -1
  48. data/sig/domainic-attributer.rbs +0 -1
@@ -1,11 +1,9 @@
1
1
  module Domainic
2
2
  module Attributer
3
3
  module DSL
4
- # A class responsible for configuring attributes through a fluent interface.
5
- #
6
- # This class provides a rich DSL for configuring attributes with support for
7
- # default values, coercion, validation, visibility controls, and change tracking.
8
- # It uses method chaining to allow natural, declarative attribute definitions.
4
+ # This class provides a rich DSL for configuring attributes with support for default values, coercion, validation,
5
+ # visibility controls, and change tracking. It uses method chaining to allow natural, declarative attribute
6
+ # definitions
9
7
  #
10
8
  # @author {https://aaronmallen.me Aaron Allen}
11
9
  # @since 0.1.0
@@ -14,37 +12,152 @@ module Domainic
14
12
 
15
13
  @options: OptionParser::result
16
14
 
17
- # Initialize a new AttributeBuilder.
15
+ # Initialize a new AttributeBuilder
18
16
  #
19
17
  # @param base [Class, Module] the class or module to build the attribute in
20
18
  # @param attribute_name [String, Symbol] the name of the attribute
21
19
  # @param attribute_type [String, Symbol] the type of attribute
22
20
  # @param type_validator [Proc, Object, nil] optional type validator
23
- # @param options [Hash] additional options for attribute configuration
24
- # @yield configuration block for additional attribute settings
21
+ # @param options [Hash{Symbol => Object}] additional options for attribute configuration. See
22
+ # {OptionParser#initialize} for details
25
23
  #
26
- # @return [void]
24
+ # @return [AttributeBuilder] the new AttributeBuilder instance
25
+ # __todo__ base,
26
+ # String | Symbol attribute_name,
27
+ # String | Symbol attribute_type,
28
+ # ?Attribute::Validator::handler? type_validator,
29
+ # OptionParser::options options,
30
+ # ) ? { (?) [self: AttributeBuilder] -> void } -> void
27
31
  def initialize: (__todo__ base, String | Symbol attribute_name, String | Symbol attribute_type, ?Attribute::Validator::handler? type_validator, OptionParser::options options) ?{ (?) [self: AttributeBuilder] -> void } -> void
28
32
 
29
- # Builds and finalizes the attribute.
33
+ # Build and finalize the {Attribute}
34
+ #
35
+ # @!visibility private
36
+ # @api private
30
37
  #
31
38
  # @return [Attribute] the configured attribute
32
39
  def build!: () -> Attribute
33
40
 
34
- # Configure value coercion.
41
+ # Provides a way to automatically transform attribute values into the desired format or type. Coercion ensures
42
+ # input values conform to the expected structure by applying one or more handlers. Handlers can be Procs,
43
+ # lambdas, or method symbols.
44
+ #
45
+ # Coercions are applied during initialization or whenever the attribute value is updated.
46
+ #
47
+ # @note When coercion is used with nilable attributes, handlers should account for `nil` values appropriately.
48
+ #
49
+ # @example Simple coercion
50
+ # class Superhero
51
+ # include Domainic::Attributer
52
+ #
53
+ # argument :code_name do
54
+ # coerce_with ->(val) { val.to_s.upcase }
55
+ # end
56
+ # end
57
+ #
58
+ # hero = Superhero.new("spiderman")
59
+ # hero.code_name # => "SPIDERMAN"
60
+ #
61
+ # @example Multiple coercions
62
+ # class Superhero
63
+ # include Domainic::Attributer
64
+ #
65
+ # option :power_level do
66
+ # coerce_with ->(val) { val.to_s } # Convert to string
67
+ # coerce_with do |val| # Remove non-digits
68
+ # val.gsub(/\D/, '')
69
+ # end
70
+ # coerce_with ->(val) { val.to_i } # Convert to integer
71
+ # end
72
+ # end
73
+ #
74
+ # hero = Superhero.new(power_level: "over 9000!")
75
+ # hero.power_level # => 9000
76
+ #
77
+ # @example Coercion with an instance method
78
+ # class Superhero
79
+ # include Domainic::Attributer
80
+ #
81
+ # option :alias_name do
82
+ # coerce_with :format_alias
83
+ # end
84
+ #
85
+ # private
86
+ #
87
+ # def format_alias(value)
88
+ # value.to_s.downcase.split.map(&:capitalize).join(' ')
89
+ # end
90
+ # end
91
+ #
92
+ # hero = Superhero.new(alias_name: "ironMAN")
93
+ # hero.alias_name # => "Ironman"
35
94
  #
36
95
  # @param proc_symbol [Proc, Symbol, nil] optional coercion handler
37
96
  # @yield optional coercion block
97
+ # @yieldparam value [Object] the value to coerce
98
+ # @yieldreturn [Object] the coerced value
38
99
  #
39
100
  # @return [self] the builder for method chaining
40
101
  def coerce_with: (?(Attribute::Coercer::proc | Object)? proc_symbol) ?{ (untyped value) -> untyped } -> self
41
102
 
42
103
  alias coerce coerce_with
43
104
 
44
- # Configure default value.
105
+ # Provides a way to assign default values to attributes. These values can be static or dynamically generated
106
+ # using a block. The default value is only applied when no explicit value is provided for the attribute
107
+ #
108
+ # @example Static default values
109
+ # class RPGCharacter
110
+ # include Domainic::Attributer
111
+ #
112
+ # option :level, Integer do
113
+ # default 1
114
+ # end
115
+ #
116
+ # option :health_max do
117
+ # default 100
118
+ # end
119
+ # end
120
+ #
121
+ # hero = RPGCharacter.new
122
+ # hero.level # => 1
123
+ # hero.health_max # => 100
124
+ #
125
+ # @example Dynamic default values
126
+ # class RPGCharacter
127
+ # include Domainic::Attributer
128
+ #
129
+ # option :created_at do
130
+ # default { Time.now }
131
+ # end
132
+ #
133
+ # option :health_current do
134
+ # default { health_max }
135
+ # end
136
+ # end
137
+ #
138
+ # hero = RPGCharacter.new
139
+ # hero.created_at # => Current timestamp
140
+ # hero.health_current # => Defaults to the value of `health_max`
141
+ #
142
+ # @example Complex dynamic default values
143
+ # class RPGCharacter
144
+ # include Domainic::Attributer
145
+ #
146
+ # option :inventory do
147
+ # default do
148
+ # base_items = ['Health Potion', 'Map']
149
+ # base_items << 'Lucky Coin' if Random.rand < 0.1
150
+ # base_items
151
+ # end
152
+ # end
153
+ # end
154
+ #
155
+ # hero = RPGCharacter.new
156
+ # hero.inventory # => ["Health Potion", "Map"] or ["Health Potion", "Map", "Lucky Coin"]
45
157
  #
46
158
  # @param value_or_proc [Object, Proc, nil] optional default value or generator
47
159
  # @yield optional default value generator block
160
+ # @yieldreturn [Object] the default value
48
161
  #
49
162
  # @return [self] the builder for method chaining
50
163
  def default: (?untyped? value_or_proc) ?{ (?) -> untyped } -> self
@@ -53,7 +166,33 @@ module Domainic
53
166
 
54
167
  alias default_value default
55
168
 
56
- # Set attribute description.
169
+ # Provides a way to add descriptive metadata to attributes. Descriptions improve code clarity by documenting
170
+ # the purpose or behavior of an attribute. These descriptions can be short or detailed, depending on the
171
+ # context.
172
+ #
173
+ # @note Descriptions are optional but highly recommended for improving readability and maintainability of the
174
+ # code.
175
+ #
176
+ # @example Adding a short description
177
+ # class MagicItem
178
+ # include Domainic::Attributer
179
+ #
180
+ # argument :name do
181
+ # desc 'The name of the magic item, must be unique'
182
+ # end
183
+ # end
184
+ #
185
+ # @example Adding a detailed description
186
+ # class MagicItem
187
+ # include Domainic::Attributer
188
+ #
189
+ # option :power_level do
190
+ # description 'The magical power level of the item, ranging from 0 to 100.
191
+ # Higher power levels increase effectiveness but may come with
192
+ # increased risks during use.'
193
+ # validate_with ->(val) { val.between?(0, 100) }
194
+ # end
195
+ # end
57
196
  #
58
197
  # @param text [String] the description text
59
198
  #
@@ -62,7 +201,33 @@ module Domainic
62
201
 
63
202
  alias desc description
64
203
 
65
- # Mark attribute as non-nilable.
204
+ # Ensures that an attribute defined in the block DSL cannot have a `nil` value. This validation is enforced
205
+ # during initialization and when modifying the attribute value at runtime. Use `non_nilable` for attributes that
206
+ # must always have a value.
207
+ #
208
+ # @example Preventing `nil` values for an attribute
209
+ # class Ninja
210
+ # include Domainic::Attributer
211
+ #
212
+ # argument :code_name do
213
+ # non_nilable
214
+ # end
215
+ # end
216
+ #
217
+ # Ninja.new(nil) # Raises ArgumentError: nil value is not allowed
218
+ #
219
+ # @example Combining `non_nilable` with other features
220
+ # class Ninja
221
+ # include Domainic::Attributer
222
+ #
223
+ # argument :rank do
224
+ # desc 'The rank of the ninja, such as Genin or Chunin'
225
+ # non_nilable
226
+ # end
227
+ # end
228
+ #
229
+ # ninja = Ninja.new('Genin') # => Works
230
+ # ninja.rank = nil # Raises ArgumentError: nil value is not allowed
66
231
  #
67
232
  # @return [self] the builder for method chaining
68
233
  def non_nilable: () -> self
@@ -81,68 +246,398 @@ module Domainic
81
246
 
82
247
  alias not_nullable non_nilable
83
248
 
84
- # Configure change callback.
249
+ # Allows defining a callback to be triggered whenever the attribute's value changes. The callback receives the
250
+ # old value and the new value as arguments, enabling custom logic to be executed on changes. Use `on_change` to
251
+ # react to changes in attribute values, such as updating dependent attributes or triggering side effects.
252
+ #
253
+ # @example Reacting to changes in an attribute
254
+ # class VideoGame
255
+ # include Domainic::Attributer
256
+ #
257
+ # option :health do
258
+ # default 100
259
+ # on_change ->(old_value, new_value) {
260
+ # puts "Health changed from #{old_value} to #{new_value}"
261
+ # }
262
+ # end
263
+ # end
264
+ #
265
+ # game = VideoGame.new
266
+ # game.health = 50 # Outputs: Health changed from 100 to 50
267
+ #
268
+ # @example Performing complex logic on change
269
+ # class VideoGame
270
+ # include Domainic::Attributer
271
+ #
272
+ # option :power_ups do
273
+ # default []
274
+ # on_change do |old_value, new_value|
275
+ # new_items = new_value - old_value
276
+ # lost_items = old_value - new_value
277
+ #
278
+ # new_items.each { |item| activate_power_up(item) }
279
+ # lost_items.each { |item| deactivate_power_up(item) }
280
+ # end
281
+ # end
282
+ #
283
+ # private
284
+ #
285
+ # def activate_power_up(item)
286
+ # puts "Activated power-up: #{item}"
287
+ # end
288
+ #
289
+ # def deactivate_power_up(item)
290
+ # puts "Deactivated power-up: #{item}"
291
+ # end
292
+ # end
293
+ #
294
+ # game = VideoGame.new
295
+ # game.power_ups = ['Shield', 'Speed Boost']
296
+ # # Outputs: Activated power-up: Shield
297
+ # # Activated power-up: Speed Boost
298
+ # game.power_ups = ['Shield']
299
+ # # Outputs: Deactivated power-up: Speed Boost
85
300
  #
86
301
  # @param proc [Proc, nil] optional callback handler
87
302
  # @yield optional callback block
303
+ # @yieldparam old_value [Object] the previous value of the attribute
304
+ # @yieldparam new_value [Object] the new value of the attribute
305
+ # @yieldreturn [void]
88
306
  #
89
307
  # @return [self] the builder for method chaining
90
308
  def on_change: (?Attribute::Callback::handler? proc) ?{ (untyped old_value, untyped new_value) -> void } -> self
91
309
 
92
- # Set private visibility for both read and write.
310
+ # Sets both the read and write visibility of an attribute to private. This ensures the attribute can only be
311
+ # accessed or modified within the class itself.
312
+ #
313
+ # @example Making an attribute private
314
+ # class SecretAgent
315
+ # include Domainic::Attributer
316
+ #
317
+ # option :real_name do
318
+ # desc 'The real name of the agent, hidden from external access.'
319
+ # private
320
+ # end
321
+ # end
322
+ #
323
+ # agent = SecretAgent.new(real_name: 'James Bond')
324
+ # agent.real_name # Raises NoMethodError: private method `real_name' called for #<SecretAgent>
325
+ # agent.real_name = 'John Doe' # Raises NoMethodError: private method `real_name=' called for #<SecretAgent>
93
326
  #
94
327
  # @return [self] the builder for method chaining
95
328
  def private: () -> self
96
329
 
97
- # Set private visibility for read.
330
+ # Sets the read visibility of an attribute to private, allowing the attribute to be read only within the class
331
+ # itself. The write visibility remains unchanged unless explicitly modified. Use `private_read` when the value
332
+ # of an attribute should be hidden from external consumers but writable by external code if needed.
333
+ #
334
+ # @example Making the reader private
335
+ # class SecretAgent
336
+ # include Domainic::Attributer
337
+ #
338
+ # option :mission_code do
339
+ # desc 'The secret mission code, readable only within the class.'
340
+ # private_read
341
+ # default { generate_code }
342
+ # end
343
+ #
344
+ # private
345
+ #
346
+ # def generate_code
347
+ # "M-#{rand(1000..9999)}"
348
+ # end
349
+ # end
350
+ #
351
+ # agent = SecretAgent.new
352
+ # agent.mission_code # Raises NoMethodError: private method `mission_code' called for #<SecretAgent>
353
+ # agent.mission_code = 'Override Code' # Works, as write visibility is still public
98
354
  #
99
355
  # @return [self] the builder for method chaining
100
356
  def private_read: () -> self
101
357
 
102
- # Set private visibility for write.
358
+ # Sets the write visibility of an attribute to private, allowing the attribute to be modified only within the
359
+ # class. The read visibility remains unchanged unless explicitly modified. Use `private_write` to ensure that an
360
+ # attribute's value can only be updated internally, while still allowing external code to read its value if
361
+ # needed.
362
+ #
363
+ # @example Making the writer private
364
+ # class SecretAgent
365
+ # include Domainic::Attributer
366
+ #
367
+ # option :mission_code do
368
+ # desc 'The secret mission code, writable only within the class.'
369
+ # private_write
370
+ # default { generate_code }
371
+ # end
372
+ #
373
+ # private
374
+ #
375
+ # def generate_code
376
+ # "M-#{rand(1000..9999)}"
377
+ # end
378
+ # end
379
+ #
380
+ # agent = SecretAgent.new
381
+ # agent.mission_code # => "M-1234"
382
+ # agent.mission_code = '007' # Raises NoMethodError: private method `mission_code=' called for #<SecretAgent>
103
383
  #
104
384
  # @return [self] the builder for method chaining
105
385
  def private_write: () -> self
106
386
 
107
- # Set protected visibility for both read and write.
387
+ # Sets both the read and write visibility of an attribute to protected, allowing access only within the class
388
+ # and its subclasses. This visibility restricts external access entirely. Use `protected` to share attributes
389
+ # within a class hierarchy while keeping them hidden from external consumers.
390
+ #
391
+ # @example Defining a protected attribute
392
+ # class SecretAgent
393
+ # include Domainic::Attributer
394
+ #
395
+ # option :mission_code do
396
+ # protected
397
+ # description 'The mission code, accessible only within the class and its subclasses.'
398
+ # end
399
+ # end
400
+ #
401
+ # class DoubleAgent < SecretAgent
402
+ # def reveal_code
403
+ # self.mission_code
404
+ # end
405
+ # end
406
+ #
407
+ # agent = SecretAgent.new(mission_code: '007')
408
+ # agent.mission_code # Raises NoMethodError
409
+ # DoubleAgent.new.reveal_code # => '007'
108
410
  #
109
411
  # @return [self] the builder for method chaining
110
412
  def protected: () -> self
111
413
 
112
- # Set protected visibility for read.
414
+ # Sets both the read and write visibility of an attribute to protected. This allows the attribute to be accessed
415
+ # or modified only within the class and its subclasses. Use `protected` for attributes that should be accessible
416
+ # to the class and its subclasses but hidden from external consumers.
417
+ #
418
+ # @example Making an attribute protected
419
+ # class SecretAgent
420
+ # include Domainic::Attributer
421
+ #
422
+ # option :mission_code do
423
+ # desc 'The mission code, accessible only within the class or subclasses.'
424
+ # protected
425
+ # end
426
+ # end
427
+ #
428
+ # class DoubleAgent < SecretAgent
429
+ # def reveal_code
430
+ # self.mission_code
431
+ # end
432
+ # end
433
+ #
434
+ # agent = SecretAgent.new(mission_code: '007')
435
+ # agent.mission_code # Raises NoMethodError
436
+ # DoubleAgent.new.reveal_code # => '007'
113
437
  #
114
438
  # @return [self] the builder for method chaining
115
439
  def protected_read: () -> self
116
440
 
117
- # Set protected visibility for write.
441
+ # Sets both the read and write visibility of an attribute to protected. This allows the attribute to be accessed
442
+ # or modified only within the class and its subclasses. Use `protected` for attributes that should be accessible
443
+ # to the class and its subclasses but hidden from external consumers.
444
+ #
445
+ # @example Making an attribute protected
446
+ # class SecretAgent
447
+ # include Domainic::Attributer
448
+ #
449
+ # option :mission_code do
450
+ # desc 'The mission code, accessible only within the class or subclasses.'
451
+ # protected
452
+ # end
453
+ # end
454
+ #
455
+ # class DoubleAgent < SecretAgent
456
+ # def reveal_code
457
+ # self.mission_code
458
+ # end
459
+ # end
460
+ #
461
+ # agent = SecretAgent.new(mission_code: '007')
462
+ # agent.mission_code # Raises NoMethodError
463
+ # DoubleAgent.new.reveal_code # => '007'
118
464
  #
119
465
  # @return [self] the builder for method chaining
120
466
  def protected_write: () -> self
121
467
 
122
- # Set public visibility for both read and write.
468
+ # Explicitly sets both the read and write visibility of an attribute to public, overriding any inherited or
469
+ # previously set visibility. By default, attributes are public, so this is typically used to revert an
470
+ # attribute's visibility if it was changed in a parent class or module.
471
+ #
472
+ # @note Attributes are public by default. Use `public` explicitly to override inherited or modified visibility.
473
+ #
474
+ # @example Reverting visibility to public in a subclass
475
+ # class SecretAgent
476
+ # include Domainic::Attributer
477
+ #
478
+ # option :mission_code do
479
+ # desc 'The mission code, protected in the base class.'
480
+ # private
481
+ # end
482
+ # end
483
+ #
484
+ # class FieldAgent < SecretAgent
485
+ # option :mission_code do
486
+ # desc 'The mission code, made public in the subclass.'
487
+ # public
488
+ # end
489
+ # end
490
+ #
491
+ # agent = FieldAgent.new(mission_code: '007')
492
+ # agent.mission_code # => '007' (now accessible)
493
+ # agent.mission_code = '008' # Works, as visibility is public in the subclass
123
494
  #
124
495
  # @return [self] the builder for method chaining
125
496
  def public: () -> self
126
497
 
127
- # Set public visibility for read.
498
+ # Explicitly sets the read visibility of an attribute to public, overriding any inherited or previously set
499
+ # visibility. By default, attributes are readable publicly, so this is typically used to revert the read
500
+ # visibility of an attribute if it was modified in a parent class or module.
501
+ #
502
+ # @note Attributes are publicly readable by default. Use `public_read` explicitly to override inherited or
503
+ # modified visibility.
504
+ #
505
+ # @example Reverting read visibility to public in a subclass
506
+ # class SecretAgent
507
+ # include Domainic::Attributer
508
+ #
509
+ # option :mission_code do
510
+ # desc 'The mission code, privately readable in the base class.'
511
+ # private_read
512
+ # end
513
+ # end
514
+ #
515
+ # class FieldAgent < SecretAgent
516
+ # option :mission_code do
517
+ # desc 'The mission code, made publicly readable in the subclass.'
518
+ # public_read
519
+ # end
520
+ # end
521
+ #
522
+ # agent = FieldAgent.new(mission_code: '007')
523
+ # agent.mission_code # => '007' (now publicly readable)
524
+ # agent.mission_code = '008' # Raises NoMethodError, as write visibility is still private
128
525
  #
129
526
  # @return [self] the builder for method chaining
130
527
  def public_read: () -> self
131
528
 
132
- # Set public visibility for write.
529
+ # Explicitly sets the write visibility of an attribute to public, overriding any inherited or previously set
530
+ # visibility. By default, attributes are writable publicly, so this is typically used to revert the write
531
+ # visibility of an attribute if it was modified in a parent class or module.
532
+ #
533
+ # @note Attributes are publicly writable by default. Use `public_write` explicitly to override inherited or
534
+ # modified visibility.
535
+ #
536
+ # @example Reverting write visibility to public in a subclass
537
+ # class SecretAgent
538
+ # include Domainic::Attributer
539
+ #
540
+ # option :mission_code do
541
+ # desc 'The mission code, writable only within the class or subclasses in the base class.'
542
+ # private_write
543
+ # end
544
+ # end
545
+ #
546
+ # class FieldAgent < SecretAgent
547
+ # option :mission_code do
548
+ # desc 'The mission code, now writable publicly in the subclass.'
549
+ # public_write
550
+ # end
551
+ # end
552
+ #
553
+ # agent = FieldAgent.new(mission_code: '007')
554
+ # agent.mission_code # Raises NoMethodError, as read visibility remains restricted
555
+ # agent.mission_code = '008' # Works, as write visibility is now public
133
556
  #
134
557
  # @return [self] the builder for method chaining
135
558
  def public_write: () -> self
136
559
 
137
- # Mark attribute as required.
560
+ # Marks an {ClassMethods#option option} attribute as required, ensuring that a value must be provided during
561
+ # initialization. If a required attribute is not supplied, an error is raised. Use `required` to enforce
562
+ # mandatory attributes.
563
+ #
564
+ # @note required options are enforced during initialization; as long as the option is provided
565
+ # (even if it is `nil`) no error will be raised.
566
+ #
567
+ # @example Defining a required attribute
568
+ # class Superhero
569
+ # include Domainic::Attributer
570
+ #
571
+ # option :name do
572
+ # desc 'The name of the superhero, which must be provided.'
573
+ # required
574
+ # end
575
+ # end
576
+ #
577
+ # Superhero.new # Raises ArgumentError: missing required attribute: name
578
+ # Superhero.new(name: 'Spiderman') # Works, as the required attribute is supplied
579
+ # Superhero.new(name: nil) # Works, as the required attribute is supplied (even if it is nil)
138
580
  #
139
581
  # @return [self] the builder for method chaining
140
582
  def required: () -> self
141
583
 
142
- # Configure value validation.
584
+ # Adds a custom validation to an attribute, allowing you to define specific criteria that the attribute's value
585
+ # must meet. Validators can be Procs, lambdas, or symbols referencing instance methods. Validation occurs during
586
+ # initialization and whenever the attribute value is updated. Use `validate_with` to enforce rules beyond type
587
+ # or presence, such as ranges, formats, or custom logic.
588
+ #
589
+ # @example Adding a simple validation
590
+ # class Superhero
591
+ # include Domainic::Attributer
592
+ #
593
+ # argument :power_level do
594
+ # desc 'The power level of the superhero, which must be an integer between 0 and 100.'
595
+ # validate_with ->(val) { val.is_a?(Integer) && val.between?(0, 100) }
596
+ # end
597
+ # end
598
+ #
599
+ # Superhero.new(150) # Raises ArgumentError: invalid value for power_level
600
+ # Superhero.new(85) # Works, as 85 is within the valid range
601
+ #
602
+ # @example Using an instance method as a validator
603
+ # class Superhero
604
+ # include Domainic::Attributer
605
+ #
606
+ # argument :alias_name do
607
+ # desc 'The alias name of the superhero, validated using an instance method.'
608
+ # validate_with :validate_alias_name
609
+ # end
610
+ #
611
+ # private
612
+ #
613
+ # def validate_alias_name(value)
614
+ # value.is_a?(String) && value.match?(/\A[A-Z][a-z]+\z/)
615
+ # end
616
+ # end
617
+ #
618
+ # Superhero.new('Spiderman') # Works, as the alias name matches the validation criteria
619
+ # Superhero.new('spiderman') # Raises ArgumentError: invalid value for alias_name
620
+ #
621
+ # @example Combining multiple validators
622
+ # class Vehicle
623
+ # include Domainic::Attributer
624
+ #
625
+ # option :speed do
626
+ # desc 'The speed of the vehicle, which must be a non-negative number.'
627
+ # validate_with ->(val) { val.is_a?(Numeric) }
628
+ # validate_with do |val|
629
+ # val.zero? || val.positive?
630
+ # end
631
+ # end
632
+ # end
633
+ #
634
+ # Vehicle.new(speed: -10) # Raises ArgumentError: invalid value for speed
635
+ # Vehicle.new(speed: 50) # Works, as 50 meets all validation criteria
143
636
  #
144
637
  # @param object_or_proc [Object, Proc, nil] optional validation handler
145
638
  # @yield optional validation block
639
+ # @yieldparam value [Object] the value to validate
640
+ # @yieldreturn [Boolean] `true` if the value is valid, `false` otherwise
146
641
  #
147
642
  # @return [self] the builder for method chaining
148
643
  def validate_with: (?Attribute::Validator::handler? object_or_proc) ?{ (untyped value) -> boolish } -> self