dry-initializer 2.5.0 → 3.0.4

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 (67) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +367 -239
  3. data/LICENSE +20 -0
  4. data/README.md +17 -79
  5. data/dry-initializer.gemspec +29 -16
  6. data/lib/dry-initializer.rb +1 -1
  7. data/lib/dry/initializer.rb +16 -14
  8. data/lib/dry/initializer/builders.rb +2 -2
  9. data/lib/dry/initializer/builders/attribute.rb +12 -7
  10. data/lib/dry/initializer/builders/initializer.rb +9 -13
  11. data/lib/dry/initializer/builders/reader.rb +3 -1
  12. data/lib/dry/initializer/builders/signature.rb +3 -3
  13. data/lib/dry/initializer/config.rb +22 -8
  14. data/lib/dry/initializer/definition.rb +20 -71
  15. data/lib/dry/initializer/dispatchers.rb +101 -33
  16. data/lib/dry/initializer/dispatchers/build_nested_type.rb +59 -0
  17. data/lib/dry/initializer/dispatchers/check_type.rb +43 -0
  18. data/lib/dry/initializer/dispatchers/prepare_default.rb +40 -0
  19. data/lib/dry/initializer/dispatchers/prepare_ivar.rb +12 -0
  20. data/lib/dry/initializer/dispatchers/prepare_optional.rb +13 -0
  21. data/lib/dry/initializer/dispatchers/prepare_reader.rb +30 -0
  22. data/lib/dry/initializer/dispatchers/prepare_source.rb +28 -0
  23. data/lib/dry/initializer/dispatchers/prepare_target.rb +44 -0
  24. data/lib/dry/initializer/dispatchers/unwrap_type.rb +22 -0
  25. data/lib/dry/initializer/dispatchers/wrap_type.rb +28 -0
  26. data/lib/dry/initializer/mixin.rb +4 -4
  27. data/lib/dry/initializer/mixin/root.rb +1 -0
  28. data/lib/dry/initializer/struct.rb +39 -0
  29. data/lib/dry/initializer/undefined.rb +2 -0
  30. data/lib/dry/initializer/version.rb +5 -0
  31. data/lib/tasks/benchmark.rake +13 -13
  32. data/lib/tasks/profile.rake +16 -16
  33. metadata +38 -103
  34. data/.codeclimate.yml +0 -23
  35. data/.gitignore +0 -10
  36. data/.rspec +0 -4
  37. data/.rubocop.yml +0 -51
  38. data/.travis.yml +0 -24
  39. data/Gemfile +0 -29
  40. data/Guardfile +0 -5
  41. data/LICENSE.txt +0 -21
  42. data/Rakefile +0 -8
  43. data/benchmarks/compare_several_defaults.rb +0 -82
  44. data/benchmarks/plain_options.rb +0 -63
  45. data/benchmarks/plain_params.rb +0 -84
  46. data/benchmarks/with_coercion.rb +0 -71
  47. data/benchmarks/with_defaults.rb +0 -66
  48. data/benchmarks/with_defaults_and_coercion.rb +0 -59
  49. data/spec/attributes_spec.rb +0 -38
  50. data/spec/coercion_of_nil_spec.rb +0 -25
  51. data/spec/custom_dispatchers_spec.rb +0 -35
  52. data/spec/custom_initializer_spec.rb +0 -30
  53. data/spec/default_values_spec.rb +0 -83
  54. data/spec/definition_spec.rb +0 -107
  55. data/spec/invalid_default_spec.rb +0 -13
  56. data/spec/missed_default_spec.rb +0 -14
  57. data/spec/optional_spec.rb +0 -71
  58. data/spec/options_tolerance_spec.rb +0 -11
  59. data/spec/public_attributes_utility_spec.rb +0 -22
  60. data/spec/reader_spec.rb +0 -87
  61. data/spec/repetitive_definitions_spec.rb +0 -69
  62. data/spec/several_assignments_spec.rb +0 -41
  63. data/spec/spec_helper.rb +0 -21
  64. data/spec/subclassing_spec.rb +0 -49
  65. data/spec/type_argument_spec.rb +0 -35
  66. data/spec/type_constraint_spec.rb +0 -78
  67. data/spec/value_coercion_via_dry_types_spec.rb +0 -29
@@ -14,17 +14,17 @@ module Dry::Initializer
14
14
 
15
15
  def options
16
16
  {
17
- as: target,
18
- type: type,
17
+ as: target,
18
+ type: type,
19
19
  optional: optional,
20
- default: default,
21
- reader: reader,
22
- desc: desc
20
+ default: default,
21
+ reader: reader,
22
+ desc: desc
23
23
  }.reject { |_, value| value.nil? }
24
24
  end
25
25
 
26
26
  def name
27
- @name ||= (option ? "option" : "parameter") << " '#{source}'"
27
+ @name ||= (option ? 'option' : 'parameter') << " '#{source}'"
28
28
  end
29
29
  alias to_s name
30
30
  alias to_str name
@@ -39,78 +39,27 @@ module Dry::Initializer
39
39
  end
40
40
 
41
41
  def inch
42
- @inch ||= (option ? "@option" : "@param ").tap do |text|
43
- text << " [Object]"
42
+ @inch ||= (option ? '@option' : '@param ').tap do |text|
43
+ text << ' [Object]'
44
44
  text << (option ? " :#{source}" : " #{source}")
45
- text << (optional ? " (optional)" : " (required)")
45
+ text << (optional ? ' (optional)' : ' (required)')
46
46
  text << " #{desc}" if desc
47
47
  end
48
48
  end
49
49
 
50
50
  private
51
51
 
52
- def initialize(option, null, source, coercer = nil, **options)
53
- @option = !!option
54
- @null = null
55
- @source = source.to_sym
56
- @target = check_target options.fetch(:as, source).to_sym
57
- @ivar = :"@#{target}"
58
- @type = check_type(coercer || options[:type])
59
- @reader = prepare_reader options.fetch(:reader, true)
60
- @default = check_default options[:default]
61
- @optional = options.fetch(:optional, @default)
62
- @desc = options[:desc]&.to_s&.capitalize
52
+ def initialize(**options)
53
+ @option = options[:option]
54
+ @null = options[:null]
55
+ @source = options[:source]
56
+ @target = options[:target]
57
+ @ivar = "@#{@target}"
58
+ @type = options[:type]
59
+ @reader = options[:reader]
60
+ @default = options[:default]
61
+ @optional = options[:optional]
62
+ @desc = options[:desc]
63
63
  end
64
-
65
- def check_source(value)
66
- if RESERVED.include? value
67
- raise ArgumentError, "Name #{value} is reserved by dry-initializer gem"
68
- end
69
-
70
- unless option || value[ATTRIBUTE]
71
- raise ArgumentError, "Invalid parameter name :'#{value}'"
72
- end
73
-
74
- value
75
- end
76
-
77
- def check_target(value)
78
- return value if value[ATTRIBUTE]
79
- raise ArgumentError, "Invalid variable name :'#{value}'"
80
- end
81
-
82
- def check_type(value)
83
- return if value.nil?
84
- arity = value.arity if value.is_a? Proc
85
- arity ||= value.method(:call).arity if value.respond_to? :call
86
- return value if [1, 2].include? arity.to_i.abs
87
- raise TypeError,
88
- "type of #{inspect} should respond to #call with 1..2 arguments"
89
- end
90
-
91
- def check_default(value)
92
- return if value.nil?
93
- return value if value.is_a?(Proc) && value.arity < 1
94
- raise TypeError,
95
- "default value of #{inspect} should be a proc without params"
96
- end
97
-
98
- def prepare_reader(value)
99
- case value.to_s
100
- when "", "false" then false
101
- when "private" then :private
102
- when "protected" then :protected
103
- else :public
104
- end
105
- end
106
-
107
- ATTRIBUTE = /\A\w+\z/
108
- RESERVED = %i[
109
- __dry_initializer_options__
110
- __dry_initializer_config__
111
- __dry_initializer_value__
112
- __dry_initializer_definition__
113
- __dry_initializer_initializer__
114
- ].freeze
115
64
  end
116
65
  end
@@ -1,44 +1,112 @@
1
- module Dry::Initializer
2
- #
3
- # @private
4
- #
5
- # Dispatchers allow adding syntax sugar to `.param` and `.option` methods.
1
+ #
2
+ # The module is responsible for __normalizing__ arguments
3
+ # of `.param` and `.option`.
4
+ #
5
+ # What the module does is convert the source list of arguments
6
+ # into the standard set of options:
7
+ # - `:option` -- whether an argument is an option (or param)
8
+ # - `:source` -- the name of source option
9
+ # - `:target` -- the target name of the reader
10
+ # - `:reader` -- if the reader's privacy (:public, :protected, :private, nil)
11
+ # - `:ivar` -- the target nane of the variable
12
+ # - `:type` -- the callable coercer of the source value
13
+ # - `:optional` -- if the argument is optional
14
+ # - `:default` -- the proc returning the default value of the source value
15
+ # - `:null` -- the value to be set to unassigned optional argument
16
+ #
17
+ # It is this set is used to build [Dry::Initializer::Definition].
18
+ #
19
+ # @example
20
+ # # from `option :foo, [], as: :bar, optional: :true
21
+ # input = { name: :foo, as: :bar, type: [], optional: true }
22
+ #
23
+ # Dry::Initializer::Dispatcher.call(input)
24
+ # # => {
25
+ # # source: "foo",
26
+ # # target: "bar",
27
+ # # reader: :public,
28
+ # # ivar: "@bar",
29
+ # # type: ->(v) { Array(v) } }, # simplified for brevity
30
+ # # optional: true,
31
+ # # default: -> { Dry::Initializer::UNDEFINED },
32
+ # # }
33
+ #
34
+ # # Settings
35
+ #
36
+ # The module uses global setting `null` to define what value
37
+ # should be set to variables that kept unassigned. By default it
38
+ # uses `Dry::Initializer::UNDEFINED`
39
+ #
40
+ # # Syntax Extensions
41
+ #
42
+ # The module supports syntax extensions. You can add any number
43
+ # of custom dispatchers __on top__ of the stack of default dispatchers.
44
+ # Every dispatcher should be a callable object that takes
45
+ # the source set of options and converts it to another set of options.
46
+ #
47
+ # @example Add special dispatcher
48
+ #
49
+ # # Define a dispatcher for key :integer
50
+ # dispatcher = proc do |integer: false, **opts|
51
+ # opts.merge(type: proc(&:to_i)) if integer
52
+ # end
53
+ #
54
+ # # Register a dispatcher
55
+ # Dry::Initializer::Dispatchers << dispatcher
56
+ #
57
+ # # Now you can use option `integer: true` instead of `type: proc(&:to_i)`
58
+ # class Foo
59
+ # extend Dry::Initializer
60
+ # param :id, integer: true
61
+ # end
62
+ #
63
+ module Dry::Initializer::Dispatchers
64
+ extend self
65
+
66
+ # @!attribute [rw] null Defines a value to be set to unassigned attributes
67
+ # @return [Object]
68
+ attr_accessor :null
69
+
6
70
  #
7
- # Every dispatcher should convert the source hash of options into
8
- # the resulting hash so that you can send additional keys to the helpers.
71
+ # Registers a new dispatcher
9
72
  #
10
- # @example Add special dispatcher
73
+ # @param [#call] dispatcher
74
+ # @return [self] itself
11
75
  #
12
- # # Define a dispatcher for key :integer
13
- # dispatcher = proc do |opts|
14
- # opts.merge(type: proc(&:to_i)) if opts[:integer]
15
- # end
76
+ def <<(dispatcher)
77
+ @pipeline = [dispatcher] + pipeline
78
+ self
79
+ end
80
+
16
81
  #
17
- # # Register a dispatcher
18
- # Dry::Initializer::Dispatchers << dispatcher
82
+ # Normalizes the source set of options
19
83
  #
20
- # # Now you can use option `integer: true` instead of `type: proc(&:to_i)`
21
- # class Foo
22
- # extend Dry::Initializer
23
- # param :id, integer: true
24
- # end
84
+ # @param [Hash<Symbol, Object>] options
85
+ # @return [Hash<Symbol, Objct>] normalized set of options
25
86
  #
26
- module Dispatchers
27
- class << self
28
- def <<(item)
29
- list << item
30
- self
31
- end
87
+ def call(**options)
88
+ options = { null: null, **options }
89
+ pipeline.reduce(options) { |opts, dispatcher| dispatcher.call(**opts) }
90
+ end
32
91
 
33
- def [](options)
34
- list.inject(options) { |opts, item| item.call(opts) }
35
- end
92
+ private
36
93
 
37
- private
94
+ require_relative 'dispatchers/build_nested_type'
95
+ require_relative 'dispatchers/check_type'
96
+ require_relative 'dispatchers/prepare_default'
97
+ require_relative 'dispatchers/prepare_ivar'
98
+ require_relative 'dispatchers/prepare_optional'
99
+ require_relative 'dispatchers/prepare_reader'
100
+ require_relative 'dispatchers/prepare_source'
101
+ require_relative 'dispatchers/prepare_target'
102
+ require_relative 'dispatchers/unwrap_type'
103
+ require_relative 'dispatchers/wrap_type'
38
104
 
39
- def list
40
- @list ||= []
41
- end
42
- end
105
+ def pipeline
106
+ @pipeline ||= [
107
+ PrepareSource, PrepareTarget, PrepareIvar, PrepareReader,
108
+ PrepareDefault, PrepareOptional,
109
+ UnwrapType, CheckType, BuildNestedType, WrapType
110
+ ]
43
111
  end
44
112
  end
@@ -0,0 +1,59 @@
1
+ #
2
+ # Prepare nested data type from a block
3
+ #
4
+ # @example
5
+ # option :foo do
6
+ # option :bar
7
+ # option :qux
8
+ # end
9
+ #
10
+ module Dry::Initializer::Dispatchers::BuildNestedType
11
+ extend self
12
+
13
+ # rubocop: disable Metrics/ParameterLists
14
+ def call(parent:, source:, target:, type: nil, block: nil, **options)
15
+ check_certainty!(source, type, block)
16
+ check_name!(target, block)
17
+ type ||= build_nested_type(parent, target, block)
18
+ { parent: parent, source: source, target: target, type: type, **options }
19
+ end
20
+ # rubocop: enable Metrics/ParameterLists
21
+
22
+ private
23
+
24
+ def check_certainty!(source, type, block)
25
+ return unless block
26
+ return unless type
27
+
28
+ raise ArgumentError, <<~MESSAGE
29
+ You should define coercer of values of argument '#{source}'
30
+ either though the parameter/option, or via nested block, but not the both.
31
+ MESSAGE
32
+ end
33
+
34
+ def check_name!(name, block)
35
+ return unless block
36
+ return unless name[/^_|__|_$/]
37
+
38
+ raise ArgumentError, <<~MESSAGE
39
+ The name of the argument '#{name}' cannot be used for nested struct.
40
+ A proper name can use underscores _ to divide alphanumeric parts only.
41
+ MESSAGE
42
+ end
43
+
44
+ def build_nested_type(parent, name, block)
45
+ return unless block
46
+
47
+ klass_name = full_name(parent, name)
48
+ build_struct(klass_name, block)
49
+ end
50
+
51
+ def full_name(parent, name)
52
+ "::#{parent.name}::#{name.to_s.split("_").compact.map(&:capitalize).join}"
53
+ end
54
+
55
+ def build_struct(klass_name, block)
56
+ eval "class #{klass_name} < Dry::Initializer::Struct; end"
57
+ const_get(klass_name).tap { |klass| klass.class_eval(&block) }
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ #
2
+ # Checks whether an unwrapped type is valid
3
+ #
4
+ module Dry::Initializer::Dispatchers::CheckType
5
+ extend self
6
+
7
+ def call(source:, type: nil, wrap: 0, **options)
8
+ check_if_callable! source, type
9
+ check_arity! source, type, wrap
10
+
11
+ { source: source, type: type, wrap: wrap, **options }
12
+ end
13
+
14
+ private
15
+
16
+ def check_if_callable!(source, type)
17
+ return if type.nil?
18
+ return if type.respond_to?(:call)
19
+
20
+ raise ArgumentError,
21
+ "The type of the argument '#{source}' should be callable"
22
+ end
23
+
24
+ def check_arity!(_source, type, wrap)
25
+ return if type.nil?
26
+ return if wrap.zero?
27
+ return if type.method(:call).arity.abs == 1
28
+
29
+ raise ArgumentError, <<~MESSAGE
30
+ The dry_intitializer supports wrapped types with one argument only.
31
+ You cannot use array types with element coercers having several arguments.
32
+
33
+ For example, this definitions are correct:
34
+ option :foo, [proc(&:to_s)]
35
+ option :bar, type: [[]]
36
+ option :baz, ->(a, b) { [a, b] }
37
+
38
+ While this is not:
39
+ option :foo, [->(a, b) { [a, b] }]
40
+ MESSAGE
41
+ end
42
+ # rubocop: enable Metrics/MethodLength
43
+ end
@@ -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