domainic-attributer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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