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
@@ -1,9 +1,9 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "dry-initializer"
3
- gem.version = "2.3.0"
3
+ gem.version = "3.0.2"
4
4
  gem.author = ["Vladimir Kochnev (marshall-lee)", "Andrew Kozin (nepalez)"]
5
5
  gem.email = "andrew.kozin@gmail.com"
6
- gem.homepage = "https://github.com/dryrb/dry-initializer"
6
+ gem.homepage = "https://github.com/dry-rb/dry-initializer"
7
7
  gem.summary = "DSL for declaring params and options of the initializer"
8
8
  gem.license = "MIT"
9
9
 
@@ -16,5 +16,5 @@ Gem::Specification.new do |gem|
16
16
  gem.add_development_dependency "rspec", "~> 3.0"
17
17
  gem.add_development_dependency "rake", "> 10"
18
18
  gem.add_development_dependency "dry-types", "> 0.5.1"
19
- gem.add_development_dependency "rubocop", "~> 0.42"
19
+ gem.add_development_dependency "rubocop", "~> 0.49.0"
20
20
  end
@@ -6,14 +6,13 @@ module Dry
6
6
  # DSL for declaring params and options of class initializers
7
7
  #
8
8
  module Initializer
9
- # Singleton for unassigned values
10
- UNDEFINED = Object.new.freeze
11
-
9
+ require_relative "initializer/undefined"
12
10
  require_relative "initializer/dsl"
13
11
  require_relative "initializer/definition"
14
12
  require_relative "initializer/builders"
15
13
  require_relative "initializer/config"
16
14
  require_relative "initializer/mixin"
15
+ require_relative "initializer/dispatchers"
17
16
 
18
17
  # Adds methods [.[]] and [.define]
19
18
  extend DSL
@@ -26,24 +25,25 @@ module Dry
26
25
 
27
26
  # Adds or redefines a parameter of [#dry_initializer]
28
27
  # @param [Symbol] name
29
- # @param [#call, nil] coercer (nil)
30
- # @option opts [#call] :type
28
+ # @param [#call, nil] type (nil)
31
29
  # @option opts [Proc] :default
32
30
  # @option opts [Boolean] :optional
33
31
  # @option opts [Symbol] :as
34
32
  # @option opts [true, false, :protected, :public, :private] :reader
33
+ # @yield block with nested definition
35
34
  # @return [self] itself
36
- def param(name, type = nil, **opts)
37
- dry_initializer.param(name, type, opts)
35
+ def param(name, type = nil, **opts, &block)
36
+ dry_initializer.param(name, type, **opts, &block)
38
37
  self
39
38
  end
40
39
 
41
40
  # Adds or redefines an option of [#dry_initializer]
42
41
  # @param (see #param)
43
42
  # @option (see #param)
43
+ # @yield (see #param)
44
44
  # @return (see #param)
45
- def option(name, type = nil, **opts)
46
- dry_initializer.option(name, type, opts)
45
+ def option(name, type = nil, **opts, &block)
46
+ dry_initializer.option(name, type, **opts, &block)
47
47
  self
48
48
  end
49
49
 
@@ -55,5 +55,7 @@ module Dry
55
55
  klass.send(:instance_variable_set, :@dry_initializer, config)
56
56
  dry_initializer.children << config
57
57
  end
58
+
59
+ require_relative "initializer/struct"
58
60
  end
59
61
  end
@@ -60,22 +60,22 @@ module Dry::Initializer::Builders
60
60
 
61
61
  def default_line
62
62
  return unless @default
63
- "#{@val} = instance_exec(&#{@item}.default) if #{@val} == #{@null}"
63
+ "#{@val} = instance_exec(&#{@item}.default) if #{@null} == #{@val}"
64
64
  end
65
65
 
66
66
  def coercion_line
67
67
  return unless @type
68
68
  arity = @type.is_a?(Proc) ? @type.arity : @type.method(:call).arity
69
69
  if arity.abs == 1
70
- "#{@val} = #{@item}.type.call(#{@val}) unless #{@val} == #{@null}"
70
+ "#{@val} = #{@item}.type.call(#{@val}) unless #{@null} == #{@val}"
71
71
  else
72
- "#{@val} = #{@item}.type.call(#{@val}, self) unless #{@val} == #{@null}"
72
+ "#{@val} = #{@item}.type.call(#{@val}, self) unless #{@null} == #{@val}"
73
73
  end
74
74
  end
75
75
 
76
76
  def assignment_line
77
77
  "#{@ivar} = #{@val}" \
78
- " unless #{@val} == #{@null} && instance_variable_defined?(:#{@ivar})"
78
+ " unless #{@null} == #{@val} && instance_variable_defined?(:#{@ivar})"
79
79
  end
80
80
  end
81
81
  end
@@ -38,7 +38,7 @@ module Dry::Initializer::Builders
38
38
  return unless @null
39
39
  [
40
40
  "def #{@target}",
41
- " #{@ivar} unless #{@ivar} == Dry::Initializer::UNDEFINED",
41
+ " #{@ivar} unless Dry::Initializer::UNDEFINED == #{@ivar}",
42
42
  "end"
43
43
  ]
44
44
  end
@@ -51,15 +51,14 @@ module Dry::Initializer
51
51
 
52
52
  # Adds or redefines a parameter
53
53
  # @param [Symbol] name
54
- # @param [#call, nil] coercer (nil)
55
- # @option opts [#call] :type
54
+ # @param [#call, nil] type (nil)
56
55
  # @option opts [Proc] :default
57
56
  # @option opts [Boolean] :optional
58
57
  # @option opts [Symbol] :as
59
58
  # @option opts [true, false, :protected, :public, :private] :reader
60
59
  # @return [self] itself
61
- def param(name, type = nil, **opts)
62
- add_definition(false, name, type, opts)
60
+ def param(name, type = nil, **opts, &block)
61
+ add_definition(false, name, type, block, **opts)
63
62
  end
64
63
 
65
64
  # Adds or redefines an option of [#dry_initializer]
@@ -68,8 +67,8 @@ module Dry::Initializer
68
67
  # @option (see #param)
69
68
  # @return (see #param)
70
69
  #
71
- def option(name, type = nil, **opts)
72
- add_definition(true, name, type, opts)
70
+ def option(name, type = nil, **opts, &block)
71
+ add_definition(true, name, type, block, **opts)
73
72
  end
74
73
 
75
74
  # The hash of public attributes for an instance of the [#extended_class]
@@ -80,7 +79,7 @@ module Dry::Initializer
80
79
  key = item.target
81
80
  next unless instance.respond_to? key
82
81
  val = instance.send(key)
83
- obj[key] = val unless val == null
82
+ obj[key] = val unless null == val
84
83
  end
85
84
  end
86
85
 
@@ -91,7 +90,7 @@ module Dry::Initializer
91
90
  definitions.values.each_with_object({}) do |item, obj|
92
91
  key = item.target
93
92
  val = instance.send(:instance_variable_get, item.ivar)
94
- obj[key] = val unless val == null
93
+ obj[key] = val unless null == val
95
94
  end
96
95
  end
97
96
 
@@ -134,13 +133,25 @@ module Dry::Initializer
134
133
  finalize
135
134
  end
136
135
 
137
- def add_definition(option, name, type, opts)
138
- definition = Definition.new(option, null, name, type, opts)
136
+ # rubocop: disable Metrics/MethodLength
137
+ def add_definition(option, name, type, block, **opts)
138
+ opts = {
139
+ parent: extended_class,
140
+ option: option,
141
+ null: null,
142
+ source: name,
143
+ type: type,
144
+ block: block,
145
+ **opts
146
+ }
147
+
148
+ options = Dispatchers.call(**opts)
149
+ definition = Definition.new(**options)
139
150
  definitions[definition.source] = definition
140
151
  finalize
141
-
142
152
  mixin.class_eval definition.code
143
153
  end
154
+ # rubocop: enable Metrics/MethodLength
144
155
 
145
156
  def final_definitions
146
157
  parent_definitions = Hash(parent&.definitions&.dup)
@@ -49,68 +49,17 @@ module Dry::Initializer
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
@@ -0,0 +1,112 @@
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
+
70
+ #
71
+ # Registers a new dispatcher
72
+ #
73
+ # @param [#call] dispatcher
74
+ # @return [self] itself
75
+ #
76
+ def <<(dispatcher)
77
+ @pipeline = [dispatcher] + pipeline
78
+ self
79
+ end
80
+
81
+ #
82
+ # Normalizes the source set of options
83
+ #
84
+ # @param [Hash<Symbol, Object>] options
85
+ # @return [Hash<Symbol, Objct>] normalized set of options
86
+ #
87
+ def call(**options)
88
+ options = { null: null, **options }
89
+ pipeline.reduce(options) { |opts, dispatcher| dispatcher.call(**opts) }
90
+ end
91
+
92
+ private
93
+
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"
104
+
105
+ def pipeline
106
+ @pipeline ||= [
107
+ PrepareSource, PrepareTarget, PrepareIvar, PrepareReader,
108
+ PrepareDefault, PrepareOptional,
109
+ UnwrapType, CheckType, BuildNestedType, WrapType
110
+ ]
111
+ end
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