domainic-attributer 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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).