dry-initializer 3.0.1 → 3.0.2

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