dry-initializer 2.3.0 → 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 (59) 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 +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 +160 -13
  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 +13 -15
  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 +3 -3
  30. data/lib/dry/initializer.rb +11 -9
  31. data/lib/dry/initializer/builders/attribute.rb +4 -4
  32. data/lib/dry/initializer/builders/reader.rb +1 -1
  33. data/lib/dry/initializer/config.rb +22 -11
  34. data/lib/dry/initializer/definition.rb +11 -62
  35. data/lib/dry/initializer/dispatchers.rb +112 -0
  36. data/lib/dry/initializer/dispatchers/build_nested_type.rb +59 -0
  37. data/lib/dry/initializer/dispatchers/check_type.rb +43 -0
  38. data/lib/dry/initializer/dispatchers/prepare_default.rb +40 -0
  39. data/lib/dry/initializer/dispatchers/prepare_ivar.rb +12 -0
  40. data/lib/dry/initializer/dispatchers/prepare_optional.rb +13 -0
  41. data/lib/dry/initializer/dispatchers/prepare_reader.rb +30 -0
  42. data/lib/dry/initializer/dispatchers/prepare_source.rb +28 -0
  43. data/lib/dry/initializer/dispatchers/prepare_target.rb +44 -0
  44. data/lib/dry/initializer/dispatchers/unwrap_type.rb +22 -0
  45. data/lib/dry/initializer/dispatchers/wrap_type.rb +27 -0
  46. data/lib/dry/initializer/mixin/root.rb +1 -0
  47. data/lib/dry/initializer/struct.rb +39 -0
  48. data/lib/dry/initializer/undefined.rb +2 -0
  49. data/spec/coercion_of_nil_spec.rb +25 -0
  50. data/spec/custom_dispatchers_spec.rb +35 -0
  51. data/spec/definition_spec.rb +6 -2
  52. data/spec/list_type_spec.rb +32 -0
  53. data/spec/nested_type_spec.rb +48 -0
  54. data/spec/spec_helper.rb +9 -1
  55. data/spec/type_argument_spec.rb +2 -2
  56. data/spec/type_constraint_spec.rb +6 -6
  57. data/spec/value_coercion_via_dry_types_spec.rb +1 -1
  58. metadata +48 -9
  59. data/.travis.yml +0 -24
@@ -0,0 +1,40 @@
1
+ #
2
+ # Prepares the `:default` option
3
+ #
4
+ # It must respond to `.call` without arguments
5
+ #
6
+ module Dry::Initializer::Dispatchers::PrepareDefault
7
+ extend self
8
+
9
+ def call(default: nil, optional: nil, **options)
10
+ default = callable! default
11
+ check_arity! default
12
+
13
+ { default: default, optional: (optional | default), **options }
14
+ end
15
+
16
+ private
17
+
18
+ def callable!(default)
19
+ return unless default
20
+ return default if default.respond_to?(:call)
21
+ return callable(default.to_proc) if default.respond_to?(:to_proc)
22
+
23
+ invalid!(default)
24
+ end
25
+
26
+ def check_arity!(default)
27
+ return unless default
28
+
29
+ arity = default.method(:call).arity.to_i
30
+ return unless arity.positive?
31
+
32
+ invalid!(default)
33
+ end
34
+
35
+ def invalid!(default)
36
+ raise TypeError, "The #{default.inspect} should be" \
37
+ " either convertable to proc with no arguments," \
38
+ " or respond to #call without arguments."
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ #
2
+ # Prepares the variable name of a parameter or an option.
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareIvar
5
+ module_function
6
+
7
+ def call(target:, **options)
8
+ ivar = "@#{target}".delete("?").to_sym
9
+
10
+ { target: target, ivar: ivar, **options }
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ #
2
+ # Defines whether an argument is optional
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareOptional
5
+ module_function
6
+
7
+ def call(optional: nil, default: nil, required: nil, **options)
8
+ optional ||= default
9
+ optional &&= !required
10
+
11
+ { optional: !!optional, default: default, **options }
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ #
2
+ # Checks the reader privacy
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareReader
5
+ extend self
6
+
7
+ def call(target: nil, reader: :public, **options)
8
+ reader = case reader.to_s
9
+ when "false", "" then nil
10
+ when "true" then :public
11
+ when "public", "private", "protected" then reader.to_sym
12
+ else invalid_reader!(target, reader)
13
+ end
14
+
15
+ { target: target, reader: reader, **options }
16
+ end
17
+
18
+ private
19
+
20
+ def invalid_reader!(target, _reader)
21
+ raise ArgumentError, <<~MESSAGE
22
+ Invalid setting for the ##{target} reader's privacy.
23
+ Use the one of the following values for the `:reader` option:
24
+ - 'public' (true) for the public reader (default)
25
+ - 'private' for the private reader
26
+ - 'protected' for the protected reader
27
+ - nil (false) if no reader should be defined
28
+ MESSAGE
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ # The dispatcher verifies a correctness of the source name
3
+ # of param or option, taken as a `:source` option.
4
+ #
5
+ # We allow any stringified name for the source.
6
+ # For example, this syntax is correct because we accept any key
7
+ # in the original hash of arguments, but give them proper names:
8
+ #
9
+ # ```ruby
10
+ # class Foo
11
+ # extend Dry::Initializer
12
+ #
13
+ # option "", as: :first
14
+ # option 1, as: :second
15
+ # end
16
+ #
17
+ # foo = Foo.new("": 42, 1: 666)
18
+ # foo.first # => 42
19
+ # foo.second # => 666
20
+ # ```
21
+ #
22
+ module Dry::Initializer::Dispatchers::PrepareSource
23
+ module_function
24
+
25
+ def call(source:, **options)
26
+ { source: source.to_s.to_sym, **options }
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ #
2
+ # Prepares the target name of a parameter or an option.
3
+ #
4
+ # Unlike source, the target must satisfy requirements for Ruby variable names.
5
+ # It also shouldn't be in conflict with names used by the gem.
6
+ #
7
+ module Dry::Initializer::Dispatchers::PrepareTarget
8
+ extend self
9
+
10
+ # List of variable names reserved by the gem
11
+ RESERVED = %i[
12
+ __dry_initializer_options__
13
+ __dry_initializer_config__
14
+ __dry_initializer_value__
15
+ __dry_initializer_definition__
16
+ __dry_initializer_initializer__
17
+ ].freeze
18
+
19
+ def call(source:, target: nil, as: nil, **options)
20
+ target ||= as || source
21
+ target = target.to_s.to_sym.downcase
22
+
23
+ check_ruby_name!(target)
24
+ check_reserved_names!(target)
25
+
26
+ { source: source, target: target, **options }
27
+ end
28
+
29
+ private
30
+
31
+ def check_ruby_name!(target)
32
+ return if target[/\A[[:alpha:]_][[:alnum:]_]*\??\z/u]
33
+
34
+ raise ArgumentError,
35
+ "The name `#{target}` is not allowed for Ruby methods"
36
+ end
37
+
38
+ def check_reserved_names!(target)
39
+ return unless RESERVED.include?(target)
40
+
41
+ raise ArgumentError,
42
+ "The method name `#{target}` is reserved by the dry-initializer gem"
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ #
2
+ # Looks at the `:type` option and counts how many nested arrays
3
+ # it contains around either nil or a callable value.
4
+ #
5
+ # The counted number is preserved in the `:wrap` virtual option
6
+ # used by the [WrapType] dispatcher.
7
+ #
8
+ module Dry::Initializer::Dispatchers::UnwrapType
9
+ extend self
10
+
11
+ def call(type: nil, wrap: 0, **options)
12
+ type, wrap = unwrap(type, 0)
13
+
14
+ { type: type, wrap: wrap, **options }
15
+ end
16
+
17
+ private
18
+
19
+ def unwrap(type, count)
20
+ type.is_a?(Array) ? unwrap(type.first, count + 1) : [type, count]
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ #
2
+ # Takes `:type` and `:wrap` to construct the final value coercer
3
+ #
4
+ module Dry::Initializer::Dispatchers::WrapType
5
+ extend self
6
+
7
+ def call(type: nil, wrap: 0, **options)
8
+ { type: wrapped_type(type, wrap), **options }
9
+ end
10
+
11
+ private
12
+
13
+ def wrapped_type(type, count)
14
+ return type if count.zero?
15
+
16
+ ->(value) { wrap_value(value, count, type) }
17
+ end
18
+
19
+ def wrap_value(value, count, type)
20
+ if count.zero?
21
+ type ? type.call(value) : value
22
+ else
23
+ return [wrap_value(value, count - 1, type)] unless value.is_a?(Array)
24
+ value.map { |item| wrap_value(item, count - 1, type) }
25
+ end
26
+ end
27
+ end
@@ -6,5 +6,6 @@ module Dry::Initializer::Mixin
6
6
  def initialize(*args)
7
7
  __dry_initializer_initialize__(*args)
8
8
  end
9
+ ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
9
10
  end
10
11
  end
@@ -0,0 +1,39 @@
1
+ #
2
+ # The nested structure that takes nested hashes with indifferent access
3
+ #
4
+ class Dry::Initializer::Struct
5
+ extend Dry::Initializer
6
+
7
+ class << self
8
+ undef_method :param
9
+
10
+ def new(options)
11
+ super(**Hash(options).each_with_object({}) { |(k, v), h| h[k.to_sym] = v })
12
+ end
13
+ alias call new
14
+ end
15
+
16
+ #
17
+ # Represents event data as a nested hash with deeply stringified keys
18
+ # @return [Hash<String, ...>]
19
+ #
20
+ def to_h
21
+ self
22
+ .class
23
+ .dry_initializer
24
+ .attributes(self)
25
+ .each_with_object({}) { |(k, v), h| h[k.to_s] = __hashify(v) }
26
+ end
27
+
28
+ private
29
+
30
+ def __hashify(value)
31
+ case value
32
+ when Hash
33
+ value.each_with_object({}) { |(k, v), obj| obj[k.to_s] = __hashify(v) }
34
+ when Array then value.map { |v| __hashify(v) }
35
+ when Dry::Initializer::Struct then value.to_h
36
+ else value
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,2 @@
1
+ module Dry::Initializer::UNDEFINED
2
+ end
@@ -0,0 +1,25 @@
1
+ describe "coercion of nil" do
2
+ before do
3
+ class Test::Foo
4
+ extend Dry::Initializer
5
+ param :bar, proc(&:to_i)
6
+ end
7
+
8
+ class Test::Baz
9
+ include Dry::Initializer.define -> do
10
+ param :qux, proc(&:to_i)
11
+ end
12
+ end
13
+ end
14
+
15
+ let(:foo) { Test::Foo.new(nil) }
16
+ let(:baz) { Test::Baz.new(nil) }
17
+
18
+ it "works with extend syntax" do
19
+ expect(foo.bar).to eq 0
20
+ end
21
+
22
+ it "works with include syntax" do
23
+ expect(baz.qux).to eq 0
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ describe "custom dispatchers" do
2
+ subject { Test::Foo.new "123" }
3
+
4
+ before do
5
+ dispatcher = ->(op) { op[:integer] ? op.merge(type: proc(&:to_i)) : op }
6
+ Dry::Initializer::Dispatchers << dispatcher
7
+ end
8
+
9
+ context "with extend syntax" do
10
+ before do
11
+ class Test::Foo
12
+ extend Dry::Initializer
13
+ param :id, integer: true
14
+ end
15
+ end
16
+
17
+ it "adds syntax sugar" do
18
+ expect(subject.id).to eq 123
19
+ end
20
+ end
21
+
22
+ context "with include syntax" do
23
+ before do
24
+ class Test::Foo
25
+ include Dry::Initializer.define -> do
26
+ param :id, integer: true
27
+ end
28
+ end
29
+ end
30
+
31
+ it "adds syntax sugar" do
32
+ expect(subject.id).to eq 123
33
+ end
34
+ end
35
+ end
@@ -22,7 +22,9 @@ describe "definition" do
22
22
  [definition.source, definition.options]
23
23
  end
24
24
 
25
- expect(params).to eq [[:foo, { as: :foo, reader: :public }]]
25
+ expect(params).to eq [
26
+ [:foo, { as: :foo, reader: :public, optional: false }]
27
+ ]
26
28
  end
27
29
 
28
30
  it "preservers definition options" do
@@ -30,7 +32,9 @@ describe "definition" do
30
32
  [definition.source, definition.options]
31
33
  end
32
34
 
33
- expect(options).to eq [[:bar, { as: :bar, reader: :public }]]
35
+ expect(options).to eq [
36
+ [:bar, { as: :bar, reader: :public, optional: false }]
37
+ ]
34
38
  end
35
39
  end
36
40
 
@@ -0,0 +1,32 @@
1
+ require "dry-types"
2
+
3
+ describe "list type argument" do
4
+ before do
5
+ class Test::Foo
6
+ extend Dry::Initializer
7
+ param :foo, [proc(&:to_s)]
8
+ option :bar, [Dry::Types["strict.string"]]
9
+ option :baz, []
10
+ end
11
+ end
12
+
13
+ context "with single items" do
14
+ subject { Test::Foo.new(1, bar: "2", baz: { qux: :QUX }) }
15
+
16
+ it "coerces and wraps them to arrays" do
17
+ expect(subject.foo).to eq %w[1]
18
+ expect(subject.bar).to eq %w[2]
19
+ expect(subject.baz).to eq [{ qux: :QUX }]
20
+ end
21
+ end
22
+
23
+ context "with arrays" do
24
+ subject { Test::Foo.new([1], bar: %w[2], baz: [{ qux: :QUX }]) }
25
+
26
+ it "coerces elements" do
27
+ expect(subject.foo).to eq %w[1]
28
+ expect(subject.bar).to eq %w[2]
29
+ expect(subject.baz).to eq [{ qux: :QUX }]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,48 @@
1
+ describe "nested type argument" do
2
+ subject { Test::Xyz.new("bar" => { "baz" => 42 }) }
3
+
4
+ context "with nested definition only" do
5
+ before do
6
+ class Test::Xyz
7
+ extend Dry::Initializer
8
+
9
+ param :foo, as: :x do
10
+ option :bar, as: :y do
11
+ option :baz, proc(&:to_s), as: :z
12
+ option :qux, as: :w, optional: true
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ it "builds the type" do
19
+ expect(subject.x.y.z).to eq "42"
20
+ end
21
+
22
+ it "converts the nested type to hash" do
23
+ expect(subject.x.to_h).to eq("y" => { "z" => "42" })
24
+ end
25
+ end
26
+
27
+ context "with nested and wrapped definitions" do
28
+ before do
29
+ class Test::Xyz
30
+ extend Dry::Initializer
31
+
32
+ param :foo, [], as: :x do
33
+ option :bar, as: :y do
34
+ option :baz, proc(&:to_s), as: :z
35
+ option :qux, as: :w, optional: true
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ it "builds the type" do
42
+ x = subject.x
43
+ expect(x).to be_instance_of Array
44
+
45
+ expect(x.first.y.z).to eq "42"
46
+ end
47
+ end
48
+ end