dry-initializer 3.0.4 → 3.1.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/CHANGELOG.md +12 -0
- data/LICENSE +1 -1
- data/README.md +4 -3
- data/dry-initializer.gemspec +15 -13
- data/lib/dry/initializer/builders/attribute.rb +92 -82
- data/lib/dry/initializer/builders/initializer.rb +56 -54
- data/lib/dry/initializer/builders/reader.rb +55 -49
- data/lib/dry/initializer/builders/signature.rb +29 -23
- data/lib/dry/initializer/builders.rb +9 -5
- data/lib/dry/initializer/config.rb +160 -158
- data/lib/dry/initializer/definition.rb +58 -54
- data/lib/dry/initializer/dispatchers/build_nested_type.rb +54 -40
- data/lib/dry/initializer/dispatchers/check_type.rb +45 -39
- data/lib/dry/initializer/dispatchers/prepare_default.rb +32 -25
- data/lib/dry/initializer/dispatchers/prepare_ivar.rb +13 -6
- data/lib/dry/initializer/dispatchers/prepare_optional.rb +14 -7
- data/lib/dry/initializer/dispatchers/prepare_reader.rb +29 -22
- data/lib/dry/initializer/dispatchers/prepare_source.rb +12 -5
- data/lib/dry/initializer/dispatchers/prepare_target.rb +44 -37
- data/lib/dry/initializer/dispatchers/unwrap_type.rb +21 -10
- data/lib/dry/initializer/dispatchers/wrap_type.rb +24 -17
- data/lib/dry/initializer/dispatchers.rb +48 -43
- data/lib/dry/initializer/dsl.rb +42 -34
- data/lib/dry/initializer/errors.rb +22 -0
- data/lib/dry/initializer/mixin/local.rb +19 -13
- data/lib/dry/initializer/mixin/root.rb +12 -7
- data/lib/dry/initializer/mixin.rb +17 -12
- data/lib/dry/initializer/struct.rb +34 -29
- data/lib/dry/initializer/undefined.rb +7 -1
- data/lib/dry/initializer/version.rb +3 -1
- data/lib/dry/initializer.rb +12 -9
- data/lib/dry-initializer.rb +3 -1
- data/lib/tasks/benchmark.rake +15 -13
- data/lib/tasks/profile.rake +20 -16
- metadata +5 -4
@@ -1,184 +1,186 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Initializer
|
5
|
+
#
|
6
|
+
# Gem-related configuration of some class
|
7
|
+
#
|
8
|
+
class Config
|
9
|
+
# @!attribute [r] null
|
10
|
+
# @return [Dry::Initializer::UNDEFINED, nil] value of unassigned variable
|
11
|
+
|
12
|
+
# @!attribute [r] extended_class
|
13
|
+
# @return [Class] the class whose config collected by current object
|
14
|
+
|
15
|
+
# @!attribute [r] parent
|
16
|
+
# @return [Dry::Initializer::Config] parent configuration
|
17
|
+
|
18
|
+
# @!attribute [r] definitions
|
19
|
+
# @return [Hash<Symbol, Dry::Initializer::Definition>]
|
20
|
+
# hash of attribute definitions with their source names
|
21
|
+
|
22
|
+
attr_reader :null, :extended_class, :parent, :definitions
|
23
|
+
|
24
|
+
# @!attribute [r] mixin
|
25
|
+
# @return [Module] reference to the module to be included into class
|
26
|
+
def mixin
|
27
|
+
@mixin ||= Module.new.tap do |mod|
|
28
|
+
initializer = self
|
29
|
+
mod.extend(Mixin::Local)
|
30
|
+
mod.define_method(:__dry_initializer_config__) do
|
31
|
+
initializer
|
32
|
+
end
|
33
|
+
mod.send :private, :__dry_initializer_config__
|
29
34
|
end
|
30
|
-
mod.send :private, :__dry_initializer_config__
|
31
35
|
end
|
32
|
-
end
|
33
36
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
# List of configs of all subclasses of the [#extended_class]
|
38
|
+
# @return [Array<Dry::Initializer::Config>]
|
39
|
+
def children
|
40
|
+
@children ||= Set.new
|
41
|
+
end
|
39
42
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
# List of definitions for initializer params
|
44
|
+
# @return [Array<Dry::Initializer::Definition>]
|
45
|
+
def params
|
46
|
+
definitions.values.reject(&:option)
|
47
|
+
end
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
49
|
+
# List of definitions for initializer options
|
50
|
+
# @return [Array<Dry::Initializer::Definition>]
|
51
|
+
def options
|
52
|
+
definitions.values.select(&:option)
|
53
|
+
end
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
55
|
+
# Adds or redefines a parameter
|
56
|
+
# @param [Symbol] name
|
57
|
+
# @param [#call, nil] type (nil)
|
58
|
+
# @option opts [Proc] :default
|
59
|
+
# @option opts [Boolean] :optional
|
60
|
+
# @option opts [Symbol] :as
|
61
|
+
# @option opts [true, false, :protected, :public, :private] :reader
|
62
|
+
# @return [self] itself
|
63
|
+
def param(name, type = nil, **opts, &block)
|
64
|
+
add_definition(false, name, type, block, **opts)
|
65
|
+
end
|
63
66
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
67
|
+
# Adds or redefines an option of [#dry_initializer]
|
68
|
+
#
|
69
|
+
# @param (see #param)
|
70
|
+
# @option (see #param)
|
71
|
+
# @return (see #param)
|
72
|
+
#
|
73
|
+
def option(name, type = nil, **opts, &block)
|
74
|
+
add_definition(true, name, type, block, **opts)
|
75
|
+
end
|
73
76
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
77
|
+
# The hash of public attributes for an instance of the [#extended_class]
|
78
|
+
# @param [Dry::Initializer::Instance] instance
|
79
|
+
# @return [Hash<Symbol, Object>]
|
80
|
+
def public_attributes(instance)
|
81
|
+
definitions.values.each_with_object({}) do |item, obj|
|
82
|
+
key = item.target
|
83
|
+
next unless instance.respond_to? key
|
81
84
|
|
82
|
-
|
83
|
-
|
85
|
+
val = instance.send(key)
|
86
|
+
obj[key] = val unless null == val
|
87
|
+
end
|
84
88
|
end
|
85
|
-
end
|
86
89
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
90
|
+
# The hash of assigned attributes for an instance of the [#extended_class]
|
91
|
+
# @param [Dry::Initializer::Instance] instance
|
92
|
+
# @return [Hash<Symbol, Object>]
|
93
|
+
def attributes(instance)
|
94
|
+
definitions.values.each_with_object({}) do |item, obj|
|
95
|
+
key = item.target
|
96
|
+
val = instance.send(:instance_variable_get, item.ivar)
|
97
|
+
obj[key] = val unless null == val
|
98
|
+
end
|
95
99
|
end
|
96
|
-
end
|
97
100
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
101
|
+
# Code of the `#__initialize__` method
|
102
|
+
# @return [String]
|
103
|
+
def code
|
104
|
+
Builders::Initializer[self]
|
105
|
+
end
|
103
106
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
107
|
+
# Finalizes config
|
108
|
+
# @return [self]
|
109
|
+
def finalize
|
110
|
+
@definitions = final_definitions
|
111
|
+
check_order_of_params
|
112
|
+
mixin.class_eval(code)
|
113
|
+
children.each(&:finalize)
|
114
|
+
self
|
115
|
+
end
|
113
116
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
117
|
+
# Human-readable representation of configured params and options
|
118
|
+
# @return [String]
|
119
|
+
def inch
|
120
|
+
line = Builders::Signature[self]
|
121
|
+
line = line.gsub("__dry_initializer_options__", "options")
|
122
|
+
lines = ["@!method initialize(#{line})"]
|
123
|
+
lines += ["Initializes an instance of #{extended_class}"]
|
124
|
+
lines += definitions.values.map(&:inch)
|
125
|
+
lines += ["@return [#{extended_class}]"]
|
126
|
+
lines.join("\n")
|
127
|
+
end
|
125
128
|
|
126
|
-
|
129
|
+
private
|
127
130
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
131
|
+
def initialize(extended_class = nil, null: UNDEFINED)
|
132
|
+
@extended_class = extended_class.tap { |klass| klass&.include mixin }
|
133
|
+
sklass = extended_class&.superclass
|
134
|
+
@parent = sklass.dry_initializer if sklass.is_a? Dry::Initializer
|
135
|
+
@null = null || parent&.null
|
136
|
+
@definitions = {}
|
137
|
+
finalize
|
138
|
+
end
|
136
139
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
end
|
155
|
-
# rubocop: enable Metrics/MethodLength
|
140
|
+
def add_definition(option, name, type, block, **opts)
|
141
|
+
opts = {
|
142
|
+
parent: extended_class,
|
143
|
+
option: option,
|
144
|
+
null: null,
|
145
|
+
source: name,
|
146
|
+
type: type,
|
147
|
+
block: block,
|
148
|
+
**opts
|
149
|
+
}
|
150
|
+
|
151
|
+
options = Dispatchers.call(**opts)
|
152
|
+
definition = Definition.new(**options)
|
153
|
+
definitions[definition.source] = definition
|
154
|
+
finalize
|
155
|
+
mixin.class_eval definition.code
|
156
|
+
end
|
156
157
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
158
|
+
def final_definitions
|
159
|
+
parent_definitions = Hash(parent&.definitions&.dup)
|
160
|
+
definitions.each_with_object(parent_definitions) do |(key, val), obj|
|
161
|
+
obj[key] = check_type(obj[key], val)
|
162
|
+
end
|
161
163
|
end
|
162
|
-
end
|
163
164
|
|
164
|
-
|
165
|
-
|
166
|
-
|
165
|
+
def check_type(previous, current)
|
166
|
+
return current unless previous
|
167
|
+
return current if previous.option == current.option
|
167
168
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
169
|
+
raise SyntaxError,
|
170
|
+
"cannot reload #{previous} of #{extended_class.superclass}" \
|
171
|
+
" by #{current} of its subclass #{extended_class}"
|
172
|
+
end
|
172
173
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
174
|
+
def check_order_of_params
|
175
|
+
params.inject(nil) do |optional, current|
|
176
|
+
if current.optional
|
177
|
+
current
|
178
|
+
elsif optional
|
179
|
+
raise SyntaxError, "#{extended_class}: required #{current}" \
|
180
|
+
" goes after optional #{optional}"
|
181
|
+
else
|
182
|
+
optional
|
183
|
+
end
|
182
184
|
end
|
183
185
|
end
|
184
186
|
end
|
@@ -1,65 +1,69 @@
|
|
1
|
-
|
2
|
-
#
|
3
|
-
# @private
|
4
|
-
# @abstract
|
5
|
-
#
|
6
|
-
# Base class for parameter or option definitions
|
7
|
-
# Defines methods to add corresponding reader to the class,
|
8
|
-
# and build value of instance attribute.
|
9
|
-
#
|
10
|
-
class Definition
|
11
|
-
attr_reader :option, :null, :source, :target, :ivar,
|
12
|
-
:type, :optional, :default, :reader,
|
13
|
-
:desc
|
1
|
+
# frozen_string_literal: true
|
14
2
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
3
|
+
module Dry
|
4
|
+
module Initializer
|
5
|
+
#
|
6
|
+
# @private
|
7
|
+
# @abstract
|
8
|
+
#
|
9
|
+
# Base class for parameter or option definitions
|
10
|
+
# Defines methods to add corresponding reader to the class,
|
11
|
+
# and build value of instance attribute.
|
12
|
+
#
|
13
|
+
class Definition
|
14
|
+
attr_reader :option, :null, :source, :target, :ivar,
|
15
|
+
:type, :optional, :default, :reader,
|
16
|
+
:desc
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
18
|
+
def options
|
19
|
+
{
|
20
|
+
as: target,
|
21
|
+
type: type,
|
22
|
+
optional: optional,
|
23
|
+
default: default,
|
24
|
+
reader: reader,
|
25
|
+
desc: desc
|
26
|
+
}.compact
|
27
|
+
end
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
def name
|
30
|
+
@name ||= "#{option ? "option" : "parameter"} '#{source}'"
|
31
|
+
end
|
32
|
+
alias_method :to_s, :name
|
33
|
+
alias_method :to_str, :name
|
34
|
+
alias_method :inspect, :name
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
def ==(other)
|
37
|
+
other.instance_of?(self.class) && (other.source == source)
|
38
|
+
end
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
text << ' [Object]'
|
44
|
-
text << (option ? " :#{source}" : " #{source}")
|
45
|
-
text << (optional ? ' (optional)' : ' (required)')
|
46
|
-
text << " #{desc}" if desc
|
40
|
+
def code
|
41
|
+
Builders::Reader[self]
|
47
42
|
end
|
48
|
-
end
|
49
43
|
|
50
|
-
|
44
|
+
def inch
|
45
|
+
@inch ||= (option ? "@option" : "@param ").tap do |text|
|
46
|
+
text << " [Object]"
|
47
|
+
text << (option ? " :#{source}" : " #{source}")
|
48
|
+
text << (optional ? " (optional)" : " (required)")
|
49
|
+
text << " #{desc}" if desc
|
50
|
+
end
|
51
|
+
end
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
53
|
+
private
|
54
|
+
|
55
|
+
def initialize(**options)
|
56
|
+
@option = options[:option]
|
57
|
+
@null = options[:null]
|
58
|
+
@source = options[:source]
|
59
|
+
@target = options[:target]
|
60
|
+
@ivar = "@#{@target}"
|
61
|
+
@type = options[:type]
|
62
|
+
@reader = options[:reader]
|
63
|
+
@default = options[:default]
|
64
|
+
@optional = options[:optional]
|
65
|
+
@desc = options[:desc]
|
66
|
+
end
|
63
67
|
end
|
64
68
|
end
|
65
69
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
# Prepare nested data type from a block
|
3
4
|
#
|
4
5
|
# @example
|
@@ -7,53 +8,66 @@
|
|
7
8
|
# option :qux
|
8
9
|
# end
|
9
10
|
#
|
10
|
-
module Dry
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
11
|
+
module Dry
|
12
|
+
module Initializer
|
13
|
+
module Dispatchers
|
14
|
+
module BuildNestedType
|
15
|
+
extend self
|
21
16
|
|
22
|
-
|
17
|
+
# rubocop: disable Metrics/ParameterLists
|
18
|
+
def call(parent:, source:, target:, type: nil, block: nil, **options)
|
19
|
+
check_certainty!(source, type, block)
|
20
|
+
check_name!(target, block)
|
21
|
+
type ||= build_nested_type(parent, target, block)
|
22
|
+
{parent: parent, source: source, target: target, type: type, **options}
|
23
|
+
end
|
24
|
+
# rubocop: enable Metrics/ParameterLists
|
23
25
|
|
24
|
-
|
25
|
-
return unless block
|
26
|
-
return unless type
|
26
|
+
private
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
MESSAGE
|
32
|
-
end
|
28
|
+
def check_certainty!(source, type, block)
|
29
|
+
return unless block
|
30
|
+
return unless type
|
33
31
|
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
raise ArgumentError, <<~MESSAGE
|
33
|
+
You should define coercer of values of argument '#{source}'
|
34
|
+
either though the parameter/option, or via nested block, but not the both.
|
35
|
+
MESSAGE
|
36
|
+
end
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
MESSAGE
|
42
|
-
end
|
38
|
+
def check_name!(name, block)
|
39
|
+
return unless block
|
40
|
+
return unless name[/^_|__|_$/]
|
43
41
|
|
44
|
-
|
45
|
-
|
42
|
+
raise ArgumentError, <<~MESSAGE
|
43
|
+
The name of the argument '#{name}' cannot be used for nested struct.
|
44
|
+
A proper name can use underscores _ to divide alphanumeric parts only.
|
45
|
+
MESSAGE
|
46
|
+
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
end
|
48
|
+
def build_nested_type(parent, name, block)
|
49
|
+
return unless block
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
klass_name = full_name(parent, name)
|
52
|
+
build_struct(klass_name, block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def full_name(parent, name)
|
56
|
+
"::#{parent.name}::#{name.to_s.split("_").compact.map(&:capitalize).join}"
|
57
|
+
end
|
54
58
|
|
55
|
-
|
56
|
-
|
57
|
-
|
59
|
+
def build_struct(klass_name, block)
|
60
|
+
# rubocop: disable Security/Eval
|
61
|
+
# rubocop: disable Style/DocumentDynamicEvalDefinition
|
62
|
+
eval <<~RUBY, TOPLEVEL_BINDING, __FILE__, __LINE__ + 1
|
63
|
+
class #{klass_name} < Dry::Initializer::Struct
|
64
|
+
end
|
65
|
+
RUBY
|
66
|
+
# rubocop: enable Style/DocumentDynamicEvalDefinition
|
67
|
+
# rubocop: enable Security/Eval
|
68
|
+
const_get(klass_name).tap { _1.class_eval(&block) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
58
72
|
end
|
59
73
|
end
|