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.
- 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
@@ -1,11 +1,9 @@
|
|
1
1
|
module Domainic
|
2
2
|
module Attributer
|
3
3
|
module DSL
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# default values, coercion, validation, visibility controls, and change tracking.
|
8
|
-
# It uses method chaining to allow natural, declarative attribute definitions.
|
4
|
+
# This class provides a rich DSL for configuring attributes with support for default values, coercion, validation,
|
5
|
+
# visibility controls, and change tracking. It uses method chaining to allow natural, declarative attribute
|
6
|
+
# definitions
|
9
7
|
#
|
10
8
|
# @author {https://aaronmallen.me Aaron Allen}
|
11
9
|
# @since 0.1.0
|
@@ -14,37 +12,146 @@ module Domainic
|
|
14
12
|
|
15
13
|
@options: OptionParser::result
|
16
14
|
|
17
|
-
# Initialize a new AttributeBuilder
|
15
|
+
# Initialize a new AttributeBuilder
|
18
16
|
#
|
19
17
|
# @param base [Class, Module] the class or module to build the attribute in
|
20
18
|
# @param attribute_name [String, Symbol] the name of the attribute
|
21
19
|
# @param attribute_type [String, Symbol] the type of attribute
|
22
20
|
# @param type_validator [Proc, Object, nil] optional type validator
|
23
|
-
# @param options [Hash] additional options for attribute configuration
|
24
|
-
#
|
21
|
+
# @param options [Hash{Symbol => Object}] additional options for attribute configuration. See
|
22
|
+
# {OptionParser#initialize} for details
|
25
23
|
#
|
26
|
-
# @return [
|
24
|
+
# @return [AttributeBuilder] the new AttributeBuilder instance
|
27
25
|
def initialize: (__todo__ base, String | Symbol attribute_name, String | Symbol attribute_type, ?Attribute::Validator::handler? type_validator, OptionParser::options options) ?{ (?) [self: AttributeBuilder] -> void } -> void
|
28
26
|
|
29
|
-
#
|
27
|
+
# Build and finalize the {Attribute}
|
28
|
+
#
|
29
|
+
# @!visibility private
|
30
|
+
# @api private
|
30
31
|
#
|
31
32
|
# @return [Attribute] the configured attribute
|
32
33
|
def build!: () -> Attribute
|
33
34
|
|
34
|
-
#
|
35
|
+
# Provides a way to automatically transform attribute values into the desired format or type. Coercion ensures
|
36
|
+
# input values conform to the expected structure by applying one or more handlers. Handlers can be Procs,
|
37
|
+
# lambdas, or method symbols.
|
38
|
+
#
|
39
|
+
# Coercions are applied during initialization or whenever the attribute value is updated.
|
40
|
+
#
|
41
|
+
# @note When coercion is used with nilable attributes, handlers should account for `nil` values appropriately.
|
42
|
+
#
|
43
|
+
# @example Simple coercion
|
44
|
+
# class Superhero
|
45
|
+
# include Domainic::Attributer
|
46
|
+
#
|
47
|
+
# argument :code_name do
|
48
|
+
# coerce_with ->(val) { val.to_s.upcase }
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# hero = Superhero.new("spiderman")
|
53
|
+
# hero.code_name # => "SPIDERMAN"
|
54
|
+
#
|
55
|
+
# @example Multiple coercions
|
56
|
+
# class Superhero
|
57
|
+
# include Domainic::Attributer
|
58
|
+
#
|
59
|
+
# option :power_level do
|
60
|
+
# coerce_with ->(val) { val.to_s } # Convert to string
|
61
|
+
# coerce_with do |val| # Remove non-digits
|
62
|
+
# val.gsub(/\D/, '')
|
63
|
+
# end
|
64
|
+
# coerce_with ->(val) { val.to_i } # Convert to integer
|
65
|
+
# end
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# hero = Superhero.new(power_level: "over 9000!")
|
69
|
+
# hero.power_level # => 9000
|
70
|
+
#
|
71
|
+
# @example Coercion with an instance method
|
72
|
+
# class Superhero
|
73
|
+
# include Domainic::Attributer
|
74
|
+
#
|
75
|
+
# option :alias_name do
|
76
|
+
# coerce_with :format_alias
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# private
|
80
|
+
#
|
81
|
+
# def format_alias(value)
|
82
|
+
# value.to_s.downcase.split.map(&:capitalize).join(' ')
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# hero = Superhero.new(alias_name: "ironMAN")
|
87
|
+
# hero.alias_name # => "Ironman"
|
35
88
|
#
|
36
89
|
# @param proc_symbol [Proc, Symbol, nil] optional coercion handler
|
37
90
|
# @yield optional coercion block
|
91
|
+
# @yieldparam value [Object] the value to coerce
|
92
|
+
# @yieldreturn [Object] the coerced value
|
38
93
|
#
|
39
94
|
# @return [self] the builder for method chaining
|
40
95
|
def coerce_with: (?(Attribute::Coercer::proc | Object)? proc_symbol) ?{ (untyped value) -> untyped } -> self
|
41
96
|
|
42
97
|
alias coerce coerce_with
|
43
98
|
|
44
|
-
#
|
99
|
+
# Provides a way to assign default values to attributes. These values can be static or dynamically generated
|
100
|
+
# using a block. The default value is only applied when no explicit value is provided for the attribute
|
101
|
+
#
|
102
|
+
# @example Static default values
|
103
|
+
# class RPGCharacter
|
104
|
+
# include Domainic::Attributer
|
105
|
+
#
|
106
|
+
# option :level, Integer do
|
107
|
+
# default 1
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# option :health_max do
|
111
|
+
# default 100
|
112
|
+
# end
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# hero = RPGCharacter.new
|
116
|
+
# hero.level # => 1
|
117
|
+
# hero.health_max # => 100
|
118
|
+
#
|
119
|
+
# @example Dynamic default values
|
120
|
+
# class RPGCharacter
|
121
|
+
# include Domainic::Attributer
|
122
|
+
#
|
123
|
+
# option :created_at do
|
124
|
+
# default { Time.now }
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# option :health_current do
|
128
|
+
# default { health_max }
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# hero = RPGCharacter.new
|
133
|
+
# hero.created_at # => Current timestamp
|
134
|
+
# hero.health_current # => Defaults to the value of `health_max`
|
135
|
+
#
|
136
|
+
# @example Complex dynamic default values
|
137
|
+
# class RPGCharacter
|
138
|
+
# include Domainic::Attributer
|
139
|
+
#
|
140
|
+
# option :inventory do
|
141
|
+
# default do
|
142
|
+
# base_items = ['Health Potion', 'Map']
|
143
|
+
# base_items << 'Lucky Coin' if Random.rand < 0.1
|
144
|
+
# base_items
|
145
|
+
# end
|
146
|
+
# end
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# hero = RPGCharacter.new
|
150
|
+
# hero.inventory # => ["Health Potion", "Map"] or ["Health Potion", "Map", "Lucky Coin"]
|
45
151
|
#
|
46
152
|
# @param value_or_proc [Object, Proc, nil] optional default value or generator
|
47
153
|
# @yield optional default value generator block
|
154
|
+
# @yieldreturn [Object] the default value
|
48
155
|
#
|
49
156
|
# @return [self] the builder for method chaining
|
50
157
|
def default: (?untyped? value_or_proc) ?{ (?) -> untyped } -> self
|
@@ -53,7 +160,33 @@ module Domainic
|
|
53
160
|
|
54
161
|
alias default_value default
|
55
162
|
|
56
|
-
#
|
163
|
+
# Provides a way to add descriptive metadata to attributes. Descriptions improve code clarity by documenting
|
164
|
+
# the purpose or behavior of an attribute. These descriptions can be short or detailed, depending on the
|
165
|
+
# context.
|
166
|
+
#
|
167
|
+
# @note Descriptions are optional but highly recommended for improving readability and maintainability of the
|
168
|
+
# code.
|
169
|
+
#
|
170
|
+
# @example Adding a short description
|
171
|
+
# class MagicItem
|
172
|
+
# include Domainic::Attributer
|
173
|
+
#
|
174
|
+
# argument :name do
|
175
|
+
# desc 'The name of the magic item, must be unique'
|
176
|
+
# end
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# @example Adding a detailed description
|
180
|
+
# class MagicItem
|
181
|
+
# include Domainic::Attributer
|
182
|
+
#
|
183
|
+
# option :power_level do
|
184
|
+
# description 'The magical power level of the item, ranging from 0 to 100.
|
185
|
+
# Higher power levels increase effectiveness but may come with
|
186
|
+
# increased risks during use.'
|
187
|
+
# validate_with ->(val) { val.between?(0, 100) }
|
188
|
+
# end
|
189
|
+
# end
|
57
190
|
#
|
58
191
|
# @param text [String] the description text
|
59
192
|
#
|
@@ -62,7 +195,33 @@ module Domainic
|
|
62
195
|
|
63
196
|
alias desc description
|
64
197
|
|
65
|
-
#
|
198
|
+
# Ensures that an attribute defined in the block DSL cannot have a `nil` value. This validation is enforced
|
199
|
+
# during initialization and when modifying the attribute value at runtime. Use `non_nilable` for attributes that
|
200
|
+
# must always have a value.
|
201
|
+
#
|
202
|
+
# @example Preventing `nil` values for an attribute
|
203
|
+
# class Ninja
|
204
|
+
# include Domainic::Attributer
|
205
|
+
#
|
206
|
+
# argument :code_name do
|
207
|
+
# non_nilable
|
208
|
+
# end
|
209
|
+
# end
|
210
|
+
#
|
211
|
+
# Ninja.new(nil) # Raises ArgumentError: nil value is not allowed
|
212
|
+
#
|
213
|
+
# @example Combining `non_nilable` with other features
|
214
|
+
# class Ninja
|
215
|
+
# include Domainic::Attributer
|
216
|
+
#
|
217
|
+
# argument :rank do
|
218
|
+
# desc 'The rank of the ninja, such as Genin or Chunin'
|
219
|
+
# non_nilable
|
220
|
+
# end
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# ninja = Ninja.new('Genin') # => Works
|
224
|
+
# ninja.rank = nil # Raises ArgumentError: nil value is not allowed
|
66
225
|
#
|
67
226
|
# @return [self] the builder for method chaining
|
68
227
|
def non_nilable: () -> self
|
@@ -81,68 +240,398 @@ module Domainic
|
|
81
240
|
|
82
241
|
alias not_nullable non_nilable
|
83
242
|
|
84
|
-
#
|
243
|
+
# Allows defining a callback to be triggered whenever the attribute's value changes. The callback receives the
|
244
|
+
# old value and the new value as arguments, enabling custom logic to be executed on changes. Use `on_change` to
|
245
|
+
# react to changes in attribute values, such as updating dependent attributes or triggering side effects.
|
246
|
+
#
|
247
|
+
# @example Reacting to changes in an attribute
|
248
|
+
# class VideoGame
|
249
|
+
# include Domainic::Attributer
|
250
|
+
#
|
251
|
+
# option :health do
|
252
|
+
# default 100
|
253
|
+
# on_change ->(old_value, new_value) {
|
254
|
+
# puts "Health changed from #{old_value} to #{new_value}"
|
255
|
+
# }
|
256
|
+
# end
|
257
|
+
# end
|
258
|
+
#
|
259
|
+
# game = VideoGame.new
|
260
|
+
# game.health = 50 # Outputs: Health changed from 100 to 50
|
261
|
+
#
|
262
|
+
# @example Performing complex logic on change
|
263
|
+
# class VideoGame
|
264
|
+
# include Domainic::Attributer
|
265
|
+
#
|
266
|
+
# option :power_ups do
|
267
|
+
# default []
|
268
|
+
# on_change do |old_value, new_value|
|
269
|
+
# new_items = new_value - old_value
|
270
|
+
# lost_items = old_value - new_value
|
271
|
+
#
|
272
|
+
# new_items.each { |item| activate_power_up(item) }
|
273
|
+
# lost_items.each { |item| deactivate_power_up(item) }
|
274
|
+
# end
|
275
|
+
# end
|
276
|
+
#
|
277
|
+
# private
|
278
|
+
#
|
279
|
+
# def activate_power_up(item)
|
280
|
+
# puts "Activated power-up: #{item}"
|
281
|
+
# end
|
282
|
+
#
|
283
|
+
# def deactivate_power_up(item)
|
284
|
+
# puts "Deactivated power-up: #{item}"
|
285
|
+
# end
|
286
|
+
# end
|
287
|
+
#
|
288
|
+
# game = VideoGame.new
|
289
|
+
# game.power_ups = ['Shield', 'Speed Boost']
|
290
|
+
# # Outputs: Activated power-up: Shield
|
291
|
+
# # Activated power-up: Speed Boost
|
292
|
+
# game.power_ups = ['Shield']
|
293
|
+
# # Outputs: Deactivated power-up: Speed Boost
|
85
294
|
#
|
86
295
|
# @param proc [Proc, nil] optional callback handler
|
87
296
|
# @yield optional callback block
|
297
|
+
# @yieldparam old_value [Object] the previous value of the attribute
|
298
|
+
# @yieldparam new_value [Object] the new value of the attribute
|
299
|
+
# @yieldreturn [void]
|
88
300
|
#
|
89
301
|
# @return [self] the builder for method chaining
|
90
302
|
def on_change: (?Attribute::Callback::handler? proc) ?{ (untyped old_value, untyped new_value) -> void } -> self
|
91
303
|
|
92
|
-
#
|
304
|
+
# Sets both the read and write visibility of an attribute to private. This ensures the attribute can only be
|
305
|
+
# accessed or modified within the class itself.
|
306
|
+
#
|
307
|
+
# @example Making an attribute private
|
308
|
+
# class SecretAgent
|
309
|
+
# include Domainic::Attributer
|
310
|
+
#
|
311
|
+
# option :real_name do
|
312
|
+
# desc 'The real name of the agent, hidden from external access.'
|
313
|
+
# private
|
314
|
+
# end
|
315
|
+
# end
|
316
|
+
#
|
317
|
+
# agent = SecretAgent.new(real_name: 'James Bond')
|
318
|
+
# agent.real_name # Raises NoMethodError: private method `real_name' called for #<SecretAgent>
|
319
|
+
# agent.real_name = 'John Doe' # Raises NoMethodError: private method `real_name=' called for #<SecretAgent>
|
93
320
|
#
|
94
321
|
# @return [self] the builder for method chaining
|
95
322
|
def private: () -> self
|
96
323
|
|
97
|
-
#
|
324
|
+
# Sets the read visibility of an attribute to private, allowing the attribute to be read only within the class
|
325
|
+
# itself. The write visibility remains unchanged unless explicitly modified. Use `private_read` when the value
|
326
|
+
# of an attribute should be hidden from external consumers but writable by external code if needed.
|
327
|
+
#
|
328
|
+
# @example Making the reader private
|
329
|
+
# class SecretAgent
|
330
|
+
# include Domainic::Attributer
|
331
|
+
#
|
332
|
+
# option :mission_code do
|
333
|
+
# desc 'The secret mission code, readable only within the class.'
|
334
|
+
# private_read
|
335
|
+
# default { generate_code }
|
336
|
+
# end
|
337
|
+
#
|
338
|
+
# private
|
339
|
+
#
|
340
|
+
# def generate_code
|
341
|
+
# "M-#{rand(1000..9999)}"
|
342
|
+
# end
|
343
|
+
# end
|
344
|
+
#
|
345
|
+
# agent = SecretAgent.new
|
346
|
+
# agent.mission_code # Raises NoMethodError: private method `mission_code' called for #<SecretAgent>
|
347
|
+
# agent.mission_code = 'Override Code' # Works, as write visibility is still public
|
98
348
|
#
|
99
349
|
# @return [self] the builder for method chaining
|
100
350
|
def private_read: () -> self
|
101
351
|
|
102
|
-
#
|
352
|
+
# Sets the write visibility of an attribute to private, allowing the attribute to be modified only within the
|
353
|
+
# class. The read visibility remains unchanged unless explicitly modified. Use `private_write` to ensure that an
|
354
|
+
# attribute's value can only be updated internally, while still allowing external code to read its value if
|
355
|
+
# needed.
|
356
|
+
#
|
357
|
+
# @example Making the writer private
|
358
|
+
# class SecretAgent
|
359
|
+
# include Domainic::Attributer
|
360
|
+
#
|
361
|
+
# option :mission_code do
|
362
|
+
# desc 'The secret mission code, writable only within the class.'
|
363
|
+
# private_write
|
364
|
+
# default { generate_code }
|
365
|
+
# end
|
366
|
+
#
|
367
|
+
# private
|
368
|
+
#
|
369
|
+
# def generate_code
|
370
|
+
# "M-#{rand(1000..9999)}"
|
371
|
+
# end
|
372
|
+
# end
|
373
|
+
#
|
374
|
+
# agent = SecretAgent.new
|
375
|
+
# agent.mission_code # => "M-1234"
|
376
|
+
# agent.mission_code = '007' # Raises NoMethodError: private method `mission_code=' called for #<SecretAgent>
|
103
377
|
#
|
104
378
|
# @return [self] the builder for method chaining
|
105
379
|
def private_write: () -> self
|
106
380
|
|
107
|
-
#
|
381
|
+
# Sets both the read and write visibility of an attribute to protected, allowing access only within the class
|
382
|
+
# and its subclasses. This visibility restricts external access entirely. Use `protected` to share attributes
|
383
|
+
# within a class hierarchy while keeping them hidden from external consumers.
|
384
|
+
#
|
385
|
+
# @example Defining a protected attribute
|
386
|
+
# class SecretAgent
|
387
|
+
# include Domainic::Attributer
|
388
|
+
#
|
389
|
+
# option :mission_code do
|
390
|
+
# protected
|
391
|
+
# description 'The mission code, accessible only within the class and its subclasses.'
|
392
|
+
# end
|
393
|
+
# end
|
394
|
+
#
|
395
|
+
# class DoubleAgent < SecretAgent
|
396
|
+
# def reveal_code
|
397
|
+
# self.mission_code
|
398
|
+
# end
|
399
|
+
# end
|
400
|
+
#
|
401
|
+
# agent = SecretAgent.new(mission_code: '007')
|
402
|
+
# agent.mission_code # Raises NoMethodError
|
403
|
+
# DoubleAgent.new.reveal_code # => '007'
|
108
404
|
#
|
109
405
|
# @return [self] the builder for method chaining
|
110
406
|
def protected: () -> self
|
111
407
|
|
112
|
-
#
|
408
|
+
# Sets both the read and write visibility of an attribute to protected. This allows the attribute to be accessed
|
409
|
+
# or modified only within the class and its subclasses. Use `protected` for attributes that should be accessible
|
410
|
+
# to the class and its subclasses but hidden from external consumers.
|
411
|
+
#
|
412
|
+
# @example Making an attribute protected
|
413
|
+
# class SecretAgent
|
414
|
+
# include Domainic::Attributer
|
415
|
+
#
|
416
|
+
# option :mission_code do
|
417
|
+
# desc 'The mission code, accessible only within the class or subclasses.'
|
418
|
+
# protected
|
419
|
+
# end
|
420
|
+
# end
|
421
|
+
#
|
422
|
+
# class DoubleAgent < SecretAgent
|
423
|
+
# def reveal_code
|
424
|
+
# self.mission_code
|
425
|
+
# end
|
426
|
+
# end
|
427
|
+
#
|
428
|
+
# agent = SecretAgent.new(mission_code: '007')
|
429
|
+
# agent.mission_code # Raises NoMethodError
|
430
|
+
# DoubleAgent.new.reveal_code # => '007'
|
113
431
|
#
|
114
432
|
# @return [self] the builder for method chaining
|
115
433
|
def protected_read: () -> self
|
116
434
|
|
117
|
-
#
|
435
|
+
# Sets both the read and write visibility of an attribute to protected. This allows the attribute to be accessed
|
436
|
+
# or modified only within the class and its subclasses. Use `protected` for attributes that should be accessible
|
437
|
+
# to the class and its subclasses but hidden from external consumers.
|
438
|
+
#
|
439
|
+
# @example Making an attribute protected
|
440
|
+
# class SecretAgent
|
441
|
+
# include Domainic::Attributer
|
442
|
+
#
|
443
|
+
# option :mission_code do
|
444
|
+
# desc 'The mission code, accessible only within the class or subclasses.'
|
445
|
+
# protected
|
446
|
+
# end
|
447
|
+
# end
|
448
|
+
#
|
449
|
+
# class DoubleAgent < SecretAgent
|
450
|
+
# def reveal_code
|
451
|
+
# self.mission_code
|
452
|
+
# end
|
453
|
+
# end
|
454
|
+
#
|
455
|
+
# agent = SecretAgent.new(mission_code: '007')
|
456
|
+
# agent.mission_code # Raises NoMethodError
|
457
|
+
# DoubleAgent.new.reveal_code # => '007'
|
118
458
|
#
|
119
459
|
# @return [self] the builder for method chaining
|
120
460
|
def protected_write: () -> self
|
121
461
|
|
122
|
-
#
|
462
|
+
# Explicitly sets both the read and write visibility of an attribute to public, overriding any inherited or
|
463
|
+
# previously set visibility. By default, attributes are public, so this is typically used to revert an
|
464
|
+
# attribute's visibility if it was changed in a parent class or module.
|
465
|
+
#
|
466
|
+
# @note Attributes are public by default. Use `public` explicitly to override inherited or modified visibility.
|
467
|
+
#
|
468
|
+
# @example Reverting visibility to public in a subclass
|
469
|
+
# class SecretAgent
|
470
|
+
# include Domainic::Attributer
|
471
|
+
#
|
472
|
+
# option :mission_code do
|
473
|
+
# desc 'The mission code, protected in the base class.'
|
474
|
+
# private
|
475
|
+
# end
|
476
|
+
# end
|
477
|
+
#
|
478
|
+
# class FieldAgent < SecretAgent
|
479
|
+
# option :mission_code do
|
480
|
+
# desc 'The mission code, made public in the subclass.'
|
481
|
+
# public
|
482
|
+
# end
|
483
|
+
# end
|
484
|
+
#
|
485
|
+
# agent = FieldAgent.new(mission_code: '007')
|
486
|
+
# agent.mission_code # => '007' (now accessible)
|
487
|
+
# agent.mission_code = '008' # Works, as visibility is public in the subclass
|
123
488
|
#
|
124
489
|
# @return [self] the builder for method chaining
|
125
490
|
def public: () -> self
|
126
491
|
|
127
|
-
#
|
492
|
+
# Explicitly sets the read visibility of an attribute to public, overriding any inherited or previously set
|
493
|
+
# visibility. By default, attributes are readable publicly, so this is typically used to revert the read
|
494
|
+
# visibility of an attribute if it was modified in a parent class or module.
|
495
|
+
#
|
496
|
+
# @note Attributes are publicly readable by default. Use `public_read` explicitly to override inherited or
|
497
|
+
# modified visibility.
|
498
|
+
#
|
499
|
+
# @example Reverting read visibility to public in a subclass
|
500
|
+
# class SecretAgent
|
501
|
+
# include Domainic::Attributer
|
502
|
+
#
|
503
|
+
# option :mission_code do
|
504
|
+
# desc 'The mission code, privately readable in the base class.'
|
505
|
+
# private_read
|
506
|
+
# end
|
507
|
+
# end
|
508
|
+
#
|
509
|
+
# class FieldAgent < SecretAgent
|
510
|
+
# option :mission_code do
|
511
|
+
# desc 'The mission code, made publicly readable in the subclass.'
|
512
|
+
# public_read
|
513
|
+
# end
|
514
|
+
# end
|
515
|
+
#
|
516
|
+
# agent = FieldAgent.new(mission_code: '007')
|
517
|
+
# agent.mission_code # => '007' (now publicly readable)
|
518
|
+
# agent.mission_code = '008' # Raises NoMethodError, as write visibility is still private
|
128
519
|
#
|
129
520
|
# @return [self] the builder for method chaining
|
130
521
|
def public_read: () -> self
|
131
522
|
|
132
|
-
#
|
523
|
+
# Explicitly sets the write visibility of an attribute to public, overriding any inherited or previously set
|
524
|
+
# visibility. By default, attributes are writable publicly, so this is typically used to revert the write
|
525
|
+
# visibility of an attribute if it was modified in a parent class or module.
|
526
|
+
#
|
527
|
+
# @note Attributes are publicly writable by default. Use `public_write` explicitly to override inherited or
|
528
|
+
# modified visibility.
|
529
|
+
#
|
530
|
+
# @example Reverting write visibility to public in a subclass
|
531
|
+
# class SecretAgent
|
532
|
+
# include Domainic::Attributer
|
533
|
+
#
|
534
|
+
# option :mission_code do
|
535
|
+
# desc 'The mission code, writable only within the class or subclasses in the base class.'
|
536
|
+
# private_write
|
537
|
+
# end
|
538
|
+
# end
|
539
|
+
#
|
540
|
+
# class FieldAgent < SecretAgent
|
541
|
+
# option :mission_code do
|
542
|
+
# desc 'The mission code, now writable publicly in the subclass.'
|
543
|
+
# public_write
|
544
|
+
# end
|
545
|
+
# end
|
546
|
+
#
|
547
|
+
# agent = FieldAgent.new(mission_code: '007')
|
548
|
+
# agent.mission_code # Raises NoMethodError, as read visibility remains restricted
|
549
|
+
# agent.mission_code = '008' # Works, as write visibility is now public
|
133
550
|
#
|
134
551
|
# @return [self] the builder for method chaining
|
135
552
|
def public_write: () -> self
|
136
553
|
|
137
|
-
#
|
554
|
+
# Marks an {ClassMethods#option option} attribute as required, ensuring that a value must be provided during
|
555
|
+
# initialization. If a required attribute is not supplied, an error is raised. Use `required` to enforce
|
556
|
+
# mandatory attributes.
|
557
|
+
#
|
558
|
+
# @note required options are enforced during initialization; as long as the option is provided
|
559
|
+
# (even if it is `nil`) no error will be raised.
|
560
|
+
#
|
561
|
+
# @example Defining a required attribute
|
562
|
+
# class Superhero
|
563
|
+
# include Domainic::Attributer
|
564
|
+
#
|
565
|
+
# option :name do
|
566
|
+
# desc 'The name of the superhero, which must be provided.'
|
567
|
+
# required
|
568
|
+
# end
|
569
|
+
# end
|
570
|
+
#
|
571
|
+
# Superhero.new # Raises ArgumentError: missing required attribute: name
|
572
|
+
# Superhero.new(name: 'Spiderman') # Works, as the required attribute is supplied
|
573
|
+
# Superhero.new(name: nil) # Works, as the required attribute is supplied (even if it is nil)
|
138
574
|
#
|
139
575
|
# @return [self] the builder for method chaining
|
140
576
|
def required: () -> self
|
141
577
|
|
142
|
-
#
|
578
|
+
# Adds a custom validation to an attribute, allowing you to define specific criteria that the attribute's value
|
579
|
+
# must meet. Validators can be Procs, lambdas, or symbols referencing instance methods. Validation occurs during
|
580
|
+
# initialization and whenever the attribute value is updated. Use `validate_with` to enforce rules beyond type
|
581
|
+
# or presence, such as ranges, formats, or custom logic.
|
582
|
+
#
|
583
|
+
# @example Adding a simple validation
|
584
|
+
# class Superhero
|
585
|
+
# include Domainic::Attributer
|
586
|
+
#
|
587
|
+
# argument :power_level do
|
588
|
+
# desc 'The power level of the superhero, which must be an integer between 0 and 100.'
|
589
|
+
# validate_with ->(val) { val.is_a?(Integer) && val.between?(0, 100) }
|
590
|
+
# end
|
591
|
+
# end
|
592
|
+
#
|
593
|
+
# Superhero.new(150) # Raises ArgumentError: invalid value for power_level
|
594
|
+
# Superhero.new(85) # Works, as 85 is within the valid range
|
595
|
+
#
|
596
|
+
# @example Using an instance method as a validator
|
597
|
+
# class Superhero
|
598
|
+
# include Domainic::Attributer
|
599
|
+
#
|
600
|
+
# argument :alias_name do
|
601
|
+
# desc 'The alias name of the superhero, validated using an instance method.'
|
602
|
+
# validate_with :validate_alias_name
|
603
|
+
# end
|
604
|
+
#
|
605
|
+
# private
|
606
|
+
#
|
607
|
+
# def validate_alias_name(value)
|
608
|
+
# value.is_a?(String) && value.match?(/\A[A-Z][a-z]+\z/)
|
609
|
+
# end
|
610
|
+
# end
|
611
|
+
#
|
612
|
+
# Superhero.new('Spiderman') # Works, as the alias name matches the validation criteria
|
613
|
+
# Superhero.new('spiderman') # Raises ArgumentError: invalid value for alias_name
|
614
|
+
#
|
615
|
+
# @example Combining multiple validators
|
616
|
+
# class Vehicle
|
617
|
+
# include Domainic::Attributer
|
618
|
+
#
|
619
|
+
# option :speed do
|
620
|
+
# desc 'The speed of the vehicle, which must be a non-negative number.'
|
621
|
+
# validate_with ->(val) { val.is_a?(Numeric) }
|
622
|
+
# validate_with do |val|
|
623
|
+
# val.zero? || val.positive?
|
624
|
+
# end
|
625
|
+
# end
|
626
|
+
# end
|
627
|
+
#
|
628
|
+
# Vehicle.new(speed: -10) # Raises ArgumentError: invalid value for speed
|
629
|
+
# Vehicle.new(speed: 50) # Works, as 50 meets all validation criteria
|
143
630
|
#
|
144
631
|
# @param object_or_proc [Object, Proc, nil] optional validation handler
|
145
632
|
# @yield optional validation block
|
633
|
+
# @yieldparam value [Object] the value to validate
|
634
|
+
# @yieldreturn [Boolean] `true` if the value is valid, `false` otherwise
|
146
635
|
#
|
147
636
|
# @return [self] the builder for method chaining
|
148
637
|
def validate_with: (?Attribute::Validator::handler? object_or_proc) ?{ (untyped value) -> boolish } -> self
|