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