dry-initializer 2.5.0 → 3.0.4

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