domainic-attributer 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.md +32 -1
  4. data/README.md +42 -355
  5. data/docs/USAGE.md +723 -0
  6. data/lib/domainic/attributer/attribute/callback.rb +21 -9
  7. data/lib/domainic/attributer/attribute/coercer.rb +28 -13
  8. data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +16 -13
  9. data/lib/domainic/attributer/attribute/signature.rb +43 -32
  10. data/lib/domainic/attributer/attribute/validator.rb +46 -16
  11. data/lib/domainic/attributer/attribute.rb +28 -18
  12. data/lib/domainic/attributer/attribute_set.rb +21 -19
  13. data/lib/domainic/attributer/class_methods.rb +136 -83
  14. data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +64 -22
  15. data/lib/domainic/attributer/dsl/attribute_builder.rb +515 -26
  16. data/lib/domainic/attributer/dsl/initializer.rb +23 -18
  17. data/lib/domainic/attributer/dsl/method_injector.rb +16 -14
  18. data/lib/domainic/attributer/errors/aggregate_error.rb +36 -0
  19. data/lib/domainic/attributer/errors/callback_execution_error.rb +30 -0
  20. data/lib/domainic/attributer/errors/coercion_execution_error.rb +37 -0
  21. data/lib/domainic/attributer/errors/error.rb +19 -0
  22. data/lib/domainic/attributer/errors/validation_execution_error.rb +30 -0
  23. data/lib/domainic/attributer/instance_methods.rb +11 -8
  24. data/lib/domainic/attributer/undefined.rb +9 -7
  25. data/lib/domainic/attributer.rb +88 -27
  26. data/sig/domainic/attributer/attribute/callback.rbs +10 -7
  27. data/sig/domainic/attributer/attribute/coercer.rbs +14 -11
  28. data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +14 -12
  29. data/sig/domainic/attributer/attribute/signature.rbs +43 -32
  30. data/sig/domainic/attributer/attribute/validator.rbs +28 -13
  31. data/sig/domainic/attributer/attribute.rbs +27 -17
  32. data/sig/domainic/attributer/attribute_set.rbs +21 -19
  33. data/sig/domainic/attributer/class_methods.rbs +133 -80
  34. data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +62 -22
  35. data/sig/domainic/attributer/dsl/attribute_builder.rbs +515 -26
  36. data/sig/domainic/attributer/dsl/initializer.rbs +21 -19
  37. data/sig/domainic/attributer/dsl/method_injector.rbs +16 -14
  38. data/sig/domainic/attributer/errors/aggregate_error.rbs +28 -0
  39. data/sig/domainic/attributer/errors/callback_execution_error.rbs +23 -0
  40. data/sig/domainic/attributer/errors/coercion_execution_error.rbs +29 -0
  41. data/sig/domainic/attributer/errors/error.rbs +17 -0
  42. data/sig/domainic/attributer/errors/validation_execution_error.rbs +23 -0
  43. data/sig/domainic/attributer/instance_methods.rbs +11 -8
  44. data/sig/domainic/attributer/undefined.rbs +5 -3
  45. data/sig/domainic/attributer.rbs +88 -27
  46. metadata +19 -6
@@ -1,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