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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.md +32 -1
  4. data/README.md +42 -355
  5. data/docs/USAGE.md +723 -0
  6. data/lib/domainic/attributer/attribute/callback.rb +21 -9
  7. data/lib/domainic/attributer/attribute/coercer.rb +28 -13
  8. data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +16 -13
  9. data/lib/domainic/attributer/attribute/signature.rb +43 -32
  10. data/lib/domainic/attributer/attribute/validator.rb +46 -16
  11. data/lib/domainic/attributer/attribute.rb +28 -18
  12. data/lib/domainic/attributer/attribute_set.rb +21 -19
  13. data/lib/domainic/attributer/class_methods.rb +136 -83
  14. data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +64 -22
  15. data/lib/domainic/attributer/dsl/attribute_builder.rb +515 -26
  16. data/lib/domainic/attributer/dsl/initializer.rb +23 -18
  17. data/lib/domainic/attributer/dsl/method_injector.rb +16 -14
  18. data/lib/domainic/attributer/errors/aggregate_error.rb +36 -0
  19. data/lib/domainic/attributer/errors/callback_execution_error.rb +30 -0
  20. data/lib/domainic/attributer/errors/coercion_execution_error.rb +37 -0
  21. data/lib/domainic/attributer/errors/error.rb +19 -0
  22. data/lib/domainic/attributer/errors/validation_execution_error.rb +30 -0
  23. data/lib/domainic/attributer/instance_methods.rb +11 -8
  24. data/lib/domainic/attributer/undefined.rb +9 -7
  25. data/lib/domainic/attributer.rb +88 -27
  26. data/sig/domainic/attributer/attribute/callback.rbs +10 -7
  27. data/sig/domainic/attributer/attribute/coercer.rbs +14 -11
  28. data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +14 -12
  29. data/sig/domainic/attributer/attribute/signature.rbs +43 -32
  30. data/sig/domainic/attributer/attribute/validator.rbs +28 -13
  31. data/sig/domainic/attributer/attribute.rbs +27 -17
  32. data/sig/domainic/attributer/attribute_set.rbs +21 -19
  33. data/sig/domainic/attributer/class_methods.rbs +133 -80
  34. data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +62 -22
  35. data/sig/domainic/attributer/dsl/attribute_builder.rbs +515 -26
  36. data/sig/domainic/attributer/dsl/initializer.rbs +21 -19
  37. data/sig/domainic/attributer/dsl/method_injector.rbs +16 -14
  38. data/sig/domainic/attributer/errors/aggregate_error.rbs +28 -0
  39. data/sig/domainic/attributer/errors/callback_execution_error.rbs +23 -0
  40. data/sig/domainic/attributer/errors/coercion_execution_error.rbs +29 -0
  41. data/sig/domainic/attributer/errors/error.rbs +17 -0
  42. data/sig/domainic/attributer/errors/validation_execution_error.rbs +23 -0
  43. data/sig/domainic/attributer/instance_methods.rbs +11 -8
  44. data/sig/domainic/attributer/undefined.rbs +5 -3
  45. data/sig/domainic/attributer.rbs +88 -27
  46. metadata +19 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a25981d1a30af4ee16bd8339c975c935eee76c0bcda186af868e28c4ae4874f8
4
- data.tar.gz: c9dd81c87a0cc108010e4463beba162f30830182c485624ba0782de971ebb93f
3
+ metadata.gz: 755badcda20958e3c9cb1855eb4c3b1fcd882b5784027a1a49a14cc92ce43f4e
4
+ data.tar.gz: 4f7ceafc636d5ec5c30612960d35446985fa538c6bcea0813226e9b23b87507d
5
5
  SHA512:
6
- metadata.gz: 97e1331498dc427a98bcaf3dfa38828d1aafe02466bcbd2ae84101542b476a92543f611eed55f842537da5224fa568c8f812f475f21fec6c4831e1704c2b456e
7
- data.tar.gz: f2a5d8b6540cc7aba2eed108a5181ea3db350dc8f3a52f30a386e94f78b0ebd9df984697ad7f92071f1bd338fa3c25619be0ff4a5e3054d52703570ba2884ea9
6
+ metadata.gz: 1b9171dd305607ca3d0a7228652cb076c5672884897675da90acdbac02843133edfc77cb998f510be3b599e2066df06c7edce0e066a7bdaa6c9c5696838c49d1
7
+ data.tar.gz: 150433dd03367419bc7a3fbd1fe24e76d06f4633e043b530f3a2212927b2e3171fc3a62cccb52f69650a73168779856db73b3efcf9604592dccbdfb9a243dae6
data/.yardopts ADDED
@@ -0,0 +1,11 @@
1
+ --title Domainic::Attributer
2
+ --readme README.md
3
+ --no-private
4
+ --protected
5
+ --markup markdown
6
+ --markup-provider redcarpet
7
+ --embed-mixins
8
+ --tag rbs
9
+ --hide-tag rbs
10
+ --files CHANGELOG.md,LICENSE,docs/USAGE.md
11
+ 'lib/**/*.rb'
data/CHANGELOG.md CHANGED
@@ -6,9 +6,40 @@ The format is based on [Keep a Changelog], and this project adheres to [Break Ve
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [v0.2.0] - 2025-01-01
10
+
11
+ ### Added
12
+
13
+ * [#22](https://github.com/domainic/domainic/pull/22) Specialized error classes (`ValidationExecutionError`,
14
+ `CallbackExecutionError`, and`CoercionExecutionError`) to provide clear error reporting for validation, callback,
15
+ and coercion failures.
16
+
17
+ ### Changed
18
+
19
+ * [#18](https://github.com/domainic/domainic/pull/18) `Domainic::Attributer::Attribute::Coercer#call` will no longer
20
+ attempt to coerce nil values when the attribute is not nilable. While small this is technically a breaking change.
21
+ * [#169](https://github.com/domainic/domainic/pull/169) removed implicit dependency on RSpec from
22
+ `Domainic::Attributer::Attribute::BelongsToAttribute`
23
+
24
+ ### Fixed
25
+
26
+ * [#18](https://github.com/domainic/domainic/pull/18) Fixed missing requires for `Domainic::Attributer::Undefined` in
27
+ the `Domainic::Attributer::Attribute` and `Domainic::Attributer::Attribute::Validator` classes.
28
+ * [#94](https://github.com/domainic/domainic/pull/94) Fixed missing requires for `Domainic::Attributer::Undefined` in
29
+ `Domainic::Attributer::DSL::OptionParser`, and `Domainic::Attributer::DSL::Initializer`
30
+ * [#94](https://github.com/domainic/domainic/pull/94) Fixed missing require for `Domainic::Attributer::Attribute` in
31
+ `Domainic::Attributer::Attribute::BelongsToAttribute`
32
+ * Various documentation improvements and corrections.
33
+
34
+ ## [v0.1.0] - 2024-12-12
35
+
36
+ * Initial release
37
+
9
38
  [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
10
39
  [Break Versioning]: https://www.taoensso.com/break-versioning
11
40
 
12
41
  <!-- versions -->
13
42
 
14
- [Unreleased]: https://github.com/domainic/domainic/tree/main/domainic-attributer
43
+ [Unreleased]: https://github.com/domainic/domainic/compare/domainic-attributer-v0.2.0...HEAD
44
+ [v0.2.0]: https://github.com/domainic/domainic/compare/domainic-attributer-v0.1.0...domainic-attributer-v0.2.0
45
+ [v0.1.0]: https://github.com/domainic/domainic/compare/53f3e992ab0e3f0092fd842c4cf89c22e41afa8a...domainic-attributer-v0.1.0
data/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Domainic::Attributer
2
2
 
3
- [![Domainic::Attributer Version](https://badge.fury.io/rb/domainic-attributer.svg)](https://rubygems.org/gems/domainic-attributer)
3
+ [![Domainic::Attributer Version](https://img.shields.io/gem/v/domainic-attributer?style=for-the-badge&logo=rubygems&logoColor=white&logoSize=auto&label=Gem%20Version)](https://rubygems.org/gems/domainic-attributer)
4
+ [![Domainic::Attributer License](https://img.shields.io/github/license/domainic/domainic?style=for-the-badge&logo=opensourceinitiative&logoColor=white&logoSize=auto)](./LICENSE)
5
+ [![Domainic::Attributer Docs](https://img.shields.io/badge/rubydoc-blue?style=for-the-badge&logo=readthedocs&logoColor=white&logoSize=auto&label=docs)](https://rubydoc.info/gems/domainic-attributer/0.1.0)
6
+ [![Domainic::Attributer Open Issues](https://img.shields.io/github/issues-search/domainic/domainic?query=state%3Aopen%20label%3Adomainic-attributer&style=for-the-badge&logo=github&logoColor=white&logoSize=auto&label=issues&color=red)](https://github.com/domainic/domainic/issues?q=state%3Aopen%20label%3Adomainic-attributer%20)
4
7
 
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
+ Domainic::Attributer is a powerful toolkit that brings clarity and safety to your Ruby class attributes.
9
+ Ever wished your class attributes could:
8
10
 
9
11
  * Validate themselves to ensure they only accept correct values?
10
12
  * Transform input data automatically into the right format?
@@ -13,384 +15,69 @@ type-safe, and well-behaved. Ever wished your class attributes could:
13
15
  * Tell you when they change?
14
16
  * Distinguish between required arguments and optional settings?
15
17
 
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.
18
+ That's exactly what Domainic::Attributer does! It provides a declarative way to define and manage attributes
19
+ in your Ruby classes, ensuring data integrity and clear interfaces. It's particularly valuable for:
20
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!
21
+ * Domain models and value objects
22
+ * Service objects and command patterns
23
+ * Configuration objects
24
+ * Any class where attribute behavior matters
23
25
 
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
26
+ Think of it as giving your attributes a brain - they know what they want, how they should behave, and
27
+ they're not afraid to speak up when something's not right!
39
28
 
40
- ### Basic Attributes
41
-
42
- Getting started with Domainic::Attributer is as easy as including the module and declaring your attributes:
29
+ ## Quick Start
43
30
 
44
31
  ```ruby
45
- class Person
32
+ class SuperDev
46
33
  include Domainic::Attributer
47
34
 
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.
35
+ argument :code_name, String
36
+ option :power_level, Integer, default: 9000
118
37
 
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
38
+ option :favorite_gem do
39
+ validate_with ->(val) { val.to_s.end_with?('ruby') }
40
+ coerce_with ->(val) { val.to_s.downcase }
162
41
  non_nilable
163
42
  end
164
-
165
- option :nickname, String do
166
- required
167
- end
168
43
  end
169
44
 
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
45
+ dev = SuperDev.new('RubyNinja', favorite_gem: 'RAILS_RUBY')
46
+ dev.favorite_gem # => "rails_ruby"
47
+ dev.power_level = 9001
48
+ dev.power_level = 'over 9000' # Raises ArgumentError: invalid value for Integer
177
49
  ```
178
50
 
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
51
+ ## Installation
348
52
 
349
- Don't like `argument` and `option`? Create your own interface:
53
+ Add this line to your application's Gemfile:
350
54
 
351
55
  ```ruby
352
- class Configuration
353
- include Domainic.Attributer(argument: :param, option: :setting)
354
-
355
- param :environment
356
- setting :debug_mode, default: false
357
- end
56
+ gem 'domainic-attributer'
358
57
  ```
359
58
 
360
- or turn off one of the methods entirely:
361
-
362
- ```ruby
363
- class Configuration
364
- include Domainic.Attributer(argument: nil)
59
+ Or install it yourself as:
365
60
 
366
- option :environment
367
- end
61
+ ```bash
62
+ gem install domainic-attributer
368
63
  ```
369
64
 
370
- ### Serialization
371
-
372
- Convert your objects to hashes easily:
65
+ ## Documentation
373
66
 
374
- ```ruby
375
- class Product
376
- include Domainic::Attributer
67
+ For detailed usage instructions and examples, see [USAGE.md](./docs/USAGE.md).
377
68
 
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
69
+ ## Contributing
385
70
 
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
- ```
71
+ We welcome contributions! Please see our
72
+ [Contributing Guidelines](https://github.com/domainic/domainic/wiki/CONTRIBUTING) for:
389
73
 
390
- ## Contributing
74
+ * Development setup and workflow
75
+ * Code style and documentation standards
76
+ * Testing requirements
77
+ * Pull request process
391
78
 
392
- Bug reports and pull requests are welcome on GitHub.
79
+ Before contributing, please review our [Code of Conduct](https://github.com/domainic/domainic/wiki/CODE_OF_CONDUCT).
393
80
 
394
81
  ## License
395
82
 
396
- The gem is available as open source under the terms of the [MIT License](LICENSE).
83
+ The gem is available as open source under the terms of the [MIT License](./LICENSE).