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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +18 -11
- data/CHANGELOG.md +85 -43
- data/benchmarks/options.rb +4 -4
- data/benchmarks/with_types.rb +2 -4
- data/benchmarks/with_types_and_defaults.rb +2 -4
- data/dry-initializer.gemspec +1 -1
- data/lib/dry/initializer.rb +77 -13
- data/lib/dry/initializer/attribute.rb +52 -0
- data/lib/dry/initializer/builder.rb +79 -72
- data/lib/dry/initializer/exceptions/default_value_error.rb +8 -0
- data/lib/dry/initializer/exceptions/params_order_error.rb +8 -0
- data/lib/dry/initializer/exceptions/type_constraint_error.rb +7 -0
- data/lib/dry/initializer/option.rb +55 -0
- data/lib/dry/initializer/param.rb +49 -0
- data/spec/missed_default_spec.rb +2 -2
- data/spec/optional_spec.rb +16 -6
- data/spec/options_var_spec.rb +39 -0
- data/spec/repetitive_definitions_spec.rb +38 -18
- data/spec/type_constraint_spec.rb +3 -3
- metadata +10 -18
- data/lib/dry/initializer/errors.rb +0 -10
- data/lib/dry/initializer/errors/default_value_error.rb +0 -6
- data/lib/dry/initializer/errors/order_error.rb +0 -7
- data/lib/dry/initializer/errors/plugin_error.rb +0 -6
- data/lib/dry/initializer/errors/redefinition_error.rb +0 -5
- data/lib/dry/initializer/errors/type_constraint_error.rb +0 -5
- data/lib/dry/initializer/mixin.rb +0 -77
- data/lib/dry/initializer/plugins.rb +0 -10
- data/lib/dry/initializer/plugins/base.rb +0 -47
- data/lib/dry/initializer/plugins/default_proc.rb +0 -28
- data/lib/dry/initializer/plugins/signature.rb +0 -28
- data/lib/dry/initializer/plugins/type_constraint.rb +0 -21
- data/lib/dry/initializer/plugins/variable_setter.rb +0 -30
- data/lib/dry/initializer/signature.rb +0 -61
- data/spec/plugin_registry_spec.rb +0 -45
data/lib/dry/initializer.rb
CHANGED
@@ -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/
|
8
|
-
require_relative "initializer/
|
9
|
-
require_relative "initializer/
|
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
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
3
|
+
def param(*args)
|
4
|
+
@params = insert(@params, Param, *args)
|
5
|
+
validate_collections
|
6
|
+
end
|
9
7
|
|
10
|
-
def
|
11
|
-
@
|
12
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
55
|
+
def attributes
|
56
|
+
@params + @options
|
57
|
+
end
|
57
58
|
|
58
|
-
def
|
59
|
-
|
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
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
89
|
+
def coercers
|
90
|
+
attributes.map(&:coercer_hash).reduce({}, :merge)
|
88
91
|
end
|
89
92
|
|
90
|
-
def
|
91
|
-
|
93
|
+
def validate_collections
|
94
|
+
optional_param = nil
|
92
95
|
|
93
|
-
|
94
|
-
|
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
|
-
|
104
|
+
self
|
98
105
|
end
|
99
106
|
end
|
100
107
|
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
|