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