dry-initializer 3.0.1 → 3.0.2
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 +4 -4
- data/.codeclimate.yml +10 -21
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/custom_ci.yml +74 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.gitignore +2 -0
- data/.rspec +2 -2
- data/.rubocop.yml +65 -27
- data/CHANGELOG.md +10 -3
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +26 -17
- data/LICENSE +20 -0
- data/README.md +11 -12
- data/docsite/source/attributes.html.md +106 -0
- data/docsite/source/container-version.html.md +39 -0
- data/docsite/source/index.html.md +43 -0
- data/docsite/source/inheritance.html.md +43 -0
- data/docsite/source/optionals-and-defaults.html.md +130 -0
- data/docsite/source/options-tolerance.html.md +27 -0
- data/docsite/source/params-and-options.html.md +74 -0
- data/docsite/source/rails-support.html.md +101 -0
- data/docsite/source/readers.html.md +43 -0
- data/docsite/source/skip-undefined.html.md +59 -0
- data/docsite/source/type-constraints.html.md +160 -0
- data/dry-initializer.gemspec +1 -1
- data/lib/dry/initializer/config.rb +5 -5
- data/lib/dry/initializer/dispatchers.rb +2 -2
- data/lib/dry/initializer/mixin/root.rb +1 -0
- data/lib/dry/initializer/struct.rb +2 -3
- data/spec/nested_type_spec.rb +4 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/type_argument_spec.rb +2 -2
- data/spec/type_constraint_spec.rb +1 -1
- data/spec/value_coercion_via_dry_types_spec.rb +1 -1
- metadata +24 -4
- data/.travis.yml +0 -28
@@ -0,0 +1,27 @@
|
|
1
|
+
---
|
2
|
+
title: Tolerance to Unknown Options
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-initializer
|
5
|
+
---
|
6
|
+
|
7
|
+
By default the initializer is strict for params (positional arguments), expecting them to be defined explicitly.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'dry-initializer'
|
11
|
+
|
12
|
+
class User
|
13
|
+
extend Dry::Initializer
|
14
|
+
end
|
15
|
+
|
16
|
+
user = User.new 'Joe' # raises ArgumentError
|
17
|
+
```
|
18
|
+
|
19
|
+
At the same time it is tolerant to unknown options. All unknown options are accepted, but ignored:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
# It accepts undefined options...
|
23
|
+
user = User.new name: 'Joe'
|
24
|
+
|
25
|
+
# ...but ignores them
|
26
|
+
user.respond_to? :name # => false
|
27
|
+
```
|
@@ -0,0 +1,74 @@
|
|
1
|
+
---
|
2
|
+
title: Params and Options
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-initializer
|
5
|
+
---
|
6
|
+
|
7
|
+
Use `param` to define plain argument:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'dry-initializer'
|
11
|
+
|
12
|
+
class User
|
13
|
+
extend Dry::Initializer
|
14
|
+
|
15
|
+
param :name
|
16
|
+
param :email
|
17
|
+
end
|
18
|
+
|
19
|
+
user = User.new 'Andrew', 'andrew@email.com'
|
20
|
+
user.name # => 'Andrew'
|
21
|
+
user.email # => 'andrew@email.com'
|
22
|
+
```
|
23
|
+
|
24
|
+
Use `option` to define named (hash) argument:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
require 'dry-initializer'
|
28
|
+
|
29
|
+
class User
|
30
|
+
extend Dry::Initializer
|
31
|
+
|
32
|
+
option :name
|
33
|
+
option :email
|
34
|
+
end
|
35
|
+
|
36
|
+
user = User.new email: 'andrew@email.com', name: 'Andrew'
|
37
|
+
user.name # => 'Andrew'
|
38
|
+
user.email # => 'andrew@email.com'
|
39
|
+
```
|
40
|
+
|
41
|
+
Options can be renamed using `:as` key:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
require 'dry-initializer'
|
45
|
+
|
46
|
+
class User
|
47
|
+
extend Dry::Initializer
|
48
|
+
|
49
|
+
option :name, as: :username
|
50
|
+
end
|
51
|
+
|
52
|
+
user = User.new name: "Joe"
|
53
|
+
user.username # => "Joe"
|
54
|
+
user.instance_variable_get :@username # => "Joe"
|
55
|
+
user.instance_variable_get :@name # => nil
|
56
|
+
user.respond_to? :name # => false
|
57
|
+
```
|
58
|
+
|
59
|
+
You can also define several ways of initializing the same argument via different options:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
require 'dry-initializer'
|
63
|
+
|
64
|
+
class User
|
65
|
+
extend Dry::Initializer
|
66
|
+
|
67
|
+
option :phone
|
68
|
+
option :telephone, as: :phone
|
69
|
+
option :name, optional: true
|
70
|
+
end
|
71
|
+
|
72
|
+
User.new(phone: '1234567890').phone # => '1234567890'
|
73
|
+
User.new(telephone: '1234567890').phone # => '1234567890'
|
74
|
+
```
|
@@ -0,0 +1,101 @@
|
|
1
|
+
---
|
2
|
+
title: Rails Support
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-initializer
|
5
|
+
---
|
6
|
+
|
7
|
+
Rails plugin is implemented in a separate [dry-initializer-rails](https://github.com/nepalez/dry-initializer-rails) gem.
|
8
|
+
|
9
|
+
It provides coercion of assigned values to corresponding ActiveRecord instances.
|
10
|
+
|
11
|
+
### Base Example
|
12
|
+
|
13
|
+
Add the `:model` setting to `param` or `option`:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
require 'dry-initializer-rails'
|
17
|
+
|
18
|
+
class CreateOrder
|
19
|
+
extend Dry::Initializer
|
20
|
+
|
21
|
+
# Params and options
|
22
|
+
param :customer, model: 'Customer' # use either a name
|
23
|
+
option :product, model: Product # or a class
|
24
|
+
|
25
|
+
def call
|
26
|
+
Order.create customer: customer, product: product
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
Now you can assign values as pre-initialized model instances:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
customer = Customer.find(1)
|
35
|
+
product = Product.find(2)
|
36
|
+
|
37
|
+
order = CreateOrder.new(customer, product: product).call
|
38
|
+
order.customer # => <Customer @id=1 ...>
|
39
|
+
order.product # => <Product @id=2 ...>
|
40
|
+
```
|
41
|
+
|
42
|
+
...or their ids:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
order = CreateOrder.new(1, product: 2).call
|
46
|
+
order.customer # => <Customer @id=1 ...>
|
47
|
+
order.product # => <Product @id=2 ...>
|
48
|
+
```
|
49
|
+
|
50
|
+
The instance is envoked using method `find_by(id: ...)`.
|
51
|
+
With wrong ids `nil` values are assigned to corresponding params and options:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
order = CreateOrder.new(0, product: 0).call
|
55
|
+
order.customer # => nil
|
56
|
+
order.product # => nil
|
57
|
+
```
|
58
|
+
|
59
|
+
### Custom Keys
|
60
|
+
|
61
|
+
You can specify custom `key` for searching model instance:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require 'dry-initializer-rails'
|
65
|
+
|
66
|
+
class CreateOrder
|
67
|
+
extend Dry::Initializer
|
68
|
+
|
69
|
+
param :customer, model: 'User', find_by: 'name'
|
70
|
+
option :product, model: Item, find_by: :name
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
This time you can send names (not ids) to the initializer:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
order = CreateOrder.new('Andrew', product: 'the_thing_no_123').call
|
78
|
+
|
79
|
+
order.customer # => <User @name='Andrew' ...>
|
80
|
+
order.product # => <Item @name='the_thing_no_123' ...>
|
81
|
+
```
|
82
|
+
|
83
|
+
### Container Syntax
|
84
|
+
|
85
|
+
If you prefer [container syntax](docs::container-version), extend plugin inside the block:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
require 'dry-initializer-rails'
|
89
|
+
|
90
|
+
class CreateOrder
|
91
|
+
include Dry::Initializer.define -> do
|
92
|
+
# ... params/options declarations
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
### Types vs Models
|
98
|
+
|
99
|
+
[Type constraints](docs::type-constraints) are checked before the coercion.
|
100
|
+
|
101
|
+
When mixing `:type` and `:model` settings for the same param/option, you should use [sum types](/gems/dry-types/1.2/sum) that accept both model instances and their attributes.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
---
|
2
|
+
title: Readers
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-initializer
|
5
|
+
---
|
6
|
+
|
7
|
+
By default public attribute reader is defined for every param and option.
|
8
|
+
|
9
|
+
You can define private or protected reader instead:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'dry-initializer'
|
13
|
+
|
14
|
+
class User
|
15
|
+
extend Dry::Initializer
|
16
|
+
|
17
|
+
param :name, reader: :private # the same as adding `private :name`
|
18
|
+
param :email, reader: :protected # the same as adding `protected :email`
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
To skip any reader, use `reader: false`:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require 'dry-initializer'
|
26
|
+
|
27
|
+
class User
|
28
|
+
extend Dry::Initializer
|
29
|
+
|
30
|
+
param :name
|
31
|
+
param :email, reader: false
|
32
|
+
end
|
33
|
+
|
34
|
+
user = User.new 'Luke', 'luke@example.com'
|
35
|
+
user.name # => 'Luke'
|
36
|
+
|
37
|
+
user.email # => #<NoMethodError ...>
|
38
|
+
user.instance_variable_get :@email # => 'luke@example.com'
|
39
|
+
```
|
40
|
+
|
41
|
+
Notice that any other value except for `false`, `:protected` and `:private` provides a public reader.
|
42
|
+
|
43
|
+
No writers are defined. Define them using pure ruby `attr_writer` when necessary.
|
@@ -0,0 +1,59 @@
|
|
1
|
+
---
|
2
|
+
title: Skip Undefined
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-initializer
|
5
|
+
---
|
6
|
+
|
7
|
+
The initializer uses special constant `Dry::Initializer::UNDEFINED` to distinguish variables that are set to `nil` from those that are not set at all.
|
8
|
+
|
9
|
+
When no value was provided, the constant is assigned to a variable, but hidden in a reader.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'dry-initializer'
|
13
|
+
|
14
|
+
class User
|
15
|
+
extend Dry::Initializer
|
16
|
+
option :email, optional: true
|
17
|
+
end
|
18
|
+
|
19
|
+
user = User.new
|
20
|
+
|
21
|
+
user.email
|
22
|
+
# => nil
|
23
|
+
|
24
|
+
user.instance_variable_get :@email
|
25
|
+
# => Dry::Initializer::UNDEFINED
|
26
|
+
```
|
27
|
+
|
28
|
+
This gives you full control of the real state of the attributes. However, all that checks cost about >30% of instantiation time, and make attribute readers 2 times slower.
|
29
|
+
|
30
|
+
To avoid the overhead in cases you don't care about the differences between `nil` and undefined, you can use a light version of the module. Add `[undefined: false]` config to either `extend` or `include` line of code:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
extend Dry::Initializer[undefined: false]
|
34
|
+
```
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
include Dry::Initializer[undefined: false] -> do
|
38
|
+
# ...
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
This time you should expect `nil` every time no value was given to an optional attribute:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'dry-initializer'
|
46
|
+
|
47
|
+
class User
|
48
|
+
extend Dry::Initializer[undefined: false]
|
49
|
+
option :email, optional: true
|
50
|
+
end
|
51
|
+
|
52
|
+
user = User.new
|
53
|
+
|
54
|
+
user.email
|
55
|
+
# => nil
|
56
|
+
|
57
|
+
user.instance_variable_get :@email
|
58
|
+
# => nil
|
59
|
+
```
|
@@ -0,0 +1,160 @@
|
|
1
|
+
---
|
2
|
+
title: Type Constraints
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-initializer
|
5
|
+
---
|
6
|
+
|
7
|
+
## Base Syntax
|
8
|
+
|
9
|
+
Use `:type` key in a `param` or `option` declarations to add type coercer.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'dry-initializer'
|
13
|
+
|
14
|
+
class User
|
15
|
+
extend Dry::Initializer
|
16
|
+
param :name, type: proc(&:to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
user = User.new :Andrew
|
20
|
+
user.name # => "Andrew"
|
21
|
+
```
|
22
|
+
|
23
|
+
Any object that responds to `#call` with 1 argument can be used as a type. Common examples are `proc(&:to_s)` for strings, `method(:Array)` (for arrays) or `Array.method(:wrap)` in Rails, `->(v) { !!v }` (for booleans), etc.
|
24
|
+
|
25
|
+
## Dry Types as coercers
|
26
|
+
|
27
|
+
Another important example is the usage of `dry-types` as type constraints:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'dry-initializer'
|
31
|
+
require 'dry-types'
|
32
|
+
|
33
|
+
class User
|
34
|
+
extend Dry::Initializer
|
35
|
+
param :name, type: Dry::Types['strict.string']
|
36
|
+
end
|
37
|
+
|
38
|
+
user = User.new :Andrew # => #<TypeError ...>
|
39
|
+
```
|
40
|
+
|
41
|
+
## Positional Argument
|
42
|
+
|
43
|
+
Instead of `:type` option you can send a constraint/coercer as the second argument:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'dry-initializer'
|
47
|
+
require 'dry-types'
|
48
|
+
|
49
|
+
class User
|
50
|
+
extend Dry::Initializer
|
51
|
+
param :name, Dry::Types['coercible.string']
|
52
|
+
param :email, proc(&:to_s)
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
## Array Types
|
57
|
+
|
58
|
+
As mentioned above, the `:type` option takes a callable object... with one important exception.
|
59
|
+
|
60
|
+
You can use arrays for values that should be wrapped to array:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class User
|
64
|
+
extend Dry::Initializer
|
65
|
+
|
66
|
+
option :name, proc(&:to_s)
|
67
|
+
option :emails, [proc(&:to_s)]
|
68
|
+
end
|
69
|
+
|
70
|
+
user = User.new name: "joe", emails: :"joe@example.com"
|
71
|
+
user.emails # => ["joe@example.com"]
|
72
|
+
|
73
|
+
user = User.new name: "jane", emails: [:"jane@example.com", :"jane@example.org"]
|
74
|
+
user.emails # => ["jane@example.com", "jane@example.org"]
|
75
|
+
```
|
76
|
+
|
77
|
+
You can wrap the coercer into several arrays as well:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class User
|
81
|
+
extend Dry::Initializer
|
82
|
+
|
83
|
+
option :emails, [[proc(&:to_s)]]
|
84
|
+
end
|
85
|
+
|
86
|
+
user = User.new name: "joe", emails: "joe@example.com"
|
87
|
+
user.emails # => [["joe@example.com"]]
|
88
|
+
```
|
89
|
+
|
90
|
+
Eventually, you can use an empty array as a coercer. In that case we just wrap the source value(s) into array, not modifying the items:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class Article
|
94
|
+
extend Dry::Initializer
|
95
|
+
|
96
|
+
option :tags, []
|
97
|
+
end
|
98
|
+
|
99
|
+
article = Article.new(tags: 1)
|
100
|
+
article.tags # => [1]
|
101
|
+
```
|
102
|
+
|
103
|
+
## Nested Options
|
104
|
+
|
105
|
+
Sometimes you need to describe a structure with nested options. In this case you can use a block with `options` inside.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class User
|
109
|
+
extend Dry::Initializer
|
110
|
+
|
111
|
+
option :name, proc(&:to_s)
|
112
|
+
|
113
|
+
option :emails, [] do
|
114
|
+
option :address, proc(&:to_s)
|
115
|
+
option :description, proc(&:to_s)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
user = User.new name: "joe",
|
120
|
+
emails: { address: "joe@example.com", description: "Job email" }
|
121
|
+
|
122
|
+
user.emails.class # => Array
|
123
|
+
user.emails.first.class # => User::Emails
|
124
|
+
user.emails.first.address # => "joe@example.com"
|
125
|
+
|
126
|
+
user.emails.to_h # => [{ address: "joe@example.com", description: "Job email" }]
|
127
|
+
```
|
128
|
+
|
129
|
+
Notice how we mixed array wrapper with a nested type.
|
130
|
+
|
131
|
+
The only syntax restriction here is that you cannot use a positional `param` _inside_ the block.
|
132
|
+
|
133
|
+
## Back References
|
134
|
+
|
135
|
+
Sometimes you need to refer back to the initialized instance. In this case use a second argument to explicitly give the instance to a coercer:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
class Location < String
|
139
|
+
attr_reader :parameter # refers back to its parameter
|
140
|
+
|
141
|
+
def initialize(name, parameter)
|
142
|
+
super(name)
|
143
|
+
@parameter = parameter
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class Parameter
|
148
|
+
extend Dry::Initializer
|
149
|
+
param :name
|
150
|
+
param :location, ->(value, param) { Location.new(value, param) }
|
151
|
+
end
|
152
|
+
|
153
|
+
offset = Parameter.new "offset", location: "query"
|
154
|
+
offset.name # => "offset"
|
155
|
+
offset.location # => "query"
|
156
|
+
offset.location.parameter == offset # true
|
157
|
+
```
|
158
|
+
|
159
|
+
[dry-types]: https://github.com/dry-rb/dry-types
|
160
|
+
[dry-types-docs]: http://dry-rb.org/gems/dry-types/
|