domainic-attributer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE +21 -0
  4. data/README.md +396 -0
  5. data/lib/domainic/attributer/attribute/callback.rb +68 -0
  6. data/lib/domainic/attributer/attribute/coercer.rb +93 -0
  7. data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +68 -0
  8. data/lib/domainic/attributer/attribute/signature.rb +338 -0
  9. data/lib/domainic/attributer/attribute/validator.rb +128 -0
  10. data/lib/domainic/attributer/attribute.rb +256 -0
  11. data/lib/domainic/attributer/attribute_set.rb +208 -0
  12. data/lib/domainic/attributer/class_methods.rb +247 -0
  13. data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +247 -0
  14. data/lib/domainic/attributer/dsl/attribute_builder.rb +233 -0
  15. data/lib/domainic/attributer/dsl/initializer.rb +130 -0
  16. data/lib/domainic/attributer/dsl/method_injector.rb +97 -0
  17. data/lib/domainic/attributer/dsl.rb +5 -0
  18. data/lib/domainic/attributer/instance_methods.rb +65 -0
  19. data/lib/domainic/attributer/undefined.rb +44 -0
  20. data/lib/domainic/attributer.rb +114 -0
  21. data/lib/domainic-attributer.rb +3 -0
  22. data/sig/domainic/attributer/attribute/callback.rbs +48 -0
  23. data/sig/domainic/attributer/attribute/coercer.rbs +59 -0
  24. data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +46 -0
  25. data/sig/domainic/attributer/attribute/signature.rbs +223 -0
  26. data/sig/domainic/attributer/attribute/validator.rbs +83 -0
  27. data/sig/domainic/attributer/attribute.rbs +150 -0
  28. data/sig/domainic/attributer/attribute_set.rbs +134 -0
  29. data/sig/domainic/attributer/class_methods.rbs +151 -0
  30. data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +130 -0
  31. data/sig/domainic/attributer/dsl/attribute_builder.rbs +156 -0
  32. data/sig/domainic/attributer/dsl/initializer.rbs +91 -0
  33. data/sig/domainic/attributer/dsl/method_injector.rbs +66 -0
  34. data/sig/domainic/attributer/dsl.rbs +1 -0
  35. data/sig/domainic/attributer/instance_methods.rbs +53 -0
  36. data/sig/domainic/attributer/undefined.rbs +14 -0
  37. data/sig/domainic/attributer.rbs +69 -0
  38. data/sig/domainic-attributer.rbs +1 -0
  39. data/sig/manifest.yaml +2 -0
  40. metadata +89 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a25981d1a30af4ee16bd8339c975c935eee76c0bcda186af868e28c4ae4874f8
4
+ data.tar.gz: c9dd81c87a0cc108010e4463beba162f30830182c485624ba0782de971ebb93f
5
+ SHA512:
6
+ metadata.gz: 97e1331498dc427a98bcaf3dfa38828d1aafe02466bcbd2ae84101542b476a92543f611eed55f842537da5224fa568c8f812f475f21fec6c4831e1704c2b456e
7
+ data.tar.gz: f2a5d8b6540cc7aba2eed108a5181ea3db350dc8f3a52f30a386e94f78b0ebd9df984697ad7f92071f1bd338fa3c25619be0ff4a5e3054d52703570ba2884ea9
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog], and this project adheres to [Break Versioning].
6
+
7
+ ## [Unreleased]
8
+
9
+ [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
10
+ [Break Versioning]: https://www.taoensso.com/break-versioning
11
+
12
+ <!-- versions -->
13
+
14
+ [Unreleased]: https://github.com/domainic/domainic/tree/main/domainic-attributer
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Aaron Allen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,396 @@
1
+ # Domainic::Attributer
2
+
3
+ [![Domainic::Attributer Version](https://badge.fury.io/rb/domainic-attributer.svg)](https://rubygems.org/gems/domainic-attributer)
4
+
5
+ Domainic::Attributer is a powerful toolkit for Ruby that brings clarity and safety to your class attributes. It's
6
+ designed to solve common Domain-Driven Design (DDD) challenges by making your class attributes self-documenting,
7
+ type-safe, and well-behaved. Ever wished your class attributes could:
8
+
9
+ * Validate themselves to ensure they only accept correct values?
10
+ * Transform input data automatically into the right format?
11
+ * Have clear, enforced visibility rules?
12
+ * Handle their own default values intelligently?
13
+ * Tell you when they change?
14
+ * Distinguish between required arguments and optional settings?
15
+
16
+ That's exactly what Domainic::Attributer does! It's particularly useful when building domain models, value objects, or
17
+ any Ruby classes where data integrity and clear interfaces matter. Instead of writing repetitive validation code, manual
18
+ type checking, and custom attribute methods, let Domainic::Attributer handle the heavy lifting while you focus on your
19
+ domain logic.
20
+
21
+ Think of it as giving your attributes a brain - they know what they want, how they should behave, and they're not afraid
22
+ to speak up when something's not right!
23
+
24
+ ## Installation
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem 'domainic-attributer'
30
+ ```
31
+
32
+ Or install it yourself as:
33
+
34
+ ```bash
35
+ gem install domainic-attributer
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Basic Attributes
41
+
42
+ Getting started with Domainic::Attributer is as easy as including the module and declaring your attributes:
43
+
44
+ ```ruby
45
+ class Person
46
+ include Domainic::Attributer
47
+
48
+ argument :name
49
+ option :age, default: nil
50
+ end
51
+
52
+ person = Person.new("Alice", age: 30)
53
+ person.name # => "Alice"
54
+ person.age # => 30
55
+ ```
56
+
57
+ ### Arguments vs Options
58
+
59
+ Domainic::Attributer gives you two ways to define attributes:
60
+
61
+ * `argument`: Required positional parameters that must be provided in order
62
+ * `option`: Named parameters that can be provided in any order (and are optional by default)
63
+
64
+ ```ruby
65
+ class Hero
66
+ include Domainic::Attributer
67
+
68
+ argument :name # Required, must be first
69
+ argument :power # Required, must be second
70
+ option :catchphrase # Optional, can be provided by name
71
+ option :sidekick # Optional, can be provided by name
72
+ end
73
+
74
+ # All valid ways to create a hero:
75
+ Hero.new("Spider-Man", "Web-slinging", catchphrase: "With great power...")
76
+ Hero.new("Batman", "Being rich", sidekick: "Robin")
77
+ Hero.new("Wonder Woman", "Super strength")
78
+ ```
79
+
80
+ #### Argument Ordering and Default Values
81
+
82
+ Arguments in Domainic::Attributer follow special ordering rules based on whether they have defaults:
83
+
84
+ * Arguments without defaults are required and are automatically moved to the front of the argument list
85
+ * Arguments with defaults are optional and are moved to the end of the argument list
86
+ * Within each group (with/without defaults), arguments maintain their order of declaration
87
+
88
+ This means the actual position when providing arguments to the constructor will be different from their declaration
89
+ order:
90
+
91
+ ```ruby
92
+ class EmailMessage
93
+ include Domainic::Attributer
94
+
95
+ # This will be the first argument (no default)
96
+ argument :to
97
+
98
+ # This will be the third argument (has default)
99
+ argument :priority, default: :normal
100
+
101
+ # This will be the second argument (no default)
102
+ argument :subject
103
+ end
104
+
105
+ # Arguments must be provided in their sorted order,
106
+ # with required arguments first:
107
+ EmailMessage.new("user@example.com", "Welcome!", :high)
108
+ # => #<EmailMessage:0x00007f9b1b8b3b10 @to="user@example.com", @priority=:high, @subject="Welcome!">
109
+
110
+ # If you try to provide the arguments in their declaration order, you'll get undesired results:
111
+ EmailMessage.new("user@example.com", :high, "Welcome!")
112
+ # => #<EmailMessage:0x00007f9b1b8b3b10 @to="user@example.com", @priority="Welcome!", @subject=:high>
113
+ ```
114
+
115
+ This behavior ensures that required arguments are provided first and optional arguments (those with defaults) come
116
+ after, making argument handling more predictable. You can rely on this ordering regardless of how you declare the
117
+ arguments in your class. Best practice is to declare arguments without defaults first, followed by those with defaults.
118
+
119
+ ### Nilability And Option Requirements
120
+
121
+ Be explicit about nil values:
122
+
123
+ ```ruby
124
+ class User
125
+ include Domainic::Attributer
126
+
127
+ argument :email do
128
+ non_nilable # or not_null, non_null, etc.
129
+ end
130
+
131
+ option :nickname do
132
+ default nil # Explicitly allow nil
133
+ end
134
+ end
135
+ ```
136
+
137
+ Ensure certain options are always provided:
138
+
139
+ ```ruby
140
+ class Order
141
+ include Domainic::Attributer
142
+
143
+ option :items, required: true
144
+ option :status, Symbol
145
+ end
146
+
147
+ Order.new(option: ['item1', 'item2']) # OK
148
+ Order.new(status: :pending) # Raises ArgumentError
149
+ ```
150
+
151
+ #### Required vs NonNilable
152
+
153
+ `required` and `non_nilable` are similar but not identical. `required` means the option must be provided when the object
154
+ is created, while `non_nilable` means the option must not be nil. A `required` option can still be nil if it's provided.
155
+
156
+ ```ruby
157
+ class User
158
+ include Domainic::Attributer
159
+
160
+ option :email, String do
161
+ required
162
+ non_nilable
163
+ end
164
+
165
+ option :nickname, String do
166
+ required
167
+ end
168
+ end
169
+
170
+ User.new(email: 'example@example.com', nickname: nil) # OK
171
+ User.new(email: nil, nickname: 'example') # Raises ArgumentError because email is non_nilable
172
+ User.new(email: 'example@example.com') # Raises ArgumentError because nickname is required
173
+
174
+ user = User.new(email: 'example@example.com', nickname: 'example')
175
+ user.nickname = nil # OK
176
+ user.email = nil # Raises ArgumentError because email is non_nilable
177
+ ```
178
+
179
+ ### Type Validation
180
+
181
+ Keep your data clean with built-in type validation:
182
+
183
+ ```ruby
184
+ class BankAccount
185
+ include Domainic::Attributer
186
+
187
+ argument :account_name, String # Direct class validation
188
+ argument :opened_at, Time # Another direct class example
189
+ option :balance, Integer, default: 0 # Combining class validation with defaults
190
+ option :status, ->(val) { [:active, :closed].include?(val) } # Custom validation
191
+ end
192
+
193
+ # Will raise ArgumentError:
194
+ BankAccount.new(:my_account_name, Time.now)
195
+ BankAccount.new("my_account_name", "not a time")
196
+ BankAccount.new("my_account_name", Time.now, balance: "not an integer")
197
+ BankAccount.new("my_account_name", Time.now, balance: 100, status: :not_included_in_the_allow_list)
198
+ ```
199
+
200
+ ### Documentation
201
+
202
+ Make your attributes self-documenting:
203
+
204
+ ```ruby
205
+ class Car
206
+ include Domainic::Attributer
207
+
208
+ argument :make, String do
209
+ desc "The make of the car"
210
+ end
211
+
212
+ argument :model, String do
213
+ description "The model of the car"
214
+ end
215
+
216
+ argument :year, ->(value) { value.is_a?(Integer) && value >= 1900 && value <= Time.now.year } do
217
+ description "The year the car was made"
218
+ end
219
+ end
220
+ ```
221
+
222
+ ### Value Coercion
223
+
224
+ Transform input values automatically:
225
+
226
+ ```ruby
227
+ class Temperature
228
+ include Domainic::Attributer
229
+
230
+ argument :celsius do |value|
231
+ coerce_with ->(val) { val.to_f }
232
+ validate_with ->(val) { val.is_a?(Float) }
233
+ end
234
+
235
+ option :unit, default: "C" do |value|
236
+ validate_with ->(val) { ["C", "F"].include?(val) }
237
+ end
238
+ end
239
+
240
+ temp = Temperature.new("24.5") # Automatically converted to Float
241
+ temp.celsius # => 24.5
242
+ ```
243
+
244
+ ### Custom Validation
245
+
246
+ Domainic::Attributer provides flexible validation options that can be combined to create sophisticated validation rules.
247
+ You can:
248
+
249
+ * Use Ruby classes directly to validate types
250
+ * Use Procs/lambdas for custom validation logic
251
+ * Chain multiple validations
252
+ * Combine validations with coercions
253
+
254
+ ```ruby
255
+ class BankTransfer
256
+ include Domainic::Attributer
257
+
258
+ # Combine coercion and multiple validations
259
+ argument :amount do
260
+ coerce_with ->(val) { val.to_f } # First coerce to float
261
+ validate_with Float # Then validate it's a float
262
+ validate_with ->(val) { val.positive? } # And validate it's positive
263
+ end
264
+
265
+ # Different validation styles
266
+ argument :status do
267
+ validate_with Symbol # Must be a Symbol
268
+ validate_with ->(val) { [:pending, :completed, :failed].include?(val) } # Must be one of these values
269
+ end
270
+
271
+ # Validation with custom error handling
272
+ argument :reference_number do
273
+ validate_with ->(val) {
274
+ raise ArgumentError, "Reference must be 8 characters" unless val.length == 8
275
+ true
276
+ }
277
+ end
278
+ end
279
+
280
+ # These will work:
281
+ BankTransfer.new("50.0", :pending, "12345678") # amount coerced to 50.0
282
+ BankTransfer.new(75.25, :completed, "ABCD1234") # amount already a float
283
+
284
+ # These will raise ArgumentError:
285
+ BankTransfer.new(-10, :pending, "12345678") # amount must be positive
286
+ BankTransfer.new(100, :invalid, "12345678") # invalid status
287
+ BankTransfer.new(100, :pending, "123") # invalid reference number
288
+ ```
289
+
290
+ Validations are run in the order they're defined, after any coercions. This lets you build up complex validation rules
291
+ while keeping them readable and maintainable.
292
+
293
+ ### Visibility Control
294
+
295
+ Control access to your attributes:
296
+
297
+ ```ruby
298
+ class SecretAgent
299
+ include Domainic::Attributer
300
+
301
+ argument :code_name
302
+ option :real_name do
303
+ private_read # Can't read real_name from outside
304
+ private_write # Can't write real_name from outside
305
+ end
306
+ option :mission do
307
+ protected # Both read and write are protected
308
+ end
309
+ end
310
+ ```
311
+
312
+ ### Change Callbacks
313
+
314
+ React to attribute changes:
315
+
316
+ ```ruby
317
+ class Thermostat
318
+ include Domainic::Attributer
319
+
320
+ option :temperature do
321
+ default 20
322
+ on_change ->(old_val, new_val) {
323
+ puts "Temperature changing from #{old_val}°C to #{new_val}°C"
324
+ }
325
+ end
326
+ end
327
+ ```
328
+
329
+ ### Default Values
330
+
331
+ Provide static defaults or generate them dynamically:
332
+
333
+ ```ruby
334
+ class Order
335
+ include Domainic::Attributer
336
+
337
+ argument :items
338
+ option :created_at do
339
+ default { Time.now } # Dynamic default
340
+ end
341
+ option :status do
342
+ default "pending" # Static default
343
+ end
344
+ end
345
+ ```
346
+
347
+ ### Custom Method Names
348
+
349
+ Don't like `argument` and `option`? Create your own interface:
350
+
351
+ ```ruby
352
+ class Configuration
353
+ include Domainic.Attributer(argument: :param, option: :setting)
354
+
355
+ param :environment
356
+ setting :debug_mode, default: false
357
+ end
358
+ ```
359
+
360
+ or turn off one of the methods entirely:
361
+
362
+ ```ruby
363
+ class Configuration
364
+ include Domainic.Attributer(argument: nil)
365
+
366
+ option :environment
367
+ end
368
+ ```
369
+
370
+ ### Serialization
371
+
372
+ Convert your objects to hashes easily:
373
+
374
+ ```ruby
375
+ class Product
376
+ include Domainic::Attributer
377
+
378
+ argument :name
379
+ argument :price
380
+ option :description, default: ""
381
+ option :internal_id do
382
+ private # Won't be included in to_h output
383
+ end
384
+ end
385
+
386
+ product = Product.new("Widget", 9.99, description: "A fantastic widget")
387
+ product.to_h # => { name: "Widget", price: 9.99, description: "A fantastic widget" }
388
+ ```
389
+
390
+ ## Contributing
391
+
392
+ Bug reports and pull requests are welcome on GitHub.
393
+
394
+ ## License
395
+
396
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer/attribute/mixin/belongs_to_attribute'
4
+
5
+ module Domainic
6
+ module Attributer
7
+ class Attribute
8
+ # A class responsible for managing change callbacks for an attribute.
9
+ #
10
+ # This class handles the execution of callbacks that are triggered when an
11
+ # attribute's value changes. Each callback must be a Proc that accepts two
12
+ # arguments: the old value and the new value.
13
+ #
14
+ # @author {https://aaronmallen.me Aaron Allen}
15
+ # @since 0.1.0
16
+ class Callback
17
+ # @rbs!
18
+ # type handler = ^(untyped old_value, untyped new_value) -> void | Proc
19
+
20
+ # @rbs @handlers: Array[handler]
21
+
22
+ include BelongsToAttribute
23
+
24
+ # Initialize a new Callback instance.
25
+ #
26
+ # @param attribute [Attribute] the attribute this Callback belongs to
27
+ # @param handlers [Array<Proc>] the handlers to use for processing
28
+ #
29
+ # @return [Callback] the new instance of Callback
30
+ # @rbs (Attribute attribute, Array[handler] | handler handlers) -> void
31
+ def initialize(attribute, handlers = [])
32
+ super
33
+ @handlers = [*handlers].map do |handler|
34
+ validate_handler!(handler)
35
+ handler
36
+ end.uniq
37
+ end
38
+
39
+ # Execute all callbacks for a value change.
40
+ #
41
+ # @param instance [Object] the instance on which to execute callbacks
42
+ # @param old_value [Object] the previous value
43
+ # @param new_value [Object] the new value
44
+ #
45
+ # @return [void]
46
+ # @rbs (untyped instance, untyped old_value, untyped new_value) -> void
47
+ def call(instance, old_value, new_value)
48
+ @handlers.each { |handler| instance.instance_exec(old_value, new_value, &handler) }
49
+ end
50
+
51
+ private
52
+
53
+ # Validate that a callback handler is a valid Proc.
54
+ #
55
+ # @param handler [Object] the handler to validate
56
+ #
57
+ # @raise [TypeError] if the handler is not a valid Proc
58
+ # @return [void]
59
+ # @rbs (handler handler) -> void
60
+ def validate_handler!(handler)
61
+ return if handler.is_a?(Proc)
62
+
63
+ raise TypeError, "`#{attribute_method_name}`: invalid handler: #{handler.inspect}. Must be a Proc."
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer/attribute/mixin/belongs_to_attribute'
4
+
5
+ module Domainic
6
+ module Attributer
7
+ class Attribute
8
+ # A class responsible for coercing attribute values.
9
+ #
10
+ # This class manages the coercion of values assigned to an attribute. Coercion can be
11
+ # handled by either a Proc that accepts a single value argument, or by referencing an
12
+ # instance method via Symbol.
13
+ #
14
+ # @author {https://aaronmallen.me Aaron Allen}
15
+ # @since 0.1.0
16
+ class Coercer
17
+ # @rbs!
18
+ # type handler = proc | Proc | Symbol
19
+ #
20
+ # type proc = ^(untyped value) -> untyped
21
+
22
+ include BelongsToAttribute
23
+
24
+ # @rbs @handlers: Array[handler]
25
+
26
+ # Initialize a new Coercer instance.
27
+ #
28
+ # @param attribute [Attribute] the attribute this Coercer belongs to
29
+ # @param handlers [Array<Proc, Symbol>] the handlers to use for processing
30
+ #
31
+ # @return [Coercer] the new instance of Coercer
32
+ # @rbs (Attribute attribute, Array[handler] | handler handlers) -> void
33
+ def initialize(attribute, handlers = [])
34
+ super
35
+ @handlers = [*handlers].map do |handler|
36
+ validate_handler!(handler)
37
+ handler
38
+ end.uniq
39
+ end
40
+
41
+ # Process a value through all coercion handlers.
42
+ #
43
+ # @param instance [Object] the instance on which to perform coercion
44
+ # @param value [Object] the value to coerce
45
+ #
46
+ # @return [Object] the coerced value
47
+ # @rbs (untyped instance, untyped value) -> untyped
48
+ def call(instance, value)
49
+ @handlers.reduce(value) { |accumulator, handler| coerce_value(instance, handler, accumulator) }
50
+ end
51
+
52
+ private
53
+
54
+ # Process a value through a single coercion handler.
55
+ #
56
+ # @param instance [Object] the instance on which to perform coercion
57
+ # @param handler [Proc, Symbol] the coercion handler
58
+ # @param value [Object] the value to coerce
59
+ #
60
+ # @raise [TypeError] if the handler is invalid
61
+ # @return [Object] the coerced value
62
+ # @rbs (untyped instance, handler, untyped value) -> untyped
63
+ def coerce_value(instance, handler, value)
64
+ case handler
65
+ when Proc
66
+ instance.instance_exec(value, &handler)
67
+ when Symbol
68
+ instance.send(handler, value)
69
+ else
70
+ # We should never get here because we validate the handlers in the initializer.
71
+ raise TypeError, "`#{attribute_method_name}`: invalid coercer: #{handler}. "
72
+ end
73
+ end
74
+
75
+ # Validate that a coercion handler is valid.
76
+ #
77
+ # @param handler [Object] the handler to validate
78
+ #
79
+ # @raise [TypeError] if the handler is not valid
80
+ # @return [void]
81
+ # @rbs (handler handler) -> void
82
+ def validate_handler!(handler)
83
+ return if handler.is_a?(Proc)
84
+ return if handler.is_a?(Symbol) &&
85
+ (@attribute.base.method_defined?(handler) || @attribute.base.private_method_defined?(handler))
86
+
87
+ raise TypeError, "`#{attribute_method_name}`: invalid coercer: #{handler.inspect}. Must be a Proc " \
88
+ 'or a Symbol referencing a method.'
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domainic
4
+ module Attributer
5
+ class Attribute
6
+ # A mixin providing common functionality for classes that belong to an Attribute.
7
+ #
8
+ # This module provides initialization and duplication behavior for classes that are owned
9
+ # by and work in conjunction with an Attribute instance. These classes typically handle
10
+ # specific aspects of attribute processing such as coercion, validation, or callbacks.
11
+ #
12
+ # @author {https://aaronmallen.me Aaron Allen}
13
+ # @since 0.1.0
14
+ module BelongsToAttribute
15
+ # @rbs @attribute: Attribute
16
+
17
+ # Initialize a new instance that belongs to an attribute.
18
+ #
19
+ # @param attribute [Attribute] the attribute this instance belongs to
20
+ #
21
+ # @return [void]
22
+ # @rbs (Attribute attribute, *untyped, **untyped) -> void
23
+ def initialize(attribute, ...)
24
+ validate_attribute!(attribute)
25
+ @attribute = attribute
26
+ end
27
+
28
+ # Create a duplicate instance associated with a new attribute.
29
+ #
30
+ # @param new_attribute [Attribute] the new attribute to associate with
31
+ #
32
+ # @return [BelongsToAttribute] a duplicate instance
33
+ # @rbs (Attribute attribute) -> BelongsToAttribute
34
+ def dup_with_attribute(new_attribute)
35
+ validate_attribute!(new_attribute)
36
+
37
+ dup.tap { |duped| duped.instance_variable_set(:@attribute, new_attribute) }
38
+ end
39
+
40
+ private
41
+
42
+ # Generate a method name for error messages.
43
+ #
44
+ # @return [String] the formatted method name
45
+ # @rbs () -> String
46
+ def attribute_method_name
47
+ "#{@attribute.base}##{@attribute.name}"
48
+ end
49
+
50
+ # Ensure that an attribute is a valid {Attribute} instance.
51
+ #
52
+ # @param attribute [Attribute] the attribute to validate
53
+ #
54
+ # @raise [TypeError] if the attribute is not a valid {Attribute} instance
55
+ # @return [void]
56
+ # @rbs (Attribute attribute) -> void
57
+ def validate_attribute!(attribute)
58
+ return if attribute.is_a?(Attribute)
59
+ return if defined?(RSpec::Mocks::TestDouble) && attribute.is_a?(RSpec::Mocks::TestDouble)
60
+
61
+ raise TypeError,
62
+ "invalid attribute: #{attribute.inspect}. Must be an Domainic::Attributer::Attribute instance"
63
+ end
64
+ end
65
+ private_constant :BelongsToAttribute
66
+ end
67
+ end
68
+ end