domainic-attributer 0.1.0 → 0.2.0

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