dry-initializer 2.4.0 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +5 -5
  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 +30 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/custom_ci.yml +58 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +56 -0
  9. data/.gitignore +2 -0
  10. data/.rspec +1 -1
  11. data/.rubocop.yml +76 -25
  12. data/CHANGELOG.md +150 -14
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +29 -0
  15. data/Gemfile +25 -18
  16. data/Gemfile.devtools +16 -0
  17. data/Guardfile +3 -3
  18. data/LICENSE +20 -0
  19. data/README.md +17 -79
  20. data/Rakefile +4 -4
  21. data/benchmarks/compare_several_defaults.rb +27 -27
  22. data/benchmarks/plain_options.rb +14 -14
  23. data/benchmarks/plain_params.rb +22 -22
  24. data/benchmarks/with_coercion.rb +14 -14
  25. data/benchmarks/with_defaults.rb +17 -17
  26. data/benchmarks/with_defaults_and_coercion.rb +14 -14
  27. data/bin/.gitkeep +0 -0
  28. data/docsite/source/attributes.html.md +106 -0
  29. data/docsite/source/container-version.html.md +39 -0
  30. data/docsite/source/index.html.md +43 -0
  31. data/docsite/source/inheritance.html.md +43 -0
  32. data/docsite/source/optionals-and-defaults.html.md +130 -0
  33. data/docsite/source/options-tolerance.html.md +27 -0
  34. data/docsite/source/params-and-options.html.md +74 -0
  35. data/docsite/source/rails-support.html.md +101 -0
  36. data/docsite/source/readers.html.md +43 -0
  37. data/docsite/source/skip-undefined.html.md +59 -0
  38. data/docsite/source/type-constraints.html.md +160 -0
  39. data/dry-initializer.gemspec +13 -13
  40. data/lib/dry-initializer.rb +1 -1
  41. data/lib/dry/initializer.rb +17 -16
  42. data/lib/dry/initializer/builders.rb +2 -2
  43. data/lib/dry/initializer/builders/attribute.rb +16 -11
  44. data/lib/dry/initializer/builders/initializer.rb +9 -13
  45. data/lib/dry/initializer/builders/reader.rb +4 -2
  46. data/lib/dry/initializer/builders/signature.rb +3 -3
  47. data/lib/dry/initializer/config.rb +25 -12
  48. data/lib/dry/initializer/definition.rb +20 -71
  49. data/lib/dry/initializer/dispatchers.rb +101 -33
  50. data/lib/dry/initializer/dispatchers/build_nested_type.rb +59 -0
  51. data/lib/dry/initializer/dispatchers/check_type.rb +43 -0
  52. data/lib/dry/initializer/dispatchers/prepare_default.rb +40 -0
  53. data/lib/dry/initializer/dispatchers/prepare_ivar.rb +12 -0
  54. data/lib/dry/initializer/dispatchers/prepare_optional.rb +13 -0
  55. data/lib/dry/initializer/dispatchers/prepare_reader.rb +30 -0
  56. data/lib/dry/initializer/dispatchers/prepare_source.rb +28 -0
  57. data/lib/dry/initializer/dispatchers/prepare_target.rb +44 -0
  58. data/lib/dry/initializer/dispatchers/unwrap_type.rb +22 -0
  59. data/lib/dry/initializer/dispatchers/wrap_type.rb +28 -0
  60. data/lib/dry/initializer/mixin.rb +4 -4
  61. data/lib/dry/initializer/mixin/root.rb +1 -0
  62. data/lib/dry/initializer/struct.rb +39 -0
  63. data/lib/dry/initializer/undefined.rb +2 -0
  64. data/lib/dry/initializer/version.rb +5 -0
  65. data/lib/tasks/benchmark.rake +13 -13
  66. data/lib/tasks/profile.rake +16 -16
  67. data/project.yml +2 -0
  68. data/spec/attributes_spec.rb +7 -7
  69. data/spec/coercion_of_nil_spec.rb +25 -0
  70. data/spec/custom_dispatchers_spec.rb +6 -6
  71. data/spec/custom_initializer_spec.rb +2 -2
  72. data/spec/default_values_spec.rb +9 -9
  73. data/spec/definition_spec.rb +16 -12
  74. data/spec/invalid_default_spec.rb +2 -2
  75. data/spec/list_type_spec.rb +32 -0
  76. data/spec/missed_default_spec.rb +2 -2
  77. data/spec/nested_type_spec.rb +48 -0
  78. data/spec/optional_spec.rb +16 -16
  79. data/spec/options_tolerance_spec.rb +2 -2
  80. data/spec/public_attributes_utility_spec.rb +5 -5
  81. data/spec/reader_spec.rb +13 -13
  82. data/spec/repetitive_definitions_spec.rb +9 -9
  83. data/spec/several_assignments_spec.rb +9 -9
  84. data/spec/spec_helper.rb +6 -3
  85. data/spec/subclassing_spec.rb +5 -5
  86. data/spec/support/coverage.rb +7 -0
  87. data/spec/support/warnings.rb +7 -0
  88. data/spec/type_argument_spec.rb +15 -15
  89. data/spec/type_constraint_spec.rb +46 -28
  90. data/spec/value_coercion_via_dry_types_spec.rb +8 -8
  91. metadata +51 -34
  92. data/.travis.yml +0 -24
@@ -0,0 +1,130 @@
1
+ ---
2
+ title: Optional Attributes and Default Values
3
+ layout: gem-single
4
+ name: dry-initializer
5
+ ---
6
+
7
+ By default both params and options are mandatory. Use `:default` key to make them optional:
8
+
9
+ ```ruby
10
+ require 'dry-initializer'
11
+
12
+ class User
13
+ extend Dry::Initializer
14
+
15
+ param :name, default: proc { 'Unknown user' }
16
+ option :email, default: proc { 'unknown@example.com' }
17
+ option :phone, optional: true
18
+ end
19
+
20
+ user = User.new
21
+ user.name # => 'Unknown user'
22
+ user.email # => 'unknown@example.com'
23
+ user.phone # => Dry::Initializer::UNDEFINED
24
+
25
+ user = User.new 'Vladimir', email: 'vladimir@example.com', phone: '71234567788'
26
+ user.name # => 'Vladimir'
27
+ user.email # => 'vladimir@example.com'
28
+ user.phone # => '71234567788'
29
+ ```
30
+
31
+ You cannot define required **parameter** after optional one. The following example raises `SyntaxError` exception:
32
+
33
+ ```ruby
34
+ require 'dry-initializer'
35
+
36
+ class User
37
+ extend Dry::Initializer
38
+
39
+ param :name, default: proc { 'Unknown name' }
40
+ param :email # => #<SyntaxError ...>
41
+ end
42
+ ```
43
+
44
+ You should assign `nil` value explicitly. Otherwise an instance variable it will be left undefined. In both cases attribute reader method will return `nil`.
45
+
46
+ ```ruby
47
+ require 'dry-initializer'
48
+
49
+ class User
50
+ extend Dry::Initializer
51
+
52
+ param :name
53
+ option :email, optional: true
54
+ end
55
+
56
+ user = User.new 'Andrew'
57
+ user.email # => nil
58
+ user.instance_variable_get :@email
59
+ # => Dry::Initializer::UNDEFINED
60
+
61
+ user = User.new 'Andrew', email: nil
62
+ user.email # => nil
63
+ user.instance_variable_get :@email
64
+ # => nil
65
+ ```
66
+
67
+ You can also set `nil` as a default value:
68
+
69
+ ```ruby
70
+ require 'dry-initializer'
71
+
72
+ class User
73
+ extend Dry::Initializer
74
+
75
+ param :name
76
+ option :email, default: proc { nil }
77
+ end
78
+
79
+ user = User.new 'Andrew'
80
+ user.email # => nil
81
+ user.instance_variable_get :@email
82
+ # => nil
83
+ ```
84
+
85
+ You **must** wrap default values into procs.
86
+
87
+ If you need to **assign** proc as a default value, wrap it to another one:
88
+
89
+ ```ruby
90
+ require 'dry-initializer'
91
+
92
+ class User
93
+ extend Dry::Initializer
94
+
95
+ param :name_proc, default: proc { proc { 'Unknown user' } }
96
+ end
97
+
98
+ user = User.new
99
+ user.name_proc.call # => 'Unknown user'
100
+ ```
101
+
102
+ Proc will be executed in a scope of new instance. You can refer to other arguments:
103
+
104
+ ```ruby
105
+ require 'dry-initializer'
106
+
107
+ class User
108
+ extend Dry::Initializer
109
+
110
+ param :name
111
+ param :email, default: proc { "#{name.downcase}@example.com" }
112
+ end
113
+
114
+ user = User.new 'Andrew'
115
+ user.email # => 'andrew@example.com'
116
+ ```
117
+
118
+ **Warning**: when using lambdas instead of procs, don't forget an argument, required by [instance_eval][instance_eval] (you can skip in in a proc).
119
+
120
+ ```ruby
121
+ require 'dry-initializer'
122
+
123
+ class User
124
+ extend Dry::Initializer
125
+
126
+ param :name, default: -> (obj) { 'Dude' }
127
+ end
128
+ ```
129
+
130
+ [instance_eval]: http://ruby-doc.org/core-2.2.0/BasicObject.html#method-i-instance_eval
@@ -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/