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.
- checksums.yaml +4 -4
- data/.yardopts +11 -0
- data/CHANGELOG.md +32 -1
- data/README.md +42 -355
- data/docs/USAGE.md +723 -0
- data/lib/domainic/attributer/attribute/callback.rb +21 -9
- data/lib/domainic/attributer/attribute/coercer.rb +28 -13
- data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +16 -13
- data/lib/domainic/attributer/attribute/signature.rb +43 -32
- data/lib/domainic/attributer/attribute/validator.rb +46 -16
- data/lib/domainic/attributer/attribute.rb +28 -18
- data/lib/domainic/attributer/attribute_set.rb +21 -19
- data/lib/domainic/attributer/class_methods.rb +136 -83
- data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +64 -22
- data/lib/domainic/attributer/dsl/attribute_builder.rb +515 -26
- data/lib/domainic/attributer/dsl/initializer.rb +23 -18
- data/lib/domainic/attributer/dsl/method_injector.rb +16 -14
- data/lib/domainic/attributer/errors/aggregate_error.rb +36 -0
- data/lib/domainic/attributer/errors/callback_execution_error.rb +30 -0
- data/lib/domainic/attributer/errors/coercion_execution_error.rb +37 -0
- data/lib/domainic/attributer/errors/error.rb +19 -0
- data/lib/domainic/attributer/errors/validation_execution_error.rb +30 -0
- data/lib/domainic/attributer/instance_methods.rb +11 -8
- data/lib/domainic/attributer/undefined.rb +9 -7
- data/lib/domainic/attributer.rb +88 -27
- data/sig/domainic/attributer/attribute/callback.rbs +10 -7
- data/sig/domainic/attributer/attribute/coercer.rbs +14 -11
- data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +14 -12
- data/sig/domainic/attributer/attribute/signature.rbs +43 -32
- data/sig/domainic/attributer/attribute/validator.rbs +28 -13
- data/sig/domainic/attributer/attribute.rbs +27 -17
- data/sig/domainic/attributer/attribute_set.rbs +21 -19
- data/sig/domainic/attributer/class_methods.rbs +133 -80
- data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +62 -22
- data/sig/domainic/attributer/dsl/attribute_builder.rbs +515 -26
- data/sig/domainic/attributer/dsl/initializer.rbs +21 -19
- data/sig/domainic/attributer/dsl/method_injector.rbs +16 -14
- data/sig/domainic/attributer/errors/aggregate_error.rbs +28 -0
- data/sig/domainic/attributer/errors/callback_execution_error.rbs +23 -0
- data/sig/domainic/attributer/errors/coercion_execution_error.rbs +29 -0
- data/sig/domainic/attributer/errors/error.rbs +17 -0
- data/sig/domainic/attributer/errors/validation_execution_error.rbs +23 -0
- data/sig/domainic/attributer/instance_methods.rbs +11 -8
- data/sig/domainic/attributer/undefined.rbs +5 -3
- data/sig/domainic/attributer.rbs +88 -27
- 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
|
+
[](#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
|
+
[](#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
|
+
[](#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
|
+
[](#table-of-contents)
|