dry-initializer 0.11.0 → 1.0.0

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +18 -11
  4. data/CHANGELOG.md +85 -43
  5. data/benchmarks/options.rb +4 -4
  6. data/benchmarks/with_types.rb +2 -4
  7. data/benchmarks/with_types_and_defaults.rb +2 -4
  8. data/dry-initializer.gemspec +1 -1
  9. data/lib/dry/initializer.rb +77 -13
  10. data/lib/dry/initializer/attribute.rb +52 -0
  11. data/lib/dry/initializer/builder.rb +79 -72
  12. data/lib/dry/initializer/exceptions/default_value_error.rb +8 -0
  13. data/lib/dry/initializer/exceptions/params_order_error.rb +8 -0
  14. data/lib/dry/initializer/exceptions/type_constraint_error.rb +7 -0
  15. data/lib/dry/initializer/option.rb +55 -0
  16. data/lib/dry/initializer/param.rb +49 -0
  17. data/spec/missed_default_spec.rb +2 -2
  18. data/spec/optional_spec.rb +16 -6
  19. data/spec/options_var_spec.rb +39 -0
  20. data/spec/repetitive_definitions_spec.rb +38 -18
  21. data/spec/type_constraint_spec.rb +3 -3
  22. metadata +10 -18
  23. data/lib/dry/initializer/errors.rb +0 -10
  24. data/lib/dry/initializer/errors/default_value_error.rb +0 -6
  25. data/lib/dry/initializer/errors/order_error.rb +0 -7
  26. data/lib/dry/initializer/errors/plugin_error.rb +0 -6
  27. data/lib/dry/initializer/errors/redefinition_error.rb +0 -5
  28. data/lib/dry/initializer/errors/type_constraint_error.rb +0 -5
  29. data/lib/dry/initializer/mixin.rb +0 -77
  30. data/lib/dry/initializer/plugins.rb +0 -10
  31. data/lib/dry/initializer/plugins/base.rb +0 -47
  32. data/lib/dry/initializer/plugins/default_proc.rb +0 -28
  33. data/lib/dry/initializer/plugins/signature.rb +0 -28
  34. data/lib/dry/initializer/plugins/type_constraint.rb +0 -21
  35. data/lib/dry/initializer/plugins/variable_setter.rb +0 -30
  36. data/lib/dry/initializer/signature.rb +0 -61
  37. data/spec/plugin_registry_spec.rb +0 -45
@@ -1,22 +1,86 @@
1
1
  module Dry
2
- # Declares arguments of the initializer (params and options)
3
- #
4
- # @api public
5
- #
6
2
  module Initializer
7
- require_relative "initializer/errors"
8
- require_relative "initializer/plugins"
9
- require_relative "initializer/signature"
3
+ require_relative "initializer/exceptions/default_value_error"
4
+ require_relative "initializer/exceptions/type_constraint_error"
5
+ require_relative "initializer/exceptions/params_order_error"
6
+
7
+ require_relative "initializer/attribute"
8
+ require_relative "initializer/param"
9
+ require_relative "initializer/option"
10
10
  require_relative "initializer/builder"
11
- require_relative "initializer/mixin"
12
11
 
13
- UNDEFINED = Object.new.freeze
12
+ # rubocop: disable Style/ConstantName
13
+ Mixin = self # for compatibility to versions below 0.12
14
+ # rubocop: enable Style/ConstantName
15
+
16
+ UNDEFINED = Object.new.tap do |obj|
17
+ obj.define_singleton_method(:inspect) { "Dry::Initializer::UNDEFINED" }
18
+ end.freeze
19
+
20
+ class << self
21
+ def extended(klass)
22
+ super
23
+ mixin = klass.send(:__initializer_mixin__)
24
+ builder = klass.send(:__initializer_builder__)
25
+
26
+ builder.call(mixin)
27
+ klass.include(mixin)
28
+ klass.send(:define_method, :initialize) do |*args|
29
+ __initialize__(*args)
30
+ end
31
+ end
32
+
33
+ def define(fn = nil, &block)
34
+ mixin = Module.new do
35
+ def initialize(*args)
36
+ __initialize__(*args)
37
+ end
38
+ end
14
39
 
15
- def self.define(proc = nil, &block)
16
- Module.new do |container|
17
- container.extend Mixin
18
- container.instance_exec(&(proc || block))
40
+ builder = Builder.new
41
+ builder.instance_exec(&(fn || block))
42
+ builder.call(mixin)
43
+ mixin
19
44
  end
45
+
46
+ def mixin(fn = nil, &block)
47
+ define(fn, &block)
48
+ end
49
+ end
50
+
51
+ def param(*args)
52
+ __initializer_builder__.param(*args).call(__initializer_mixin__)
53
+ end
54
+
55
+ def option(*args)
56
+ __initializer_builder__.option(*args).call(__initializer_mixin__)
57
+ end
58
+
59
+ private
60
+
61
+ def __initializer_mixin__
62
+ @__initializer_mixin__ ||= Module.new do
63
+ def initialize(*args)
64
+ __initialize__(*args)
65
+ end
66
+ end
67
+ end
68
+
69
+ def __initializer_builder__
70
+ @__initializer_builder__ ||= Dry::Initializer::Builder.new
71
+ end
72
+
73
+ def inherited(klass)
74
+ builder = @__initializer_builder__.dup
75
+ mixin = Module.new
76
+
77
+ klass.instance_variable_set :@__initializer_builder__, builder
78
+ klass.instance_variable_set :@__initializer_mixin__, mixin
79
+
80
+ builder.call(mixin)
81
+ klass.include mixin
82
+
83
+ super
20
84
  end
21
85
  end
22
86
  end
@@ -0,0 +1,52 @@
1
+ module Dry::Initializer
2
+ # Contains definitions for a single attribute, and builds its parts of mixin
3
+ class Attribute
4
+ attr_reader :source, :target, :coercer, :default, :optional, :reader
5
+
6
+ # definition for the getter method
7
+ def getter
8
+ return unless reader
9
+ command = %w(private protected).include?(reader.to_s) ? reader : :public
10
+
11
+ <<-RUBY.gsub(/^ *\|/, "")
12
+ |def #{target}
13
+ | @#{target} unless @#{target} == Dry::Initializer::UNDEFINED
14
+ |end
15
+ |#{command} :#{target}
16
+ RUBY
17
+ end
18
+
19
+ private
20
+
21
+ def initialize(source, coercer = nil, **options)
22
+ @source = source
23
+ @target = options.fetch(:as, source)
24
+ @coercer = coercer || options[:type]
25
+ @reader = options.fetch(:reader, :public)
26
+ @default = options[:default]
27
+ @optional = !!(options[:optional] || @default)
28
+ validate
29
+ end
30
+
31
+ def validate
32
+ validate_target
33
+ validate_default
34
+ validate_coercer
35
+ end
36
+
37
+ def validate_target
38
+ return if target =~ /\A\w+\Z/
39
+ fail ArgumentError.new("Invalid name '#{target}' for the target variable")
40
+ end
41
+
42
+ def validate_default
43
+ return if default.nil? || default.is_a?(Proc)
44
+ fail DefaultValueError.new(source, default)
45
+ end
46
+
47
+ def validate_coercer
48
+ return if coercer.nil? || coercer.respond_to?(:call)
49
+ fail TypeConstraintError.new(source, coercer)
50
+ end
51
+ end
52
+ end
@@ -1,100 +1,107 @@
1
- require "set"
2
1
  module Dry::Initializer
3
- # Rebuilds the initializer every time a new argument defined
4
- #
5
- # @api private
6
- #
7
2
  class Builder
8
- include Plugins
3
+ def param(*args)
4
+ @params = insert(@params, Param, *args)
5
+ validate_collections
6
+ end
9
7
 
10
- def initialize
11
- @signature = Signature.new
12
- @plugins = Set.new [VariableSetter, TypeConstraint, DefaultProc]
13
- @parts = []
8
+ def option(*args)
9
+ @options = insert(@options, Option, *args)
10
+ validate_collections
11
+ end
12
+
13
+ def call(mixin)
14
+ defaults = send(:defaults)
15
+ coercers = send(:coercers)
16
+ mixin.send(:define_method, :__defaults__) { defaults }
17
+ mixin.send(:define_method, :__coercers__) { coercers }
18
+ mixin.class_eval(code, __FILE__, __LINE__ + 1)
14
19
  end
15
20
 
16
- # Register new plugin to be applied as a chunk of code, or a proc
17
- # to be evaluated in the instance's scope
18
- #
19
- # @param [Dry::Initializer::Plugin]
20
- #
21
- # @return [Dry::Initializer::Builder]
22
- #
23
- def register(plugin)
24
- plugins = @plugins + [plugin]
25
- copy { @plugins = plugins }
21
+ private
22
+
23
+ def initialize
24
+ @params = []
25
+ @options = []
26
26
  end
27
27
 
28
- # Defines new agrument and reloads mixin definitions
29
- #
30
- # @param [#to_sym] name
31
- # @param [Hash<Symbol, Object>] settings
32
- #
33
- # @return [Dry::Initializer::Builder]
34
- #
35
- def define(name, settings)
36
- signature = @signature.add(name, settings)
37
- parts = @parts + @plugins.map { |p| p.call(name, settings) }.compact
38
-
39
- copy do
40
- @signature = signature
41
- @parts = parts
28
+ def insert(collection, klass, source, *args)
29
+ index = collection.index { |option| option.source == source.to_s }
30
+
31
+ if index
32
+ new_item = klass.new(source, *args)
33
+ collection.dup.tap { |list| list[index] = new_item }
34
+ else
35
+ new_item = klass.new(source, *args)
36
+ collection + [new_item]
42
37
  end
43
38
  end
44
39
 
45
- # Redeclares initializer and readers in the mixin module
46
- #
47
- # @param [Module] mixin
48
- #
49
- def call(mixin)
50
- define_readers(mixin)
51
- reload_initializer(mixin)
52
- reload_callback(mixin)
53
- mixin
40
+ def code
41
+ <<-RUBY.gsub(/^ +\|/, "")
42
+ |def __initialize__(#{initializer_signatures})
43
+ | @__options__ = __options__
44
+ |#{initializer_presetters}
45
+ |#{initializer_setters}
46
+ |end
47
+ |private :__initialize__
48
+ |private :__defaults__
49
+ |private :__coercers__
50
+ |
51
+ |#{getters}
52
+ RUBY
54
53
  end
55
54
 
56
- private
55
+ def attributes
56
+ @params + @options
57
+ end
57
58
 
58
- def copy(&block)
59
- dup.tap { |instance| instance.instance_eval(&block) }
59
+ def initializer_signatures
60
+ sig = @params.map(&:initializer_signature).compact.uniq
61
+ sig << (@options.any? ? "**__options__" : "__options__ = {}")
62
+ sig.join(", ")
60
63
  end
61
64
 
62
- def define_readers(mixin)
63
- define_reader(
64
- mixin,
65
- :attr_reader,
66
- ->(item) { item.settings[:reader] != false }
67
- )
68
- define_reader mixin, :private
69
- define_reader mixin, :protected
65
+ def initializer_presetters
66
+ attributes.map(&:initializer_presetter)
67
+ .compact
68
+ .uniq
69
+ .map { |line| " #{line}" }
70
+ .join("\n")
70
71
  end
71
72
 
72
- def define_reader(mixin, method, filter_lambda = nil)
73
- filter_lambda ||= ->(item) { item.settings[:reader] == method }
74
- readers = @signature.select(&filter_lambda).map(&:rename)
75
- mixin.send method, *readers if readers.any?
73
+ def initializer_setters
74
+ attributes.map(&:initializer_setter)
75
+ .compact
76
+ .uniq
77
+ .map { |text| text.lines.map { |line| " #{line}" }.join }
78
+ .join("\n")
76
79
  end
77
80
 
78
- def reload_initializer(mixin)
79
- mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
80
- def __initialize__(#{@signature.call})
81
- @__options__ = __options__
82
- #{@parts.select { |part| String === part }.join("\n")}
83
- __after_initialize__
84
- end
85
- RUBY
81
+ def getters
82
+ attributes.map(&:getter).compact.uniq.join("\n")
83
+ end
84
+
85
+ def defaults
86
+ attributes.map(&:default_hash).reduce({}, :merge)
87
+ end
86
88
 
87
- mixin.send :private, :__initialize__
89
+ def coercers
90
+ attributes.map(&:coercer_hash).reduce({}, :merge)
88
91
  end
89
92
 
90
- def reload_callback(mixin)
91
- blocks = @parts.select { |part| Proc === part }
93
+ def validate_collections
94
+ optional_param = nil
92
95
 
93
- mixin.send :define_method, :__after_initialize__ do
94
- blocks.each { |block| instance_eval(&block) }
96
+ @params.each do |param|
97
+ if param.optional
98
+ optional_param = param.source if param.optional
99
+ elsif optional_param
100
+ fail ParamsOrderError.new(param.source, optional_param)
101
+ end
95
102
  end
96
103
 
97
- mixin.send :private, :__after_initialize__
104
+ self
98
105
  end
99
106
  end
100
107
  end
@@ -0,0 +1,8 @@
1
+ module Dry::Initializer
2
+ class DefaultValueError < TypeError
3
+ def initialize(name, value)
4
+ super "Cannot set #{value.inspect} directly as a default value" \
5
+ " of the argument '#{name}'. Wrap it to either proc or lambda."
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Dry::Initializer
2
+ class ParamsOrderError < SyntaxError
3
+ def initialize(required, optional)
4
+ super "Optional param '#{optional}'" \
5
+ " should not preceed required '#{required}'"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Dry::Initializer
2
+ class TypeConstraintError < TypeError
3
+ def initialize(name, type)
4
+ super "#{type} constraint for argument '#{name}' doesn't respond to #call"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ module Dry::Initializer
2
+ class Option < Attribute
3
+ # part of __initializer__ definition
4
+ def initializer_signature
5
+ "**__options__"
6
+ end
7
+
8
+ # part of __initializer__ body
9
+ def initializer_presetter
10
+ "@#{target} = Dry::Initializer::UNDEFINED"
11
+ end
12
+
13
+ # part of __initializer__ body
14
+ def initializer_setter
15
+ "#{setter_part}#{maybe_optional}"
16
+ end
17
+
18
+ # part of __defaults__
19
+ def default_hash
20
+ default ? { :"option_#{source}" => default } : {}
21
+ end
22
+
23
+ # part of __coercers__
24
+ def coercer_hash
25
+ coercer ? { :"option_#{source}" => coercer } : {}
26
+ end
27
+
28
+ private
29
+
30
+ def maybe_optional
31
+ " if __options__.key? :'#{source}'" if optional && !default
32
+ end
33
+
34
+ def setter_part
35
+ "@#{target} = #{maybe_coerced}"
36
+ end
37
+
38
+ def maybe_coerced
39
+ return maybe_default unless coercer
40
+ "__coercers__[:'option_#{source}'].call(#{maybe_default})"
41
+ end
42
+
43
+ def maybe_default
44
+ "__options__.fetch(:'#{source}') { #{default_part} }"
45
+ end
46
+
47
+ def default_part
48
+ if default
49
+ "instance_eval(&__defaults__[:'option_#{source}'])"
50
+ else
51
+ "raise ArgumentError, \"option :'#{source}' is required\""
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,49 @@
1
+ module Dry::Initializer
2
+ class Param < Attribute
3
+ # part of __initializer__ definition
4
+ def initializer_signature
5
+ optional ? "#{target} = Dry::Initializer::UNDEFINED" : target
6
+ end
7
+
8
+ # part of __initializer__ body
9
+ def initializer_presetter; end
10
+
11
+ # part of __initializer__ body
12
+ def initializer_setter
13
+ "@#{target} = #{maybe_coerced}"
14
+ end
15
+
16
+ # part of __defaults__
17
+ def default_hash
18
+ default ? { :"param_#{target}" => default } : {}
19
+ end
20
+
21
+ # part of __coercers__
22
+ def coercer_hash
23
+ coercer ? { :"param_#{target}" => coercer } : {}
24
+ end
25
+
26
+ private
27
+
28
+ def initialize(*args, **options)
29
+ fail ArgumentError.new("Do not rename params") if options.key? :as
30
+ super
31
+ end
32
+
33
+ def maybe_coerced
34
+ return maybe_default unless coercer
35
+ "__coercers__[:param_#{target}].call(#{maybe_default})"
36
+ end
37
+
38
+ def maybe_default
39
+ "#{target}#{default_part}"
40
+ end
41
+
42
+ def default_part
43
+ return unless default
44
+ " == Dry::Initializer::UNDEFINED ?" \
45
+ " instance_eval(&__defaults__[:param_#{target}]) :" \
46
+ " #{target}"
47
+ end
48
+ end
49
+ end