dry-initializer 0.11.0 → 1.0.0

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