dry-initializer 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +12 -0
  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 +12 -0
  10. data/.rspec +4 -0
  11. data/.rubocop.yml +89 -0
  12. data/CHANGELOG.md +890 -0
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +29 -0
  15. data/Gemfile +38 -0
  16. data/Guardfile +5 -0
  17. data/LICENSE +20 -0
  18. data/LICENSE.txt +21 -0
  19. data/README.md +89 -0
  20. data/Rakefile +8 -0
  21. data/benchmarks/compare_several_defaults.rb +82 -0
  22. data/benchmarks/plain_options.rb +63 -0
  23. data/benchmarks/plain_params.rb +84 -0
  24. data/benchmarks/with_coercion.rb +71 -0
  25. data/benchmarks/with_defaults.rb +66 -0
  26. data/benchmarks/with_defaults_and_coercion.rb +59 -0
  27. data/docsite/source/attributes.html.md +106 -0
  28. data/docsite/source/container-version.html.md +39 -0
  29. data/docsite/source/index.html.md +43 -0
  30. data/docsite/source/inheritance.html.md +43 -0
  31. data/docsite/source/optionals-and-defaults.html.md +130 -0
  32. data/docsite/source/options-tolerance.html.md +27 -0
  33. data/docsite/source/params-and-options.html.md +74 -0
  34. data/docsite/source/rails-support.html.md +101 -0
  35. data/docsite/source/readers.html.md +43 -0
  36. data/docsite/source/skip-undefined.html.md +59 -0
  37. data/docsite/source/type-constraints.html.md +160 -0
  38. data/dry-initializer.gemspec +20 -0
  39. data/lib/dry-initializer.rb +1 -0
  40. data/lib/dry/initializer.rb +61 -0
  41. data/lib/dry/initializer/builders.rb +7 -0
  42. data/lib/dry/initializer/builders/attribute.rb +81 -0
  43. data/lib/dry/initializer/builders/initializer.rb +61 -0
  44. data/lib/dry/initializer/builders/reader.rb +50 -0
  45. data/lib/dry/initializer/builders/signature.rb +32 -0
  46. data/lib/dry/initializer/config.rb +184 -0
  47. data/lib/dry/initializer/definition.rb +65 -0
  48. data/lib/dry/initializer/dispatchers.rb +112 -0
  49. data/lib/dry/initializer/dispatchers/build_nested_type.rb +59 -0
  50. data/lib/dry/initializer/dispatchers/check_type.rb +43 -0
  51. data/lib/dry/initializer/dispatchers/prepare_default.rb +40 -0
  52. data/lib/dry/initializer/dispatchers/prepare_ivar.rb +12 -0
  53. data/lib/dry/initializer/dispatchers/prepare_optional.rb +13 -0
  54. data/lib/dry/initializer/dispatchers/prepare_reader.rb +30 -0
  55. data/lib/dry/initializer/dispatchers/prepare_source.rb +28 -0
  56. data/lib/dry/initializer/dispatchers/prepare_target.rb +44 -0
  57. data/lib/dry/initializer/dispatchers/unwrap_type.rb +22 -0
  58. data/lib/dry/initializer/dispatchers/wrap_type.rb +27 -0
  59. data/lib/dry/initializer/dsl.rb +43 -0
  60. data/lib/dry/initializer/mixin.rb +15 -0
  61. data/lib/dry/initializer/mixin/local.rb +19 -0
  62. data/lib/dry/initializer/mixin/root.rb +11 -0
  63. data/lib/dry/initializer/struct.rb +39 -0
  64. data/lib/dry/initializer/undefined.rb +2 -0
  65. data/lib/tasks/benchmark.rake +41 -0
  66. data/lib/tasks/profile.rake +78 -0
  67. data/spec/attributes_spec.rb +38 -0
  68. data/spec/coercion_of_nil_spec.rb +25 -0
  69. data/spec/custom_dispatchers_spec.rb +35 -0
  70. data/spec/custom_initializer_spec.rb +30 -0
  71. data/spec/default_values_spec.rb +83 -0
  72. data/spec/definition_spec.rb +111 -0
  73. data/spec/invalid_default_spec.rb +13 -0
  74. data/spec/list_type_spec.rb +32 -0
  75. data/spec/missed_default_spec.rb +14 -0
  76. data/spec/nested_type_spec.rb +48 -0
  77. data/spec/optional_spec.rb +71 -0
  78. data/spec/options_tolerance_spec.rb +11 -0
  79. data/spec/public_attributes_utility_spec.rb +22 -0
  80. data/spec/reader_spec.rb +87 -0
  81. data/spec/repetitive_definitions_spec.rb +69 -0
  82. data/spec/several_assignments_spec.rb +41 -0
  83. data/spec/spec_helper.rb +29 -0
  84. data/spec/subclassing_spec.rb +49 -0
  85. data/spec/type_argument_spec.rb +35 -0
  86. data/spec/type_constraint_spec.rb +78 -0
  87. data/spec/value_coercion_via_dry_types_spec.rb +29 -0
  88. metadata +209 -0
@@ -0,0 +1,71 @@
1
+ Bundler.require(:benchmarks)
2
+
3
+ require "dry-initializer"
4
+ class DryTest
5
+ extend Dry::Initializer[undefined: false]
6
+
7
+ option :foo, proc(&:to_s)
8
+ option :bar, proc(&:to_s)
9
+ end
10
+
11
+ class DryTestUndefined
12
+ extend Dry::Initializer
13
+
14
+ option :foo, proc(&:to_s)
15
+ option :bar, proc(&:to_s)
16
+ end
17
+
18
+ class PlainRubyTest
19
+ attr_reader :foo, :bar
20
+
21
+ def initialize(options)
22
+ @foo = options[:foo].to_s
23
+ @bar = options[:bar].to_s
24
+ end
25
+ end
26
+
27
+ require "virtus"
28
+ class VirtusTest
29
+ include Virtus.model
30
+
31
+ attribute :foo, String
32
+ attribute :bar, String
33
+ end
34
+
35
+ require "fast_attributes"
36
+ class FastAttributesTest
37
+ extend FastAttributes
38
+
39
+ define_attributes initialize: true do
40
+ attribute :foo, String
41
+ attribute :bar, String
42
+ end
43
+ end
44
+
45
+ puts "Benchmark for instantiation with coercion"
46
+
47
+ Benchmark.ips do |x|
48
+ x.config time: 15, warmup: 10
49
+
50
+ x.report("plain Ruby") do
51
+ PlainRubyTest.new foo: "FOO", bar: "BAR"
52
+ end
53
+
54
+ x.report("dry-initializer") do
55
+ DryTest.new foo: "FOO", bar: "BAR"
56
+ end
57
+
58
+ x.report("dry-initializer (with UNDEFINED)") do
59
+ DryTestUndefined.new foo: "FOO", bar: "BAR"
60
+ end
61
+
62
+ x.report("virtus") do
63
+ VirtusTest.new foo: "FOO", bar: "BAR"
64
+ end
65
+
66
+ x.report("fast_attributes") do
67
+ FastAttributesTest.new foo: "FOO", bar: "BAR"
68
+ end
69
+
70
+ x.compare!
71
+ end
@@ -0,0 +1,66 @@
1
+ Bundler.require(:benchmarks)
2
+
3
+ require "dry-initializer"
4
+ class DryTest
5
+ extend Dry::Initializer[undefined: false]
6
+
7
+ option :foo, default: -> { "FOO" }
8
+ option :bar, default: -> { "BAR" }
9
+ end
10
+
11
+ class DryTestUndefined
12
+ extend Dry::Initializer
13
+
14
+ option :foo, default: -> { "FOO" }
15
+ option :bar, default: -> { "BAR" }
16
+ end
17
+
18
+ class PlainRubyTest
19
+ attr_reader :foo, :bar
20
+
21
+ def initialize(foo: "FOO", bar: "BAR")
22
+ @foo = foo
23
+ @bar = bar
24
+ end
25
+ end
26
+
27
+ require "kwattr"
28
+ class KwattrTest
29
+ kwattr foo: "FOO", bar: "BAR"
30
+ end
31
+
32
+ require "active_attr"
33
+ class ActiveAttrTest
34
+ include ActiveAttr::AttributeDefaults
35
+
36
+ attribute :foo, default: "FOO"
37
+ attribute :bar, default: "BAR"
38
+ end
39
+
40
+ puts "Benchmark for instantiation with default values"
41
+
42
+ Benchmark.ips do |x|
43
+ x.config time: 15, warmup: 10
44
+
45
+ x.report("plain Ruby") do
46
+ PlainRubyTest.new
47
+ end
48
+
49
+ x.report("dry-initializer") do
50
+ DryTest.new
51
+ end
52
+
53
+ x.report("dry-initializer (with UNDEFINED)") do
54
+ DryTestUndefined.new
55
+ end
56
+
57
+ x.report("kwattr") do
58
+ KwattrTest.new
59
+ end
60
+
61
+ x.report("active_attr") do
62
+ ActiveAttrTest.new
63
+ end
64
+
65
+ x.compare!
66
+ end
@@ -0,0 +1,59 @@
1
+ Bundler.require(:benchmarks)
2
+
3
+ require "dry-initializer"
4
+ class DryTest
5
+ extend Dry::Initializer[undefined: false]
6
+
7
+ option :foo, proc(&:to_s), default: -> { "FOO" }
8
+ option :bar, proc(&:to_s), default: -> { "BAR" }
9
+ end
10
+
11
+ class DryTestUndefined
12
+ extend Dry::Initializer
13
+
14
+ option :foo, proc(&:to_s), default: -> { "FOO" }
15
+ option :bar, proc(&:to_s), default: -> { "BAR" }
16
+ end
17
+
18
+ class PlainRubyTest
19
+ attr_reader :foo, :bar
20
+
21
+ def initialize(foo: "FOO", bar: "BAR")
22
+ @foo = foo
23
+ @bar = bar
24
+ raise TypeError unless String === @foo
25
+ raise TypeError unless String === @bar
26
+ end
27
+ end
28
+
29
+ require "virtus"
30
+ class VirtusTest
31
+ include Virtus.model
32
+
33
+ attribute :foo, String, default: "FOO"
34
+ attribute :bar, String, default: "BAR"
35
+ end
36
+
37
+ puts "Benchmark for instantiation with type constraints and default values"
38
+
39
+ Benchmark.ips do |x|
40
+ x.config time: 15, warmup: 10
41
+
42
+ x.report("plain Ruby") do
43
+ PlainRubyTest.new
44
+ end
45
+
46
+ x.report("dry-initializer") do
47
+ DryTest.new
48
+ end
49
+
50
+ x.report("dry-initializer (with UNDEFINED)") do
51
+ DryTest.new
52
+ end
53
+
54
+ x.report("virtus") do
55
+ VirtusTest.new
56
+ end
57
+
58
+ x.compare!
59
+ end
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: Attributes
3
+ layout: gem-single
4
+ name: dry-initializer
5
+ ---
6
+
7
+ Sometimes you need to access all attributes assigned via params and options of the object constructor.
8
+
9
+ We support 2 methods: `attributes` and `public_attributes` for this goal. Both methods are wrapped into container accessible via `.dry_types` container:
10
+
11
+ ```ruby
12
+ require 'dry-initializer'
13
+
14
+ class User
15
+ extend Dry::Initializer
16
+
17
+ param :name
18
+ option :email, optional: true
19
+ option :telefon, optional: true, as: :phone
20
+ end
21
+
22
+ user = User.new "Andy", telefon: "71002003040"
23
+
24
+ User.dry_initializer.attributes(user)
25
+ # => { name: "Andy", phone: "71002003040" }
26
+ ```
27
+
28
+ What the method does is extracts *variables assigned* to the object (and skips unassigned ones like the `email` above). It doesn't matter whether you send it via `params` or `option`; we look at the result of the instantiation, not at the interface.
29
+
30
+ Method `public_attributes` works different. Let's look at the following example to see the difference:
31
+
32
+ ```ruby
33
+ require 'dry-initializer'
34
+
35
+ class User
36
+ extend Dry::Initializer
37
+
38
+ param :name
39
+ option :telefon, optional: true, as: :phone
40
+ option :email, optional: true
41
+ option :token, optional: true, reader: :private
42
+ option :password, optional: true, reader: false
43
+ end
44
+
45
+ user = User.new "Andy", telefon: "71002003040", token: "foo", password: "bar"
46
+
47
+ User.dry_initializer.attributes(user)
48
+ # => { name: "Andy", phone: "71002003040", token: "foo", password: "bar" }
49
+
50
+ User.dry_initializer.public_attributes(user)
51
+ # => { name: "Andy", phone: "71002003040", email: nil }
52
+ ```
53
+
54
+ Notice that `public_attribute` reads *public reader methods*, not variables. That's why it skips both the private `token`, and the `password` whose reader hasn't been defined.
55
+
56
+ Another difference concerns unassigned values. Because the reader `user.email` returns `nil` (its `@email` variable contains `Dry::Initializer::UNDEFINED` constant), the `public_attributes` adds this value to the hash using the method.
57
+
58
+ The third thing to mention is that you can override the reader, and it is the overriden method which will be used by `public_attributes`:
59
+
60
+ ```ruby
61
+ require 'dry-initializer'
62
+
63
+ class User
64
+ extend Dry::Initializer
65
+
66
+ param :name
67
+ option :password, optional: true
68
+
69
+ def password
70
+ super.hash.to_s
71
+ end
72
+ end
73
+
74
+ user = User.new "Joe", password: "foo"
75
+
76
+ User.dry_initializer.attributes(user)
77
+ # => { user: "Joe", password: "foo" }
78
+
79
+ User.dry_initializer.public_attributes(user)
80
+ # => { user: "Joe", password: "-1844874613000160009" }
81
+ ```
82
+
83
+ This feature works for the "extend Dry::Initializer" syntax. But what about "include Dry::Initializer.define ..."? Now we don't pollute class namespace with new methods, that's why `.dry_initializer` is absent.
84
+
85
+ To access config you can use a hack. Under the hood we define private instance method `#__dry_initializer_config__` which refers to the same container. So you can write:
86
+
87
+ ```ruby
88
+ require 'dry-initializer'
89
+
90
+ class User
91
+ extend Dry::Initializer
92
+ param :name
93
+ end
94
+
95
+ user = User.new "Joe"
96
+
97
+ user.send(:__dry_initializer_config__).attributes(user)
98
+ # => { user: "Joe" }
99
+
100
+ user.send(:__dry_initializer_config__).public_attributes(user)
101
+ # => { user: "Joe" }
102
+ ```
103
+
104
+ This is a hack because the `__dry_initializer_config__` is not a part of the gem's public interface; there's a possibility it can be changed or removed in the later releases.
105
+
106
+ We'll try to be careful with it, and mark it as deprecated method in case of such a removal.
@@ -0,0 +1,39 @@
1
+ ---
2
+ title: Container Version
3
+ layout: gem-single
4
+ name: dry-initializer
5
+ ---
6
+
7
+ Instead of extending a class with the `Dry::Initializer`, you can include a container with the `initializer` method only. This method should be preferred when you don't need subclassing.
8
+
9
+ ```ruby
10
+ require 'dry-initializer'
11
+
12
+ class User
13
+ # notice `-> do .. end` syntax
14
+ include Dry::Initializer.define -> do
15
+ param :name, proc(&:to_s)
16
+ param :role, default: proc { 'customer' }
17
+ option :admin, default: proc { false }
18
+ end
19
+ end
20
+ ```
21
+
22
+ If you still need the DSL (`param` and `option`) to be inherited, use the direct extension:
23
+
24
+ ```ruby
25
+ require 'dry-initializer'
26
+
27
+ class BaseService
28
+ extend Dry::Initializer
29
+ alias_method :dependency, :param
30
+ end
31
+
32
+ class ShowUser < BaseService
33
+ dependency :user
34
+
35
+ def call
36
+ puts user&.name
37
+ end
38
+ end
39
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: Introduction & Usage
3
+ description: DSL for defining initializer params and options
4
+ layout: gem-single
5
+ order: 8
6
+ type: gem
7
+ name: dry-initializer
8
+ sections:
9
+ - container-version
10
+ - params-and-options
11
+ - options-tolerance
12
+ - optionals-and-defaults
13
+ - type-constraints
14
+ - readers
15
+ - inheritance
16
+ - skip-undefined
17
+ - attributes
18
+ - rails-support
19
+ ---
20
+
21
+ `dry-initializer` is a simple mixin of class methods `params` and `options` for instances.
22
+
23
+ ## Synopsis
24
+
25
+ ```ruby
26
+ require 'dry-initializer'
27
+
28
+ class User
29
+ extend Dry::Initializer
30
+
31
+ param :name, proc(&:to_s)
32
+ param :role, default: proc { 'customer' }
33
+ option :admin, default: proc { false }
34
+ option :phone, optional: true
35
+ end
36
+
37
+ user = User.new 'Vladimir', 'admin', admin: true
38
+
39
+ user.name # => 'Vladimir'
40
+ user.role # => 'admin'
41
+ user.admin # => true
42
+ user.phone # => nil
43
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: Inheritance
3
+ layout: gem-single
4
+ name: dry-initializer
5
+ ---
6
+
7
+ Subclassing preserves all definitions being made inside a superclass.
8
+
9
+ ```ruby
10
+ require 'dry-initializer'
11
+
12
+ class User
13
+ extend Dry::Initializer
14
+
15
+ param :name
16
+ end
17
+
18
+ class Employee < User
19
+ param :position
20
+ end
21
+
22
+ employee = Employee.new('John', 'supercargo')
23
+ employee.name # => 'John'
24
+ employee.position # => 'supercargo'
25
+
26
+ employee = Employee.new # => fails because type
27
+ ```
28
+
29
+ You can override params and options.
30
+ Such overriding leaves initial order of params (positional arguments) unchanged:
31
+
32
+ ```ruby
33
+ class Employee < User
34
+ param :position, optional: true
35
+ param :name, default: proc { 'Unknown' }
36
+ end
37
+
38
+ user = User.new # => Boom! because User#name is required
39
+ employee = Employee.new # passes because who cares on employee's name
40
+
41
+ employee.name
42
+ # => 'Unknown' because it is the name that positioned first like in User
43
+ ```
@@ -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