dry-initializer 2.3.0 → 3.0.2

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