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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +10 -21
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/custom_ci.yml +74 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.gitignore +2 -0
  10. data/.rspec +2 -2
  11. data/.rubocop.yml +65 -27
  12. data/CHANGELOG.md +10 -3
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +29 -0
  15. data/Gemfile +26 -17
  16. data/LICENSE +20 -0
  17. data/README.md +11 -12
  18. data/docsite/source/attributes.html.md +106 -0
  19. data/docsite/source/container-version.html.md +39 -0
  20. data/docsite/source/index.html.md +43 -0
  21. data/docsite/source/inheritance.html.md +43 -0
  22. data/docsite/source/optionals-and-defaults.html.md +130 -0
  23. data/docsite/source/options-tolerance.html.md +27 -0
  24. data/docsite/source/params-and-options.html.md +74 -0
  25. data/docsite/source/rails-support.html.md +101 -0
  26. data/docsite/source/readers.html.md +43 -0
  27. data/docsite/source/skip-undefined.html.md +59 -0
  28. data/docsite/source/type-constraints.html.md +160 -0
  29. data/dry-initializer.gemspec +1 -1
  30. data/lib/dry/initializer/config.rb +5 -5
  31. data/lib/dry/initializer/dispatchers.rb +2 -2
  32. data/lib/dry/initializer/mixin/root.rb +1 -0
  33. data/lib/dry/initializer/struct.rb +2 -3
  34. data/spec/nested_type_spec.rb +4 -0
  35. data/spec/spec_helper.rb +7 -0
  36. data/spec/type_argument_spec.rb +2 -2
  37. data/spec/type_constraint_spec.rb +1 -1
  38. data/spec/value_coercion_via_dry_types_spec.rb +1 -1
  39. metadata +24 -4
  40. 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/