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
@@ -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