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
data/docs/USAGE.md ADDED
@@ -0,0 +1,723 @@
1
+ # Domainic::Attributer Usage Guide
2
+
3
+ A comprehensive guide to all features and capabilities of Domainic::Attributer.
4
+
5
+ ## Table of Contents
6
+
7
+ * [Core Concepts](#core-concepts)
8
+ * [Arguments vs Options](#arguments-vs-options)
9
+ * [Attribute Lifecycle](#attribute-lifecycle)
10
+ * [Error Types](#error-types)
11
+ * [Features](#features)
12
+ * [Type Validation](#type-validation)
13
+ * [Value Coercion](#value-coercion)
14
+ * [Nilability Control](#nilability-control)
15
+ * [Change Tracking](#change-tracking)
16
+ * [Visibility Control](#visibility-control)
17
+ * [Default Values](#default-values)
18
+ * [Documentation](#documentation)
19
+ * [Custom Method Names](#custom-method-names)
20
+ * [Best Practices](#best-practices)
21
+ * [Validation vs Coercion](#validation-vs-coercion)
22
+ * [Managing Complex Attributes](#managing-complex-attributes)
23
+ * [Advanced Topics](#advanced-topics)
24
+ * [Attribute Inheritance](#attribute-inheritance)
25
+ * [Custom Validators](#custom-validators)
26
+
27
+ ## Core Concepts
28
+
29
+ ### Arguments vs Options
30
+
31
+ Domainic::Attributer provides two ways to define attributes:
32
+
33
+ * `argument`: Required positional parameters that must be provided in order
34
+ * `option`: Named parameters that can be provided in any order (optional by default)
35
+
36
+ ```ruby
37
+ class Spaceship
38
+ include Domainic::Attributer
39
+
40
+ argument :captain # Required, must be first
41
+ argument :warp_core # Required, must be second
42
+ option :shields # Optional, provided by name
43
+ option :phasers # Optional, provided by name
44
+ end
45
+
46
+ # Valid ways to create a spaceship:
47
+ Spaceship.new('Kirk', 'Dilithium', shields: true)
48
+ Spaceship.new('Picard', 'Matter/Antimatter', phasers: 'Charged')
49
+ ```
50
+
51
+ ### Attribute Lifecycle
52
+
53
+ Domainic::Attributer manages attributes throughout their entire lifecycle. All constraints (type validation, coercion,
54
+ nullability checks, etc.) are enforced both during initialization and whenever attributes are modified.
55
+
56
+ #### Initialization Phase
57
+
58
+ When creating a new object, attributes are processed in this order:
59
+
60
+ 1. Arguments are processed in their defined order
61
+ 2. Options are processed in any order
62
+ 3. For each attribute:
63
+
64
+ * Default value is generated if no value provided
65
+ * Value is coerced to the correct format
66
+ * Value is validated
67
+ * Change callbacks are triggered
68
+
69
+ ```ruby
70
+ class Jedi
71
+ include Domainic::Attributer
72
+
73
+ argument :name, String
74
+ option :midi_chlorians, Integer do
75
+ default 3000
76
+ validate_with ->(val) { val.positive? }
77
+ end
78
+ end
79
+
80
+ # During initialization:
81
+ jedi = Jedi.new(123) # Raises ArgumentError (name must be String)
82
+ jedi = Jedi.new('Yoda', midi_chlorians: -1) # Raises ArgumentError (must be positive)
83
+ jedi = Jedi.new('Yoda') # Works! midi_chlorians defaults to 3000
84
+ ```
85
+
86
+ #### Runtime Changes
87
+
88
+ The same validations and coercions apply when modifying attributes after initialization:
89
+
90
+ ```ruby
91
+ jedi = Jedi.new('Yoda')
92
+ jedi.name = 456 # Raises ArgumentError (must be String)
93
+ jedi.midi_chlorians = -1 # Raises ArgumentError (must be positive)
94
+ jedi.midi_chlorians = '4000' # Coerced to Integer automatically
95
+ ```
96
+
97
+ ### Error Types
98
+
99
+ Domainic::Attributer uses specialized error classes to provide clear feedback when something goes wrong during attribute
100
+ processing.
101
+
102
+ #### Validation Failures
103
+
104
+ When a value fails validation (returns false or nil), an `ArgumentError` is raised:
105
+
106
+ ```ruby
107
+ class Spaceship
108
+ include Domainic::Attributer
109
+
110
+ argument :name, String
111
+ argument :crew_count, Integer do
112
+ validate_with ->(val) { val.positive? }
113
+ end
114
+ end
115
+
116
+ ship = Spaceship.new(123, 5) # Raises ArgumentError: invalid value for String
117
+ ship = Spaceship.new("Enterprise", -1) # Raises ArgumentError: has invalid value: -1
118
+ ```
119
+
120
+ #### Internal Error Handling
121
+
122
+ The following errors are raised by Domainic::Attributer when internal processing fails:
123
+
124
+ * `ValidationExecutionError` - Raised when a validation handler itself raises an error
125
+ * `CoercionExecutionError` - Raised when a coercion handler raises an error
126
+ * `CallbackExecutionError` - Raised when a change callback raises an error
127
+
128
+ These errors can be caught and handled for debugging or error recovery:
129
+
130
+ ```ruby
131
+ class TimeMachine
132
+ include Domainic::Attributer
133
+
134
+ option :year, Integer do
135
+ on_change ->(old_val, new_val) {
136
+ calculate_temporal_coordinates
137
+ }
138
+ end
139
+
140
+ private
141
+
142
+ def calculate_temporal_coordinates
143
+ # Complex calculation that might fail
144
+ raise "Flux capacitor malfunction!"
145
+ end
146
+ end
147
+
148
+ machine = TimeMachine.new
149
+ begin
150
+ machine.year = 1985
151
+ rescue Domainic::Attributer::CallbackExecutionError => e
152
+ puts "Time travel failed: #{e.message}"
153
+ end
154
+ ```
155
+
156
+ [![Return to Top](https://img.shields.io/badge/%E2%96%B2%20Return%20to%20Top-blue?style=for-the-badge)](#table-of-contents)
157
+
158
+ ## Features
159
+
160
+ ### Type Validation
161
+
162
+ Type validation ensures attributes contain the correct type of data.
163
+
164
+ <details>
165
+ <summary>Available Methods</summary>
166
+
167
+ #### Option Hash Style
168
+
169
+ * `validate: handler` - Add a validation handler
170
+ * `validate_with: handler` - Alias for validate
171
+ * `validators: [handler1, handler2]` - Add multiple handlers
172
+
173
+ #### Block Style
174
+
175
+ * `validate_with(handler)` - Add a validation handler
176
+ * `validate(handler)` - Alias for validate_with
177
+ * `validates(handler)` - Alias for validate_with
178
+
179
+ </details>
180
+
181
+ ```ruby
182
+ class Pokemon
183
+ include Domainic::Attributer
184
+
185
+ # Simple type validation
186
+ argument :name, String
187
+ argument :level, Integer
188
+
189
+ # Custom validation logic
190
+ option :moves do
191
+ validate_with ->(val) { val.is_a?(Array) && val.size <= 4 }
192
+ end
193
+
194
+ # Combining multiple validations
195
+ option :evolution do
196
+ validate_with PokemonSpecies # Custom type check
197
+ validate_with ->(val) {
198
+ return true if val.nil? # Allow nil values
199
+ val.level > level # Must be higher level
200
+ }
201
+ end
202
+ end
203
+
204
+ pikachu = Pokemon.new("Pikachu", 5)
205
+ pikachu.moves = ["Thunderbolt", "Quick Attack", "Tail Whip", "Thunder Wave"] # Works!
206
+ pikachu.moves = ["Thunderbolt", "Quick Attack", "Tail Whip", "Thunder Wave", "Tackle"] # Raises ArgumentError
207
+ ```
208
+
209
+ ### Value Coercion
210
+
211
+ Transform input values into the correct format automatically.
212
+
213
+ <details>
214
+ <summary>Available Methods</summary>
215
+
216
+ #### Option Hash Style
217
+
218
+ * `coerce: handler` - Add a coercion handler
219
+ * `coerce_with: handler` - Alias for coerce
220
+ * `coercers: [handler1, handler2]` - Add multiple handlers
221
+
222
+ #### Block Style
223
+
224
+ * `coerce_with(handler)` - Add a coercion handler
225
+ * `coerce(handler)` - Alias for coerce_with
226
+
227
+ Handlers can be:
228
+
229
+ * Procs/lambdas accepting one argument
230
+ * Symbols referencing instance methods
231
+
232
+ </details>
233
+
234
+ ```ruby
235
+ class Superhero
236
+ include Domainic::Attributer
237
+
238
+ # For non-nilable attributes, you don't need to handle nil
239
+ argument :code_name do
240
+ non_nilable
241
+ coerce_with ->(val) { val.to_s.upcase }
242
+ end
243
+
244
+ # For nilable attributes, your coercer must handle nil
245
+ option :secret_identity do
246
+ coerce_with ->(val) { val.nil? ? nil : val.to_s.capitalize }
247
+ end
248
+
249
+ # Multiple coercions are applied in order
250
+ option :power_level do
251
+ coerce_with ->(val) { val.to_s } # First convert to string
252
+ coerce_with ->(val) { val.gsub(/\D/, '') } # Remove non-digits
253
+ coerce_with ->(val) { val.to_i } # Convert to integer
254
+ end
255
+ end
256
+
257
+ hero = Superhero.new("spiderman")
258
+ hero.code_name # => "SPIDERMAN"
259
+ hero.secret_identity = :parker # => "Parker"
260
+ hero.secret_identity = nil # => nil
261
+ hero.power_level = "over 9000!" # => 9000
262
+ ```
263
+
264
+ ### Nilability Control
265
+
266
+ Manage how attributes handle nil values.
267
+
268
+ <details>
269
+ <summary>Available Methods</summary>
270
+
271
+ #### Option Hash Style
272
+
273
+ * `non_nilable: true` - Prevent nil values
274
+ * `non_nil: true` - Alias for non_nilable
275
+ * `non_null: true` - Alias for non_nilable
276
+ * `non_nullable: true` - Alias for non_nilable
277
+ * `not_nil: true` - Alias for non_nilable
278
+ * `not_nilable: true` - Alias for non_nilable
279
+ * `not_null: true` - Alias for non_nilable
280
+ * `not_nullable: true` - Alias for non_nilable
281
+ * `null: false` - Another way to prevent nil
282
+
283
+ #### Block Style
284
+
285
+ * `non_nilable` - Prevent nil values
286
+ * `non_nil` - Alias for non_nilable
287
+ * `non_null` - Alias for non_nilable
288
+ * `non_nullable` - Alias for non_nilable
289
+ * `not_nil` - Alias for non_nilable
290
+ * `not_nilable` - Alias for non_nilable
291
+ * `not_null` - Alias for non_nilable
292
+ * `not_nullable` - Alias for non_nilable
293
+
294
+ </details>
295
+
296
+ ```ruby
297
+ class Ninja
298
+ include Domainic::Attributer
299
+
300
+ # Using block style
301
+ argument :code_name do
302
+ non_nilable # Must always have a code name
303
+ end
304
+
305
+ # Using option hash style
306
+ argument :rank, non_null: true # Must always have a rank
307
+
308
+ # Optional but can't be nil if provided
309
+ option :special_technique, not_nilable: true
310
+
311
+ # Optional and allows nil
312
+ option :current_mission
313
+ end
314
+
315
+ ninja = Ninja.new(nil, 'Genin') # Raises ArgumentError
316
+ ninja = Ninja.new('Shadow', nil) # Raises ArgumentError
317
+ ninja = Ninja.new('Shadow', 'Genin', special_technique: nil) # Raises ArgumentError
318
+ ninja = Ninja.new('Shadow', 'Genin', current_mission: nil) # Works!
319
+ ```
320
+
321
+ ### Change Tracking
322
+
323
+ Monitor and react to attribute value changes.
324
+
325
+ <details>
326
+ <summary>Available Methods</summary>
327
+
328
+ #### Option Hash Style
329
+
330
+ * `on_change: handler` - Add a change handler
331
+ * `callback: handler` - Alias for on_change
332
+ * `callbacks: [handler1, handler2]` - Add multiple handlers
333
+
334
+ #### Block Style
335
+
336
+ * `on_change(handler)` - Add a change handler
337
+
338
+ Handlers must be Procs/lambdas accepting two arguments (old_value, new_value)
339
+ </details>
340
+
341
+ ```ruby
342
+ class VideoGame
343
+ include Domainic::Attributer
344
+
345
+ argument :title
346
+
347
+ option :health do
348
+ default 100
349
+ validate_with ->(val) { val.between?(0, 100) }
350
+
351
+ on_change ->(old_val, new_val) {
352
+ game_over! if new_val <= 0
353
+ heal_effect! if new_val > old_val
354
+ damage_effect! if new_val < old_val
355
+ }
356
+ end
357
+
358
+ option :power_ups, Array, default: [] do
359
+ on_change ->(old_val, new_val) {
360
+ new_items = new_val - old_val
361
+ lost_items = old_val - new_val
362
+
363
+ new_items.each { |item| activate_power_up(item) }
364
+ lost_items.each { |item| deactivate_power_up(item) }
365
+ }
366
+ end
367
+
368
+ private
369
+
370
+ def game_over!; end
371
+ def heal_effect!; end
372
+ def damage_effect!; end
373
+ def activate_power_up(item); end
374
+ def deactivate_power_up(item); end
375
+ end
376
+
377
+ game = VideoGame.new('Super Ruby World')
378
+ game.health = 0 # Triggers game_over!
379
+ game.health = 50 # Triggers damage_effect!
380
+ game.power_ups = ['Star'] # Activates the star power-up
381
+ ```
382
+
383
+ ### Visibility Control
384
+
385
+ Control attribute access levels.
386
+
387
+ <details>
388
+ <summary>Available Methods</summary>
389
+
390
+ #### Option Hash Style
391
+
392
+ * `read: :private/:protected/:public` - Set read visibility
393
+ * `read_access: :private/:protected/:public` - Alias for read
394
+ * `reader: :private/:protected/:public` - Alias for read
395
+ * `write_access: :private/:protected/:public` - Set write visibility
396
+ * `writer: :private/:protected/:public` - Alias for write_access
397
+
398
+ #### Block Style
399
+
400
+ * `private` - Make both read and write private
401
+ * `private_read` - Make only read private
402
+ * `private_write` - Make only write private
403
+ * `protected` - Make both read and write protected
404
+ * `protected_read` - Make only read protected
405
+ * `protected_write` - Make only write protected
406
+ * `public` - Make both read and write public
407
+ * `public_read` - Make only read public
408
+ * `public_write` - Make only write public
409
+
410
+ </details>
411
+
412
+ ```ruby
413
+ class SecretAgent
414
+ include Domainic::Attributer
415
+
416
+ # Public interface
417
+ argument :code_name
418
+
419
+ # Private data
420
+ option :real_name do
421
+ private # Both read and write are private
422
+ end
423
+
424
+ # Mixed visibility
425
+ option :current_mission do
426
+ protected_read # Other agents can read
427
+ private_write # Only self can update
428
+ end
429
+
430
+ # Hash style visibility
431
+ option :gadget_count,
432
+ read: :public, # Anyone can read
433
+ write: :protected # Only agents can update
434
+ end
435
+
436
+ agent = SecretAgent.new('007')
437
+ agent.code_name # => "007"
438
+ agent.real_name # NoMethodError
439
+ agent.gadget_count = 5 # NoMethodError (unless called from another agent)
440
+ ```
441
+
442
+ ### Default Values
443
+
444
+ Provide static defaults or generate them dynamically.
445
+
446
+ <details>
447
+ <summary>Available Methods</summary>
448
+
449
+ #### Option Hash Style
450
+
451
+ * `default: value` - Set a static default
452
+ * `default_generator: proc` - Set a dynamic default
453
+ * `default_value: value` - Alias for default
454
+
455
+ #### Block Style
456
+
457
+ * `default(value)` - Set static default
458
+ * `default { block }` - Set dynamic default
459
+ * `default_generator(value)` - Alias for default
460
+ * `default_value(value)` - Alias for default
461
+
462
+ </details>
463
+
464
+ ```ruby
465
+ class RPGCharacter
466
+ include Domainic::Attributer
467
+
468
+ argument :name
469
+
470
+ # Static defaults
471
+ option :level, Integer, default: 1
472
+ option :health_max, default: 100
473
+
474
+ # Dynamic defaults
475
+ option :created_at do
476
+ default { Time.now }
477
+ end
478
+
479
+ option :health_current do
480
+ default { health_max }
481
+ end
482
+
483
+ # Complex default generation
484
+ option :inventory do
485
+ default {
486
+ base_items = ['Health Potion', 'Map']
487
+ base_items << 'Lucky Coin' if Random.rand < 0.1
488
+ base_items
489
+ }
490
+ end
491
+ end
492
+
493
+ hero = RPGCharacter.new('Ruby Knight')
494
+ hero.level # => 1
495
+ hero.health_current # => 100
496
+ hero.inventory # => ["Health Potion", "Map"] or ["Health Potion", "Map", "Lucky Coin"]
497
+ ```
498
+
499
+ ### Documentation
500
+
501
+ Add descriptions to your attributes for better code clarity.
502
+
503
+ <details>
504
+ <summary>Available Methods</summary>
505
+
506
+ #### Option Hash Style
507
+
508
+ * `desc: text` - Short description
509
+ * `description: text` - Full description (overrides desc)
510
+
511
+ #### Block Style
512
+
513
+ * `desc(text)` - Short description
514
+ * `description(text)` - Full description
515
+
516
+ </details>
517
+
518
+ ```ruby
519
+ class MagicItem
520
+ include Domainic::Attributer
521
+
522
+ argument :name do
523
+ description 'The name of the magic item, must be unique'
524
+ end
525
+
526
+ option :power_level do
527
+ desc 'Magical energy from 0-100'
528
+ validate_with ->(val) { val.between?(0, 100) }
529
+ end
530
+
531
+ option :enchantments,
532
+ description: 'List of active enchantments on the item',
533
+ default: []
534
+ end
535
+ ```
536
+
537
+ ### Custom Method Names
538
+
539
+ Create your own DSL by customizing method names or disabling features you don't need.
540
+
541
+ ```ruby
542
+ # Custom method names
543
+ class GameConfig
544
+ include Domainic.Attributer(
545
+ argument: :required_setting,
546
+ option: :optional_setting
547
+ )
548
+
549
+ required_setting :difficulty
550
+ optional_setting :sound_enabled, default: true
551
+ end
552
+
553
+ # Disable features
554
+ class StrictConfig
555
+ include Domainic.Attributer(
556
+ option: nil # Only allows arguments
557
+ )
558
+
559
+ argument :api_key
560
+ argument :environment
561
+ # option method is not available
562
+ end
563
+ ```
564
+
565
+ [![Return to Top](https://img.shields.io/badge/%E2%96%B2%20Return%20to%20Top-blue?style=for-the-badge)](#table-of-contents)
566
+
567
+ ## Best Practices
568
+
569
+ ### Validation vs Coercion
570
+
571
+ Use validation when you want to ensure values meet specific criteria:
572
+
573
+ ```ruby
574
+ class SpellBook
575
+ include Domainic::Attributer
576
+
577
+ # Bad: Using coercion for validation
578
+ option :spell_count do
579
+ coerce_with ->(val) {
580
+ val = val.to_i
581
+ raise ArgumentError unless val.positive?
582
+ val
583
+ }
584
+ end
585
+
586
+ # Good: Separate concerns
587
+ option :spell_count do
588
+ coerce_with ->(val) { val.to_i }
589
+ validate_with ->(val) { val.positive? }
590
+ end
591
+ end
592
+ ```
593
+
594
+ ### Managing Complex Attributes
595
+
596
+ For attributes with multiple validations or transformations, use the block syntax for better readability:
597
+
598
+ ```ruby
599
+ class BattleMech
600
+ include Domainic::Attributer
601
+
602
+ # Hard to read
603
+ option :weapon_system,
604
+ description: 'Primary weapon configuration',
605
+ non_nilable: true,
606
+ validate_with: [
607
+ WeaponSystem,
608
+ ->(val) { val.power_draw <= max_power },
609
+ ->(val) { val.weight <= max_weight }
610
+ ],
611
+ on_change: ->(old_val, new_val) { recalculate_power_grid }
612
+
613
+ # Better organization
614
+ option :weapon_system do
615
+ description 'Primary weapon configuration'
616
+ non_nilable
617
+
618
+ validate_with WeaponSystem
619
+ validate_with ->(val) { val.power_draw <= max_power }
620
+ validate_with ->(val) { val.weight <= max_weight }
621
+
622
+ on_change ->(old_val, new_val) {
623
+ recalculate_power_grid
624
+ }
625
+ end
626
+ end
627
+ ```
628
+
629
+ [![Return to Top](https://img.shields.io/badge/%E2%96%B2%20Return%20to%20Top-blue?style=for-the-badge)](#table-of-contents)
630
+
631
+ ## Advanced Topics
632
+
633
+ ### Attribute Inheritance
634
+
635
+ Attributes are inherited from parent classes, and subsequent definitions in child classes add to rather than replace
636
+ the parent's configuration:
637
+
638
+ ```ruby
639
+ class Superhero
640
+ include Domainic::Attributer
641
+
642
+ argument :name, String
643
+ argument :powers do
644
+ validate_with Array
645
+ validate_with ->(val) { val.any? } # Must have at least one power
646
+ end
647
+ end
648
+
649
+ class XMen < Superhero
650
+ # Adds additional validation to the inherited :powers attribute
651
+ argument :powers do
652
+ validate_with ->(val) { val.all? { |p| p.is_a?(String) } } # Powers must be strings
653
+ end
654
+
655
+ # Adds a new attribute specific to X-Men
656
+ option :mutant_name
657
+ end
658
+
659
+ # Now :powers must be:
660
+ # 1. An Array (from parent)
661
+ # 2. Non-empty (from parent)
662
+ # 3. Contain only strings (from child)
663
+
664
+ wolverine = XMen.new(
665
+ "Logan",
666
+ ["Healing", "Adamantium Claws"], # Works - array of strings
667
+ mutant_name: "Wolverine"
668
+ )
669
+
670
+ # Fails - powers must be strings
671
+ cyclops = XMen.new("Scott", [:optic_blast])
672
+
673
+ # Fails - powers can't be empty
674
+ jubilee = XMen.new("Jubilation", [])
675
+ ```
676
+
677
+ ### Custom Validators
678
+
679
+ Create reusable validators for common patterns:
680
+
681
+ ```ruby
682
+ module GameValidators
683
+ HealthPoints = ->(val) { val.between?(0, 100) }
684
+
685
+ Username = ->(val) {
686
+ val.match?(/\A[a-z0-9_]{3,16}\z/i)
687
+ }
688
+
689
+ class DamageRange
690
+ def self.===(value)
691
+ value.is_a?(Range) &&
692
+ value.begin.is_a?(Integer) &&
693
+ value.end.is_a?(Integer) &&
694
+ value.begin.positive? &&
695
+ value.begin < value.end
696
+ end
697
+ end
698
+ end
699
+
700
+ class Player
701
+ include Domainic::Attributer
702
+
703
+ argument :username do
704
+ coerce_with ->(val) { val.to_s.downcase }
705
+ validate_with GameValidators::Username
706
+ end
707
+
708
+ option :hp do
709
+ default 100
710
+ validate_with GameValidators::HealthPoints
711
+ end
712
+
713
+ option :damage_range do
714
+ default 1..10
715
+ validate_with GameValidators::DamageRange
716
+ end
717
+ end
718
+ ```
719
+
720
+ This completes our comprehensive guide to Domainic::Attributer. Remember that the key to effective use is finding the
721
+ right balance of validation, coercion, and error handling for your specific needs.
722
+
723
+ [![Return to Top](https://img.shields.io/badge/%E2%96%B2%20Return%20to%20Top-blue?style=for-the-badge)](#table-of-contents)