artisanal-model 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +17 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +333 -0
- data/Rakefile +6 -0
- data/artisanal-model.gemspec +36 -0
- data/benchmarks/with_virtus_and_hashie.rb +107 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/artisanal/model/attribute.rb +86 -0
- data/lib/artisanal/model/builder.rb +40 -0
- data/lib/artisanal/model/config.rb +17 -0
- data/lib/artisanal/model/dsl.rb +43 -0
- data/lib/artisanal/model/initializer.rb +7 -0
- data/lib/artisanal/model/model.rb +93 -0
- data/lib/artisanal/model/version.rb +5 -0
- data/lib/artisanal/model.rb +15 -0
- data/lib/artisanal-model.rb +1 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 74c99f3a53173417323c014e30722f54063402e2ed9a2e1e615131ff8ed02402
|
4
|
+
data.tar.gz: 33422ea03f1cdedad2ab332e4874525a367d9a344743cdd65378ad2f425b56a7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 713b969405ef5bc54e0604e6f99fbca0e154051836588e6208a5fb9e283eab7cf38825f321e4cb4dcbf184bfe948a7acbf3a33fead7270d58b604c46e43badef
|
7
|
+
data.tar.gz: 3a606d437f1db3d241c3530b08e1be9d461525dea970861a2dc5d7fde4db5970a836c0019944a498d0b0728f7d70d0dbe0bb335b634705b97cc7a1205d002721
|
@@ -0,0 +1,17 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
orbs:
|
4
|
+
gem: goldstar/publish-gem@1.0.1
|
5
|
+
|
6
|
+
workflows:
|
7
|
+
main:
|
8
|
+
jobs:
|
9
|
+
- gem/test
|
10
|
+
- gem/build-and-deploy:
|
11
|
+
context: packagecloud
|
12
|
+
requires:
|
13
|
+
- gem/test
|
14
|
+
filters:
|
15
|
+
branches:
|
16
|
+
only: master
|
17
|
+
packagecloud-repo: goldstar/production
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in artisanal-model.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
group :benchmarks do
|
9
|
+
if RUBY_VERSION < "2.4"
|
10
|
+
gem "activesupport", "< 5"
|
11
|
+
else
|
12
|
+
gem "activesupport"
|
13
|
+
end
|
14
|
+
|
15
|
+
gem "benchmark-ips", "~> 2.5"
|
16
|
+
gem "dry-initializer"
|
17
|
+
gem "hashie"
|
18
|
+
gem "virtus"
|
19
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Jared Hoyt
|
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,333 @@
|
|
1
|
+
# artisanal-model
|
2
|
+
|
3
|
+
Artisanal::Model is a light-weight attribute modeling DSL that wraps [dry-initializer](https://dry-rb.org/gems/dry-initializer/), providing extra configuration and a slightly cleaner DSL.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'artisanal-model'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install artisanal-model
|
20
|
+
|
21
|
+
## Configuration
|
22
|
+
|
23
|
+
Artisanal::Model configuration is done on a per-model basis. There is no global configuration (at this time):
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class Model
|
27
|
+
include Artisanal::Model(writable: true)
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
The configuration will be carried down to subclasses automatically. However, and subclass can override any settings with the `configure` method:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Person < Model
|
35
|
+
artisanal_model.configure { |config| config.writable = false }
|
36
|
+
# ... or
|
37
|
+
artisanal_model.config.writable = false
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
#### `defaults(hash)`
|
42
|
+
|
43
|
+
The `defaults` setting allows you to provide default values to the `attribute` dsl method. For example, if you would like all attributes to be optional and private:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class Model
|
47
|
+
include Artisanal::Model(defaults: { optional: true, reader: :private })
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
See the [dry-initializer](https://dry-rb.org/gems/dry-initializer/) documentation for a list of most of the options that can be passed to the `attribute` method.
|
52
|
+
|
53
|
+
#### `writable(boolean, default: false)`
|
54
|
+
|
55
|
+
Setting `writable` to true will enable the mass-assignment `#assign_attributes` method as well as add `writer: true` to the `defaults` configuration option.
|
56
|
+
|
57
|
+
You can also manually add `writer: true` to `defaults` without setting `writable` to true. This would give you attribute writers but skip creating the mass-assignment method.
|
58
|
+
|
59
|
+
#### `undefined(boolean, default: false)`
|
60
|
+
|
61
|
+
Setting `undefined` to true will configure dry-initializer to differentiate between `nil` values and undefineds. It will also automatically filter out undefined values when serializing your model to a hash.
|
62
|
+
|
63
|
+
See dry-initializer [Skip Undefined](https://dry-rb.org/gems/dry-initializer/skip-undefined/) documentation for more information.
|
64
|
+
|
65
|
+
#### `symbolize(boolean, default: false)`
|
66
|
+
|
67
|
+
Setting `symbolize` to true will make artisanal-model intern the keys of any attributes passed in during initialization and mass-assignment. Only attributes belonging to the model will be symbolized; all
|
68
|
+
other keys will be left as strings.
|
69
|
+
|
70
|
+
See the [integration test](blob/master/spec/artisanal/integration/stringified_arguments_spec.rb) for more details.
|
71
|
+
See the [benchmarks](#benchmarks) for the performance impact of "indifferent access".
|
72
|
+
|
73
|
+
## Examples
|
74
|
+
|
75
|
+
For the following examples, consider the following `Model` class with some default configuration.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class Model
|
79
|
+
include Artisanal::Model(
|
80
|
+
defaults: { optional: true, type: Dry::Types::Any }
|
81
|
+
)
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
You can define attributes on your models using Artisanal::Model's `attribute` dsl method:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class Person < Model
|
89
|
+
attribute :first_name
|
90
|
+
attribute :last_name
|
91
|
+
attribute :email
|
92
|
+
end
|
93
|
+
|
94
|
+
Person.new(first_name: 'John', last_name: 'Smith', email: 'john@example.com').tap do |person|
|
95
|
+
person.first_name #=> 'John'
|
96
|
+
person.email #=> 'john@example.com'
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
Also, the keys passed into the initializer do not need to be symbolized ahead of time. Artisanal::Model will take care of that for you before passing them into dry-initializer:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
Person.new('first_name' => 'John').tap do |person|
|
104
|
+
person.first_name #=> 'John'
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
### dry-initializer
|
109
|
+
|
110
|
+
For the most part, the parameters available to the `attribute` method are the same ones available to dry-initializer's [option method](https://dry-rb.org/gems/dry-initializer/params-and-options/). These allow you to do things like coerce values, rename incoming fields, set defaults, define required fields, and set method access control.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class Person < Model
|
114
|
+
attribute :first_name, default: -> { 'Bob' }
|
115
|
+
attribute :last_name, optional: false
|
116
|
+
attribute :email, from: :email_address
|
117
|
+
attribute :phone, reader: :private
|
118
|
+
attribute :age, ->(value, person) { value.to_i }
|
119
|
+
end
|
120
|
+
|
121
|
+
attrs = {
|
122
|
+
last_name: 'Smith',
|
123
|
+
email_address: 'john@example.com',
|
124
|
+
phone: '555.123.4567',
|
125
|
+
age: '37'
|
126
|
+
}
|
127
|
+
|
128
|
+
Person.new(attrs).tap do |person|
|
129
|
+
person.first_name #=> 'Bob'
|
130
|
+
person.email #=> 'john@example.com'
|
131
|
+
person.phone #=> NoMethodError: private method `phone' called for...
|
132
|
+
person.age #=> 37
|
133
|
+
end
|
134
|
+
|
135
|
+
Person.new(first_name: 'Steve')
|
136
|
+
#=> KeyError: Person: option 'last_name' is required
|
137
|
+
```
|
138
|
+
|
139
|
+
### aliased fields
|
140
|
+
|
141
|
+
The dry-initializer gem already lets you use the `:as` option to give your field a new name. To make this a little more straightforward, artisanal-model adds a `:from` option that is the inverse of `:as`:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class Person < Model
|
145
|
+
attribute :email_address, as: :email
|
146
|
+
# is the same as ...
|
147
|
+
attribute :email, from: :email_address
|
148
|
+
end
|
149
|
+
|
150
|
+
Person.new(email_address: 'john@example.com').email #=> 'john@example.com'
|
151
|
+
```
|
152
|
+
|
153
|
+
### coercions
|
154
|
+
|
155
|
+
In addition to the functionality dry-initializer provides, Artisanal::Model also adds some niceties that make the dsl a little less verbose. For example, coercions in dry-initializer are required to be a callable type (e.g. a proc or a [dry-type](https://dry-rb.org/gems/dry-types/)).
|
156
|
+
|
157
|
+
However, Artisanal::Model will allow you to specify a class or an array and will wrap the type coercion with a proc in the background:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
class Address < Model
|
161
|
+
attribute :street
|
162
|
+
attribute :city
|
163
|
+
attribute :state
|
164
|
+
attribute :zip
|
165
|
+
end
|
166
|
+
|
167
|
+
class Tag < Model
|
168
|
+
attribute :name
|
169
|
+
end
|
170
|
+
|
171
|
+
class Person < Model
|
172
|
+
attribute :name
|
173
|
+
attribute :address, Address
|
174
|
+
attribute :tags, Array[Tag]
|
175
|
+
attribute :emails, Set[Dry::Types['string']]
|
176
|
+
end
|
177
|
+
|
178
|
+
attrs = {
|
179
|
+
name: 'John Smith',
|
180
|
+
address: {
|
181
|
+
street: '123 Main St.',
|
182
|
+
city: 'Portland',
|
183
|
+
state: 'OR',
|
184
|
+
zip: '97213'
|
185
|
+
},
|
186
|
+
tags: [
|
187
|
+
{ name: 'Ruby' },
|
188
|
+
{ name: 'Developer' }
|
189
|
+
],
|
190
|
+
email: ['john@example.com', 'jsmith@example.com']
|
191
|
+
}
|
192
|
+
|
193
|
+
Person.new(attrs).tap do |person|
|
194
|
+
person.name #=> 'John Smith'
|
195
|
+
|
196
|
+
person.address.street #=> '123 Main St.'
|
197
|
+
person.address.zip #=> '97213'
|
198
|
+
|
199
|
+
person.tags.count #=> 2
|
200
|
+
person.tags.first.name #=> 'Ruby'
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
### writers
|
205
|
+
|
206
|
+
Artisanal::Model can also add writer methods that aren't provided from dry-initializer:
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
# Model.include Artisanal::Model(writable: true, ...)
|
210
|
+
|
211
|
+
class Person < Model
|
212
|
+
attribute :name
|
213
|
+
attribute :email, writer: false
|
214
|
+
attribute :phone, writer: :protected # the same as adding `protected :phone`
|
215
|
+
attribute :age, writer: :private # the same as adding `private :age`
|
216
|
+
end
|
217
|
+
|
218
|
+
attrs = {
|
219
|
+
name: 'John',
|
220
|
+
email: 'john@example.com',
|
221
|
+
phone: '555.123.4567',
|
222
|
+
age: '37'
|
223
|
+
}
|
224
|
+
|
225
|
+
Person.new(attrs).tap do |person|
|
226
|
+
person.name = 'Bob'
|
227
|
+
person.name #=> 'Bob'
|
228
|
+
|
229
|
+
person.email = 'bob@example.com' # => NoMethodError: undefined method `email' called for ...
|
230
|
+
person.phone = '555.987.6543' # => NoMethodError: protected method `phone' called for ...
|
231
|
+
person.age = '21' # => NoMethodError: private method `age' called for ...
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
Notice that any other value except for `false`, `:protected` and `:private` provides a public writer.
|
236
|
+
|
237
|
+
With `writable` enabled, models will also have a `assign_attributes` method to do attribute mass-assignment:
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
class Person < Model
|
241
|
+
attribute :name
|
242
|
+
attribute :email
|
243
|
+
attribute :age
|
244
|
+
end
|
245
|
+
|
246
|
+
Person.new(name: 'John Smith', email: 'john@example.com', age: '37').tap do |person|
|
247
|
+
person.name #=> 'John Smith'
|
248
|
+
|
249
|
+
person.assign_attributes(name: 'Bob Johnson', email: 'bob@example.com')
|
250
|
+
|
251
|
+
person.name #=> 'Bob Johnson'
|
252
|
+
person.email #=> 'bob@example.com'
|
253
|
+
person.age #=> '37'
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
### serialization
|
258
|
+
|
259
|
+
Artisanal::Models can also be converted back into hashes for storage or representation purposes. By default, the result will only include public attributes, but `to_h` will also let you request private attributes as well:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
class Person < Model
|
263
|
+
attribute :name
|
264
|
+
attribute :email
|
265
|
+
attribute :phone, reader: :private
|
266
|
+
attribute :age, reader: :protected
|
267
|
+
end
|
268
|
+
|
269
|
+
Person.new(name: 'John Smith', phone: '555.123.4567', age: '37').tap do |person|
|
270
|
+
person.to_h #=> { name: 'John Smith', email: nil }
|
271
|
+
person.to_h(scope: :private) #=> { phone: '555.123.4567' }
|
272
|
+
person.to_h(scope: [:public, :protected]) #=> { name: 'John Smith', email: nil, age: '37' }
|
273
|
+
person.to_h(scope: :all) #=> { name: 'John Smith', email: nil, phone: '555.123.4567', age: '37' }
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
### undefined attributes
|
278
|
+
|
279
|
+
Dry-initializer [differentiates](https://dry-rb.org/gems/dry-initializer/skip-undefined/) between a `nil` value passed in for an attribute and nothing passed in at all.
|
280
|
+
|
281
|
+
This can be turned off through Artisanal::Model for performance reasons if you don't care about the differences between `nil` and undefined. However, if turned on, serializing to a hash will also exclude undefined values by default:
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
# Model.include Artisanal::Model(undefined: true, ...)
|
285
|
+
|
286
|
+
class Person < Model
|
287
|
+
attribute :name
|
288
|
+
attribute :email
|
289
|
+
attribute :phone
|
290
|
+
end
|
291
|
+
|
292
|
+
Person.new(name: 'John Smith', phone: nil).tap do |person|
|
293
|
+
person.to_h #=> { name: 'John Smith', phone: nil }
|
294
|
+
person.to_h(include_undefined: true) #=> { name: 'John Smith', email: nil, phone: nil }
|
295
|
+
end
|
296
|
+
```
|
297
|
+
|
298
|
+
## Benchmarks
|
299
|
+
|
300
|
+
Comparing artisanal-model with plain ruby, dry-initializer, hashie, and virtus:
|
301
|
+
|
302
|
+
```
|
303
|
+
Calculating -------------------------------------
|
304
|
+
plain Ruby 2.493M (± 2.8%) i/s - 37.407M in 15.016557s
|
305
|
+
dry-initializer 402.247k (± 2.7%) i/s - 6.051M in 15.054567s
|
306
|
+
artisanal-model 322.343k (± 3.2%) i/s - 4.843M in 15.040670s
|
307
|
+
artisanal-model (WITH WRITERS)
|
308
|
+
329.785k (± 2.6%) i/s - 4.965M in 15.066329s
|
309
|
+
artisanal-model (WITH INDIFFERENT ACCESS)
|
310
|
+
284.767k (± 2.2%) i/s - 4.292M in 15.078616s
|
311
|
+
hashie 37.250k (± 1.8%) i/s - 559.827k in 15.034072s
|
312
|
+
virtus 136.092k (± 2.0%) i/s - 2.049M in 15.059855s
|
313
|
+
|
314
|
+
Comparison:
|
315
|
+
plain Ruby: 2492919.5 i/s
|
316
|
+
dry-initializer: 402247.4 i/s - 6.20x slower
|
317
|
+
artisanal-model (WITH WRITERS): 329785.0 i/s - 7.56x slower
|
318
|
+
artisanal-model: 322342.7 i/s - 7.73x slower
|
319
|
+
artisanal-model (WITH INDIFFERENT ACCESS): 284766.8 i/s - 8.75x slower
|
320
|
+
virtus: 136092.4 i/s - 18.32x slower
|
321
|
+
hashie: 37250.4 i/s - 66.92x slower
|
322
|
+
```
|
323
|
+
|
324
|
+
## Development
|
325
|
+
|
326
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
327
|
+
|
328
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
329
|
+
|
330
|
+
## Contributing
|
331
|
+
|
332
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/goldstar/artisanal-model.
|
333
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "artisanal/model/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "artisanal-model"
|
7
|
+
spec.version = Artisanal::Model::VERSION
|
8
|
+
spec.authors = ["Jared Hoyt, Matthew Peychich"]
|
9
|
+
spec.email = ["jaredhoyt@gmail.com, mpeychich@mac.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{A light attributes wrapper for dry-initializer}
|
12
|
+
spec.description = %q{A light attributes wrapper for dry-initializer}
|
13
|
+
spec.homepage = "https://github.com/goldstar/artisanal-model"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.required_ruby_version = ">= 2.4"
|
26
|
+
|
27
|
+
spec.add_runtime_dependency "dry-initializer", ">= 2.5.0"
|
28
|
+
|
29
|
+
spec.add_development_dependency "dry-types", ">= 0.13.3"
|
30
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
31
|
+
spec.add_development_dependency "rb-readline"
|
32
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
34
|
+
spec.add_development_dependency "rspec_junit_formatter"
|
35
|
+
spec.add_development_dependency "rspec-its", "~> 1.2"
|
36
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
Bundler.require(:benchmarks)
|
2
|
+
|
3
|
+
class PlainRubyTest
|
4
|
+
attr_reader :foo, :bar, :baz
|
5
|
+
|
6
|
+
def initialize(foo: "FOO", bar: "BAR", baz_old: "BAZ")
|
7
|
+
@foo = foo
|
8
|
+
@bar = bar
|
9
|
+
@baz = baz_old
|
10
|
+
raise TypeError unless String === @foo
|
11
|
+
raise TypeError unless String === @bar
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require "dry-initializer"
|
16
|
+
class DryTest
|
17
|
+
extend Dry::Initializer[undefined: false]
|
18
|
+
|
19
|
+
option :foo, proc(&:to_s), default: -> { "FOO" }
|
20
|
+
option :bar, proc(&:to_s), default: -> { "BAR" }
|
21
|
+
option :baz_old, proc(&:to_s), default: -> { "BAZ" }, as: :baz
|
22
|
+
end
|
23
|
+
|
24
|
+
require "artisanal-model"
|
25
|
+
class ArtisanalTest
|
26
|
+
include Artisanal::Model
|
27
|
+
|
28
|
+
attribute :foo, proc(&:to_s), default: -> { "FOO" }
|
29
|
+
attribute :bar, proc(&:to_s), default: -> { "BAR" }
|
30
|
+
attribute :baz, proc(&:to_s), default: -> { "BAR" }, from: :baz_old
|
31
|
+
end
|
32
|
+
|
33
|
+
require "artisanal-model"
|
34
|
+
class ArtisanalTestWithWriters
|
35
|
+
include Artisanal::Model(writable: true)
|
36
|
+
|
37
|
+
attribute :foo, proc(&:to_s), default: -> { "FOO" }
|
38
|
+
attribute :bar, proc(&:to_s), default: -> { "BAR" }
|
39
|
+
attribute :baz, proc(&:to_s), default: -> { "BAR" }, from: :baz_old
|
40
|
+
end
|
41
|
+
|
42
|
+
require "artisanal-model"
|
43
|
+
class ArtisanalTestWithIndifferentAccess
|
44
|
+
include Artisanal::Model(symbolize: true)
|
45
|
+
|
46
|
+
attribute :foo, proc(&:to_s), default: -> { "FOO" }
|
47
|
+
attribute :bar, proc(&:to_s), default: -> { "BAR" }
|
48
|
+
attribute :baz, proc(&:to_s), default: -> { "BAR" }, from: :baz_old
|
49
|
+
end
|
50
|
+
|
51
|
+
require "hashie"
|
52
|
+
class HashieTest < Hashie::Trash
|
53
|
+
include Hashie::Extensions::Dash::Coercion
|
54
|
+
include Hashie::Extensions::IndifferentAccess
|
55
|
+
include Hashie::Extensions::MethodAccess
|
56
|
+
|
57
|
+
property :foo, coerce: proc(&:to_s), default: "FOO"
|
58
|
+
property :bar, coerce: proc(&:to_s), default: "BAR"
|
59
|
+
property :baz, coerce: proc(&:to_s), default: "BAR", from: :baz_old
|
60
|
+
end
|
61
|
+
|
62
|
+
require "virtus"
|
63
|
+
class VirtusTest
|
64
|
+
include Virtus.model
|
65
|
+
|
66
|
+
attribute :foo, String, default: "FOO"
|
67
|
+
attribute :bar, String, default: "BAR"
|
68
|
+
attribute :baz_old, String, default: "BAR"
|
69
|
+
|
70
|
+
alias_method :baz, :baz_old
|
71
|
+
end
|
72
|
+
|
73
|
+
puts "Benchmark for example models with virtus and hashie"
|
74
|
+
|
75
|
+
Benchmark.ips do |x|
|
76
|
+
x.config time: 15, warmup: 10
|
77
|
+
|
78
|
+
x.report("plain Ruby") do
|
79
|
+
PlainRubyTest.new
|
80
|
+
end
|
81
|
+
|
82
|
+
x.report("dry-initializer") do
|
83
|
+
DryTest.new
|
84
|
+
end
|
85
|
+
|
86
|
+
x.report("artisanal-model") do
|
87
|
+
ArtisanalTest.new
|
88
|
+
end
|
89
|
+
|
90
|
+
x.report("artisanal-model (WITH WRITERS)") do
|
91
|
+
ArtisanalTestWithWriters.new
|
92
|
+
end
|
93
|
+
|
94
|
+
x.report("artisanal-model (WITH INDIFFERENT ACCESS)") do
|
95
|
+
ArtisanalTestWithIndifferentAccess.new
|
96
|
+
end
|
97
|
+
|
98
|
+
x.report("hashie") do
|
99
|
+
HashieTest.new
|
100
|
+
end
|
101
|
+
|
102
|
+
x.report("virtus") do
|
103
|
+
VirtusTest.new
|
104
|
+
end
|
105
|
+
|
106
|
+
x.compare!
|
107
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "artisanal/model"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Artisanal::Model
|
2
|
+
class Attribute < Module
|
3
|
+
attr_reader :name, :type, :options
|
4
|
+
|
5
|
+
def initialize(name, coercer=nil, **options)
|
6
|
+
@name, @options = name, options
|
7
|
+
@type = coercer || options[:type]
|
8
|
+
|
9
|
+
# Convert :from option to :as
|
10
|
+
if options.has_key? :from
|
11
|
+
@name, options[:as] = options[:from], name
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add default values for certain types
|
15
|
+
unless options.has_key?(:default)
|
16
|
+
options.merge!(default: type_default)
|
17
|
+
end
|
18
|
+
|
19
|
+
raise ArgumentError.new("type missing for attribute #{name}") if type.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def included(base)
|
23
|
+
# Create dry-initializer option
|
24
|
+
base.option(name, **options.merge(type: type_builder))
|
25
|
+
|
26
|
+
# Create writer method
|
27
|
+
define_writer(base, name) if options[:writer]
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def class_coercer(type)
|
33
|
+
if type.respond_to? :artisanal_model
|
34
|
+
->(value, parent) { value.is_a?(type) ? value : type.new(value, parent) }
|
35
|
+
else
|
36
|
+
->(value) { value.is_a?(type) ? value : type.new(value) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def define_writer(base, target)
|
41
|
+
define_method("#{target}=") do |value|
|
42
|
+
coercer = artisanal_model.schema[target].type
|
43
|
+
arity = coercer.is_a?(Proc) ? coercer.arity : coercer.method(:call).arity
|
44
|
+
args = arity.abs == 1 ? [value] : [value, self]
|
45
|
+
|
46
|
+
coercer.call(*args).tap { |result| instance_variable_set("@#{target}", result) }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Scope writer to protected or private
|
50
|
+
if [:protected, :private].include? options[:writer]
|
51
|
+
base.send(options[:writer], "#{target}=")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def enumerable_coercer(type)
|
56
|
+
coercer = type_builder(type.first)
|
57
|
+
arity = coercer.is_a?(Proc) ? coercer.arity : coercer.method(:call).arity
|
58
|
+
|
59
|
+
if arity.abs == 1
|
60
|
+
->(collection) { type.class.new(collection.map { |value| coercer.call(value) }) }
|
61
|
+
else
|
62
|
+
->(collection, parent) { type.class.new(collection.map { |value| coercer.call(value, parent) }) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def type_builder(type=self.type)
|
67
|
+
case type
|
68
|
+
when Class
|
69
|
+
class_coercer(type)
|
70
|
+
when Enumerable
|
71
|
+
enumerable_coercer(type)
|
72
|
+
else
|
73
|
+
type
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def type_default
|
78
|
+
case type
|
79
|
+
when Array
|
80
|
+
-> { Array.new }
|
81
|
+
when Set
|
82
|
+
-> { Set.new }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'dry-initializer'
|
2
|
+
|
3
|
+
module Artisanal::Model
|
4
|
+
require_relative 'dsl'
|
5
|
+
|
6
|
+
class Builder < Module
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
@config = Config.new(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def included(base)
|
14
|
+
base.extend Dry::Initializer[undefined: config.undefined?]
|
15
|
+
base.extend Artisanal::Model::DSL
|
16
|
+
|
17
|
+
# Make attributes mutable
|
18
|
+
define_writers if config.writable?
|
19
|
+
|
20
|
+
# Store artisanal model config
|
21
|
+
base.artisanal_model.config = config
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def define_writers
|
27
|
+
# Add writers to all attributes
|
28
|
+
config.defaults[:writer] = true unless config.defaults.has_key? :writer
|
29
|
+
|
30
|
+
# Define mass-assignment method
|
31
|
+
define_method(:assign_attributes) do |attrs|
|
32
|
+
attrs = artisanal_model.symbolize(attrs)
|
33
|
+
|
34
|
+
(attrs.keys & artisanal_model.schema.keys).each do |key|
|
35
|
+
public_send("#{key}=", attrs[key]) if respond_to? "#{key}="
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Artisanal::Model
|
2
|
+
class Config
|
3
|
+
attr_reader :options, :defaults, :writable, :undefined, :symbolize
|
4
|
+
|
5
|
+
alias_method :writable?, :writable
|
6
|
+
alias_method :undefined?, :undefined
|
7
|
+
alias_method :symbolize?, :symbolize
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
@options = options
|
11
|
+
@defaults = { optional: true }.merge(options.fetch(:defaults, {}))
|
12
|
+
@writable = options.fetch(:writable, false)
|
13
|
+
@undefined = options.fetch(:undefined, false)
|
14
|
+
@symbolize = options.fetch(:symbolize, false)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Artisanal::Model
|
2
|
+
require_relative 'initializer'
|
3
|
+
require_relative 'model'
|
4
|
+
|
5
|
+
module DSL
|
6
|
+
def self.extended(base)
|
7
|
+
base.prepend Initializer
|
8
|
+
base.include InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
def inherited(subclass)
|
12
|
+
subclass.include Artisanal::Model(**artisanal_model.config.options)
|
13
|
+
super(subclass)
|
14
|
+
end
|
15
|
+
|
16
|
+
def artisanal_model
|
17
|
+
@artisanal_model ||= Model.new(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def schema
|
21
|
+
artisanal_model.schema
|
22
|
+
end
|
23
|
+
|
24
|
+
def attribute(*args, **kwargs)
|
25
|
+
artisanal_model.attribute(*args, **kwargs)
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
def artisanal_model
|
30
|
+
self.class.artisanal_model
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes(*args)
|
34
|
+
artisanal_model.attributes(self, *args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_h(**args)
|
38
|
+
artisanal_model.to_h(self, **args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Artisanal::Model
|
4
|
+
require_relative 'attribute'
|
5
|
+
|
6
|
+
class Model
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :klass
|
10
|
+
attr_writer :config
|
11
|
+
|
12
|
+
delegate [:dry_initializer] => :klass
|
13
|
+
delegate [:definitions, :null] => :dry_initializer
|
14
|
+
|
15
|
+
alias_method :schema, :definitions
|
16
|
+
|
17
|
+
def initialize(klass)
|
18
|
+
@klass = klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def attribute(name, type=nil, **opts)
|
22
|
+
klass.include Attribute.new(name, type, **config.defaults.merge(opts))
|
23
|
+
end
|
24
|
+
|
25
|
+
def attributes(instance, **kwargs)
|
26
|
+
scope = kwargs.fetch(:scope, :public)
|
27
|
+
include_undefined = kwargs.fetch(:include_undefined, false)
|
28
|
+
schema.values.each_with_object({}) do |item, attrs|
|
29
|
+
next unless attribute_in_scope?(instance, item.target, scope)
|
30
|
+
next unless include_undefined || attribute_defined?(instance, item.target)
|
31
|
+
|
32
|
+
attrs[item.target] = instance.send(item.target)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def config
|
37
|
+
@config ||= Config.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def symbolize(attributes={})
|
41
|
+
attributes = attributes.to_h
|
42
|
+
|
43
|
+
return attributes unless config.symbolize?
|
44
|
+
|
45
|
+
attributes.dup.tap do |attrs|
|
46
|
+
(attribute_names & attrs.keys).each do |key|
|
47
|
+
attrs[key.intern] = attrs.delete(key)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_h(instance, **args)
|
53
|
+
attributes(instance, **args).each_with_object({}) do |(key, value), result|
|
54
|
+
if value.is_a? Hash
|
55
|
+
result[key] = value
|
56
|
+
elsif value.is_a? Enumerable
|
57
|
+
result[key] = value.map { |v| v.respond_to?(:to_h) ? v.to_h(**args) : v }
|
58
|
+
elsif !value.nil? && value.respond_to?(:to_h)
|
59
|
+
result[key] = value.to_h(**args)
|
60
|
+
else
|
61
|
+
result[key] = value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def attribute_defined?(instance, name)
|
69
|
+
instance.instance_variable_get("@#{name}") != Dry::Initializer::UNDEFINED
|
70
|
+
end
|
71
|
+
|
72
|
+
def attribute_in_scope?(instance, name, scope)
|
73
|
+
scope = Array(scope)
|
74
|
+
|
75
|
+
(instance.respond_to?(name, true) && scope.include?(:all)) ||
|
76
|
+
(scope.include?(:public) && instance.public_methods.include?(name)) ||
|
77
|
+
(scope.include?(:protected) && instance.protected_methods.include?(name)) ||
|
78
|
+
(scope.include?(:private) && instance.private_methods.include?(name))
|
79
|
+
end
|
80
|
+
|
81
|
+
def attribute_names
|
82
|
+
@attribute_names ||= schema.keys.map(&:to_s)
|
83
|
+
end
|
84
|
+
|
85
|
+
def method_missing(method, *args)
|
86
|
+
if dry_initializer.respond_to?(method)
|
87
|
+
dry_initializer.send(method, *args)
|
88
|
+
else
|
89
|
+
super
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Artisanal
|
2
|
+
require_relative "model/builder"
|
3
|
+
require_relative 'model/config'
|
4
|
+
require_relative "model/version"
|
5
|
+
|
6
|
+
def self.Model(**opts)
|
7
|
+
Model::Builder.new(**opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
module Model
|
11
|
+
def self.included(base)
|
12
|
+
base.include Artisanal::Model()
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'artisanal/model'
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: artisanal-model
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jared Hoyt, Matthew Peychich
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-initializer
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.5.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.5.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-types
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.13.3
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.13.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.10'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.10'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rb-readline
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec_junit_formatter
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec-its
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.2'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.2'
|
125
|
+
description: A light attributes wrapper for dry-initializer
|
126
|
+
email:
|
127
|
+
- jaredhoyt@gmail.com, mpeychich@mac.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".circleci/config.yml"
|
133
|
+
- ".gitignore"
|
134
|
+
- ".rspec"
|
135
|
+
- ".travis.yml"
|
136
|
+
- Gemfile
|
137
|
+
- LICENSE.txt
|
138
|
+
- README.md
|
139
|
+
- Rakefile
|
140
|
+
- artisanal-model.gemspec
|
141
|
+
- benchmarks/with_virtus_and_hashie.rb
|
142
|
+
- bin/console
|
143
|
+
- bin/setup
|
144
|
+
- lib/artisanal-model.rb
|
145
|
+
- lib/artisanal/model.rb
|
146
|
+
- lib/artisanal/model/attribute.rb
|
147
|
+
- lib/artisanal/model/builder.rb
|
148
|
+
- lib/artisanal/model/config.rb
|
149
|
+
- lib/artisanal/model/dsl.rb
|
150
|
+
- lib/artisanal/model/initializer.rb
|
151
|
+
- lib/artisanal/model/model.rb
|
152
|
+
- lib/artisanal/model/version.rb
|
153
|
+
homepage: https://github.com/goldstar/artisanal-model
|
154
|
+
licenses:
|
155
|
+
- MIT
|
156
|
+
metadata: {}
|
157
|
+
post_install_message:
|
158
|
+
rdoc_options: []
|
159
|
+
require_paths:
|
160
|
+
- lib
|
161
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '2.4'
|
166
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - ">="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
requirements: []
|
172
|
+
rubygems_version: 3.0.3.1
|
173
|
+
signing_key:
|
174
|
+
specification_version: 4
|
175
|
+
summary: A light attributes wrapper for dry-initializer
|
176
|
+
test_files: []
|