domainic-attributer 0.1.0 → 0.2.2
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.
- checksums.yaml +4 -4
- data/.yardopts +11 -0
- data/CHANGELOG.md +39 -1
- data/README.md +41 -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 +44 -33
- data/lib/domainic/attributer/attribute/validator.rb +47 -17
- data/lib/domainic/attributer/attribute.rb +29 -19
- data/lib/domainic/attributer/attribute_set.rb +23 -21
- 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 +51 -32
- data/sig/domainic/attributer/attribute/validator.rbs +28 -13
- data/sig/domainic/attributer/attribute.rbs +41 -17
- data/sig/domainic/attributer/attribute_set.rbs +21 -19
- data/sig/domainic/attributer/class_methods.rbs +190 -83
- data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +56 -22
- data/sig/domainic/attributer/dsl/attribute_builder.rbs +521 -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 +19 -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 +21 -13
- data/sig/domainic/attributer/dsl.rbs +0 -1
- data/sig/domainic-attributer.rbs +0 -1
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)
|