object_forge 0.3.0 → 0.4.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/README.md +139 -60
- data/lib/object_forge/crucible.rb +49 -13
- data/lib/object_forge/forge.rb +110 -40
- data/lib/object_forge/forge_dsl.rb +42 -37
- data/lib/object_forge/forgeyard.rb +13 -12
- data/lib/object_forge/molds/hash_mold.rb +4 -4
- data/lib/object_forge/molds/keywords_mold.rb +5 -5
- data/lib/object_forge/molds/single_argument_mold.rb +5 -5
- data/lib/object_forge/molds/struct_mold.rb +17 -13
- data/lib/object_forge/molds/wrapped_mold.rb +2 -2
- data/lib/object_forge/molds.rb +78 -25
- data/lib/object_forge/sequence.rb +2 -2
- data/lib/object_forge/version.rb +1 -1
- data/lib/object_forge.rb +96 -34
- data/sig/object_forge/molds.rbs +4 -4
- data/sig/object_forge.rbs +41 -15
- metadata +16 -11
data/lib/object_forge/forge.rb
CHANGED
|
@@ -5,7 +5,12 @@ require_relative "forge_dsl"
|
|
|
5
5
|
require_relative "molds"
|
|
6
6
|
|
|
7
7
|
module ObjectForge
|
|
8
|
-
# Object
|
|
8
|
+
# Object building forge.
|
|
9
|
+
#
|
|
10
|
+
# Usually created through {.define} or {Forgeyard#define} using {ForgeDSL}.
|
|
11
|
+
# Alternatively, can be directly initalized with {Parameters} if you prefer not using the DSL.
|
|
12
|
+
#
|
|
13
|
+
# Then, {#forge} can be called to build instances of {#forge_target}.
|
|
9
14
|
#
|
|
10
15
|
# @since 0.1.0
|
|
11
16
|
class Forge
|
|
@@ -14,62 +19,72 @@ module ObjectForge
|
|
|
14
19
|
# through means other than {ForgeDSL}.
|
|
15
20
|
#
|
|
16
21
|
# @!attribute [r] attributes
|
|
17
|
-
#
|
|
18
|
-
# @return [Hash{Symbol => Any}]
|
|
22
|
+
# Default values of the attributes.
|
|
23
|
+
# @return [Hash{Symbol => Proc, Any}]
|
|
19
24
|
#
|
|
20
25
|
# @!attribute [r] traits
|
|
21
26
|
# Attributes belonging to traits.
|
|
22
|
-
# @return [Hash{Symbol => Hash{Symbol => Any}}]
|
|
23
|
-
#
|
|
24
|
-
# @!attribute [r]
|
|
25
|
-
# A forge's
|
|
26
|
-
#
|
|
27
|
-
#
|
|
27
|
+
# @return [Hash{Symbol => Hash{Symbol => Proc, Any}}]
|
|
28
|
+
#
|
|
29
|
+
# @!attribute [r] options
|
|
30
|
+
# A forge's options.
|
|
31
|
+
# Known options:
|
|
32
|
+
# - +:mold+ — a +call+able object that knows how to build the instance,
|
|
33
|
+
# taking a class and a hash of attributes.
|
|
34
|
+
# - +:crucible+ — a +call+able object that knows how to resolve attributes,
|
|
35
|
+
# taking a hash of initial attributes.
|
|
36
|
+
# - +:after_forge+/+:after_build+ — a +call+able object that is passed
|
|
37
|
+
# the forged instance and can do anything with it.
|
|
28
38
|
# @since 0.3.0
|
|
29
39
|
# @return [Hash{Symbol => Any}]
|
|
30
|
-
Parameters = Struct.new(:attributes, :traits, :
|
|
40
|
+
Parameters = Struct.new(:attributes, :traits, :options, keyword_init: true)
|
|
31
41
|
|
|
32
|
-
# Define (and
|
|
42
|
+
# Define (and initialize) a forge using DSL.
|
|
33
43
|
#
|
|
34
44
|
# @see ForgeDSL
|
|
35
45
|
# @thread_safety Thread-safe if DSL definition is thread-safe.
|
|
36
46
|
#
|
|
37
|
-
# @param
|
|
47
|
+
# @param forge_target [Class, Any] class or object to forge
|
|
38
48
|
# @param name [Symbol, nil] forge name
|
|
39
|
-
# @yieldparam
|
|
49
|
+
# @yieldparam dsl [ForgeDSL]
|
|
40
50
|
# @yieldreturn [void]
|
|
41
51
|
# @return [Forge] forge
|
|
42
|
-
def self.define(
|
|
43
|
-
new(
|
|
52
|
+
def self.define(forge_target, name: nil, &)
|
|
53
|
+
new(forge_target, ForgeDSL.new(&), name:)
|
|
44
54
|
end
|
|
45
55
|
|
|
46
|
-
# @return [Symbol, nil] forge name
|
|
56
|
+
# @return [Symbol, nil] forge name, only used for identification purposes
|
|
47
57
|
attr_reader :name
|
|
48
58
|
|
|
49
59
|
# @return [Class, Any] class or object to forge
|
|
50
|
-
|
|
60
|
+
# @since 0.4.0
|
|
61
|
+
attr_reader :forge_target
|
|
62
|
+
alias target forge_target
|
|
51
63
|
|
|
52
64
|
# @return [Parameters, ForgeDSL] forge parameters
|
|
53
65
|
attr_reader :parameters
|
|
54
66
|
|
|
55
|
-
# @param
|
|
67
|
+
# @param forge_target [Class, Any] class or object to forge,
|
|
68
|
+
# will be passed to mold as +forge_target+ argument
|
|
56
69
|
# @param parameters [Parameters, ForgeDSL] forge parameters
|
|
57
|
-
# @param name [Symbol, nil] forge name
|
|
58
|
-
#
|
|
59
|
-
|
|
70
|
+
# @param name [Symbol, nil] forge name
|
|
71
|
+
#
|
|
72
|
+
# @raise [ObjectInterfaceError] if forge options do not have expected interface;
|
|
73
|
+
# see {Parameters#options} for details
|
|
74
|
+
def initialize(forge_target, parameters, name: nil)
|
|
60
75
|
@name = name
|
|
61
|
-
@
|
|
76
|
+
@forge_target = forge_target
|
|
62
77
|
@parameters = parameters
|
|
63
|
-
|
|
78
|
+
|
|
79
|
+
options = @parameters.options
|
|
80
|
+
@crucible = determine_crucible(options)
|
|
81
|
+
@mold = determine_mold(forge_target, options)
|
|
82
|
+
@after_forge_hook = determine_after_forge_hook(options)
|
|
64
83
|
end
|
|
65
84
|
|
|
66
|
-
# Forge a new instance.
|
|
85
|
+
# Forge a new instance, applying attributes to forge target.
|
|
67
86
|
#
|
|
68
|
-
#
|
|
69
|
-
# @overload forge(traits, overrides, &)
|
|
70
|
-
#
|
|
71
|
-
# Positional arguments are taken as trait names, keyword arguments as attribute overrides,
|
|
72
|
-
# unless there are exactly two positional arguments: an array and a hash.
|
|
87
|
+
# Positional arguments are taken as trait names, keyword arguments as attribute overrides.
|
|
73
88
|
#
|
|
74
89
|
# All traits and overrides are applied in argument order,
|
|
75
90
|
# with overrides always applied after traits.
|
|
@@ -80,13 +95,16 @@ module ObjectForge
|
|
|
80
95
|
# +traits+ and +overrides+ are thread-safe.
|
|
81
96
|
#
|
|
82
97
|
# @param traits [Array<Symbol>] traits to apply
|
|
83
|
-
# @param overrides [Hash{Symbol => Any}] attribute overrides
|
|
98
|
+
# @param overrides [Hash{Symbol => Proc, Any}] attribute overrides
|
|
84
99
|
# @yieldparam object [Any] forged instance
|
|
85
100
|
# @yieldreturn [void]
|
|
86
|
-
# @return [Any]
|
|
101
|
+
# @return [Any] forged instance
|
|
102
|
+
#
|
|
103
|
+
# @raise [ArgumentError] if a trait name is unknown
|
|
87
104
|
def forge(*traits, **overrides)
|
|
88
105
|
resolved_attributes = resolve_attributes(traits, overrides)
|
|
89
|
-
instance = @mold.call(
|
|
106
|
+
instance = @mold.call(forge_target: @forge_target, attributes: resolved_attributes)
|
|
107
|
+
@after_forge_hook&.call(instance)
|
|
90
108
|
yield instance if block_given?
|
|
91
109
|
instance
|
|
92
110
|
end
|
|
@@ -96,31 +114,83 @@ module ObjectForge
|
|
|
96
114
|
|
|
97
115
|
private
|
|
98
116
|
|
|
117
|
+
# Get a crucible object based on parameters.
|
|
118
|
+
#
|
|
119
|
+
# It's either the object provided in options, or {Crucible}.
|
|
120
|
+
#
|
|
121
|
+
# @param options [Hash]
|
|
122
|
+
# @option options [#call, nil] :crucible
|
|
123
|
+
# @return [#call]
|
|
124
|
+
#
|
|
125
|
+
# @raise [ObjectInterfaceError]
|
|
126
|
+
#
|
|
127
|
+
# @since 0.4.0
|
|
128
|
+
def determine_crucible(options)
|
|
129
|
+
crucible = options[:crucible] || Crucible
|
|
130
|
+
|
|
131
|
+
unless crucible.respond_to?(:call)
|
|
132
|
+
raise ObjectInterfaceError, "crucible must respond to #call"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
crucible
|
|
136
|
+
end
|
|
137
|
+
|
|
99
138
|
# Get appropriate mold based on parameters.
|
|
100
139
|
#
|
|
101
140
|
# If +mold+ is already set, it will be used directly, or,
|
|
102
141
|
# if it is Class, it will be wrapped in {Molds::WrappedMold} if posssible.
|
|
103
|
-
# If +nil+, a mold will be selected based on +
|
|
142
|
+
# If +nil+, a mold will be selected based on +forge_target+ class.
|
|
104
143
|
#
|
|
105
|
-
# @param
|
|
106
|
-
# @param
|
|
144
|
+
# @param forge_target [Class, Any]
|
|
145
|
+
# @param options [Hash]
|
|
146
|
+
# @option options [#call, Class, nil] :mold
|
|
107
147
|
# @return [#call]
|
|
108
148
|
#
|
|
109
|
-
# @raise [
|
|
149
|
+
# @raise [ObjectInterfaceError]
|
|
110
150
|
#
|
|
111
151
|
# @since 0.3.0
|
|
112
|
-
def determine_mold(
|
|
113
|
-
Molds.wrap_mold(mold) || Molds.mold_for(
|
|
152
|
+
def determine_mold(forge_target, options)
|
|
153
|
+
Molds.wrap_mold(options[:mold]) || Molds.mold_for(forge_target)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Get after-forge hook if specified.
|
|
157
|
+
#
|
|
158
|
+
# Both +:after_forge+ and +:after_build+ are accepted, but +:after_forge+
|
|
159
|
+
# wins if both are present.
|
|
160
|
+
#
|
|
161
|
+
# @param options [Hash]
|
|
162
|
+
# @option options [#call, nil] :after_forge
|
|
163
|
+
# @option options [#call, nil] :after_build
|
|
164
|
+
# @return [#call, nil]
|
|
165
|
+
#
|
|
166
|
+
# @raise [ObjectInterfaceError]
|
|
167
|
+
#
|
|
168
|
+
# @since 0.4.0
|
|
169
|
+
def determine_after_forge_hook(options)
|
|
170
|
+
hook = options[:after_forge] || options[:after_build] || nil
|
|
171
|
+
|
|
172
|
+
unless hook.nil? || hook.respond_to?(:call)
|
|
173
|
+
raise ObjectInterfaceError, "after-forge hook must respond to #call"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
hook
|
|
114
177
|
end
|
|
115
178
|
|
|
116
179
|
# Resolve attributes using default attributes, specified traits and overrides.
|
|
117
180
|
#
|
|
118
181
|
# @param traits [Array<Symbol>]
|
|
119
|
-
# @param overrides [Hash{Symbol => Any}]
|
|
182
|
+
# @param overrides [Hash{Symbol => Proc, Any}]
|
|
120
183
|
# @return [Hash{Symbol => Any}]
|
|
184
|
+
#
|
|
185
|
+
# @raise [ArgumentError]
|
|
121
186
|
def resolve_attributes(traits, overrides)
|
|
187
|
+
unless (unknown_traits = traits.difference(@parameters.traits.keys)).empty?
|
|
188
|
+
raise ArgumentError,
|
|
189
|
+
"unknown traits for forge#{" #{name}" if name}: #{unknown_traits.join(", ")}"
|
|
190
|
+
end
|
|
191
|
+
|
|
122
192
|
attributes = @parameters.attributes.merge(*@parameters.traits.values_at(*traits), overrides)
|
|
123
|
-
|
|
193
|
+
@crucible.call(attributes)
|
|
124
194
|
end
|
|
125
195
|
end
|
|
126
196
|
end
|
|
@@ -27,8 +27,8 @@ module ObjectForge
|
|
|
27
27
|
# @return [Hash{Symbol => Hash{Symbol => Proc}}] trait definitions
|
|
28
28
|
attr_reader :traits
|
|
29
29
|
|
|
30
|
-
# @return [Hash{Symbol => Any}]
|
|
31
|
-
attr_reader :
|
|
30
|
+
# @return [Hash{Symbol => Any}] options for forge, such as mold
|
|
31
|
+
attr_reader :options
|
|
32
32
|
|
|
33
33
|
# Define forge's parameters through DSL.
|
|
34
34
|
#
|
|
@@ -39,7 +39,7 @@ module ObjectForge
|
|
|
39
39
|
#
|
|
40
40
|
# @example with block parameter
|
|
41
41
|
# ForgeDSL.new do |f|
|
|
42
|
-
# f.mold = ObjectForge::Molds::
|
|
42
|
+
# f.mold = ObjectForge::Molds::KeywordsMold.new
|
|
43
43
|
# f.attribute(:name) { "Name" }
|
|
44
44
|
# f[:description] { name.upcase }
|
|
45
45
|
# f.duration { rand(1000) }
|
|
@@ -47,27 +47,27 @@ module ObjectForge
|
|
|
47
47
|
#
|
|
48
48
|
# @example without block parameter
|
|
49
49
|
# ForgeDSL.new do
|
|
50
|
-
# self.mold = ::ObjectForge::Molds::
|
|
50
|
+
# self.mold = ::ObjectForge::Molds::KeywordsMold.new
|
|
51
51
|
# attribute(:name) { "Name" }
|
|
52
52
|
# self[:description] { name.upcase }
|
|
53
53
|
# duration { rand(1000) }
|
|
54
54
|
# end
|
|
55
55
|
#
|
|
56
|
-
# @yieldparam
|
|
56
|
+
# @yieldparam dsl [ForgeDSL] self
|
|
57
57
|
# @yieldreturn [void]
|
|
58
58
|
def initialize(&dsl)
|
|
59
59
|
super
|
|
60
60
|
@attributes = {}
|
|
61
61
|
@sequences = {}
|
|
62
62
|
@traits = {}
|
|
63
|
-
@
|
|
63
|
+
@options = {}
|
|
64
64
|
|
|
65
65
|
dsl.arity.zero? ? instance_exec(&dsl) : yield(self)
|
|
66
66
|
|
|
67
67
|
freeze
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
# Freezes the instance, including +
|
|
70
|
+
# Freezes the instance, including +options+, +attributes+, +sequences+ and +traits+.
|
|
71
71
|
# Prevents further responses through +#method_missing+.
|
|
72
72
|
#
|
|
73
73
|
# @note Called automatically in {#initialize}.
|
|
@@ -78,40 +78,42 @@ module ObjectForge
|
|
|
78
78
|
@attributes.freeze
|
|
79
79
|
@sequences.freeze
|
|
80
80
|
@traits.freeze
|
|
81
|
-
@
|
|
81
|
+
@options.freeze
|
|
82
82
|
self
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
# Set a value for a forge's
|
|
85
|
+
# Set a value for a forge's option.
|
|
86
86
|
#
|
|
87
|
-
# Possible
|
|
87
|
+
# Possible options depend on used forge, but for default {Forge} a +:mold+ is expected.
|
|
88
|
+
# Check its documentation for full list of available options.
|
|
88
89
|
#
|
|
89
|
-
# It is also possible to set
|
|
90
|
+
# It is also possible to set options through +method_missing+, using name with a +=+ suffix.
|
|
90
91
|
#
|
|
91
92
|
# @see Molds
|
|
93
|
+
# @see Forge::Parameters#options
|
|
92
94
|
#
|
|
93
95
|
# @example
|
|
94
|
-
# f.
|
|
96
|
+
# f.option(:mold, ->(forge_target:, attributes:, **) { forge.new(**attributes) })
|
|
95
97
|
# f.mold = ObjectForge::Molds::SingleArgumentMold.new
|
|
96
98
|
#
|
|
97
|
-
# @param name [
|
|
98
|
-
# @param value [Any] value for the
|
|
99
|
-
# @return [Symbol]
|
|
99
|
+
# @param name [Symbol] option name
|
|
100
|
+
# @param value [Any] value for the option
|
|
101
|
+
# @return [Symbol] option name
|
|
100
102
|
#
|
|
101
|
-
# @raise [
|
|
102
|
-
def
|
|
103
|
+
# @raise [TypeError] if +name+ is not a Symbol
|
|
104
|
+
def option(name, value)
|
|
103
105
|
unless ::Symbol === name
|
|
104
|
-
raise ::
|
|
106
|
+
raise ::TypeError, "option name must be a Symbol, #{name.class} given"
|
|
105
107
|
end
|
|
106
108
|
|
|
107
|
-
@
|
|
109
|
+
@options[name] = value
|
|
108
110
|
|
|
109
111
|
name
|
|
110
112
|
end
|
|
111
113
|
|
|
112
114
|
# Define an attribute, possibly transient.
|
|
113
115
|
#
|
|
114
|
-
# DSL does not know or care what attributes the
|
|
116
|
+
# DSL does not know or care what attributes the target class has,
|
|
115
117
|
# so the only difference between "real" and "transient" attributes
|
|
116
118
|
# is how the class itself treats them.
|
|
117
119
|
#
|
|
@@ -125,6 +127,7 @@ module ObjectForge
|
|
|
125
127
|
# f.attribute(:name) { "Name" }
|
|
126
128
|
# f[:description] { name.downcase }
|
|
127
129
|
# f.duration { rand(1000) }
|
|
130
|
+
#
|
|
128
131
|
# @example using conflicting and reserved names
|
|
129
132
|
# f.attribute(:[]) { "Brackets" }
|
|
130
133
|
# f.attribute(:[]=) { "#{self[:[]]} are brackets" }
|
|
@@ -134,11 +137,11 @@ module ObjectForge
|
|
|
134
137
|
# @yieldreturn [Any] attribute value
|
|
135
138
|
# @return [Symbol] attribute name
|
|
136
139
|
#
|
|
137
|
-
# @raise [
|
|
140
|
+
# @raise [TypeError] if +name+ is not a Symbol
|
|
138
141
|
# @raise [DSLError] if no block is given
|
|
139
142
|
def attribute(name, &definition)
|
|
140
143
|
unless ::Symbol === name
|
|
141
|
-
raise ::
|
|
144
|
+
raise ::TypeError,
|
|
142
145
|
"attribute name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
143
146
|
end
|
|
144
147
|
unless block_given?
|
|
@@ -166,9 +169,11 @@ module ObjectForge
|
|
|
166
169
|
# f.sequence(:date, Date.today)
|
|
167
170
|
# f.sequence(:id) { _1.to_s }
|
|
168
171
|
# f.sequence(:dated_id, 10) { |n| "#{Date.today}/#{n}-#{id}" }
|
|
172
|
+
#
|
|
169
173
|
# @example using external sequence
|
|
170
174
|
# seq = Sequence.new(1)
|
|
171
175
|
# f.sequence(:global_id, seq)
|
|
176
|
+
#
|
|
172
177
|
# @example sequence reuse
|
|
173
178
|
# f.sequence(:id, "a") # => "a", "b", ...
|
|
174
179
|
# f.trait :new_id do
|
|
@@ -181,11 +186,11 @@ module ObjectForge
|
|
|
181
186
|
# @yieldreturn [Any] attribute value
|
|
182
187
|
# @return [Symbol] attribute name
|
|
183
188
|
#
|
|
184
|
-
# @raise [
|
|
185
|
-
# @raise [
|
|
189
|
+
# @raise [TypeError] if +name+ is not a Symbol
|
|
190
|
+
# @raise [ObjectInterfaceError] if +initial+ does not respond to #succ and is not a {Sequence}
|
|
186
191
|
def sequence(name, initial = 1, **nil, &)
|
|
187
192
|
unless ::Symbol === name
|
|
188
|
-
raise ::
|
|
193
|
+
raise ::TypeError,
|
|
189
194
|
"sequence name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
190
195
|
end
|
|
191
196
|
|
|
@@ -210,6 +215,7 @@ module ObjectForge
|
|
|
210
215
|
# f.name { "***xXxSPECIALxXx***" }
|
|
211
216
|
# f.sequence(:special_id) { "~~~ SpEcIaL #{_1} ~~~" }
|
|
212
217
|
# end
|
|
218
|
+
#
|
|
213
219
|
# @example externally defined trait
|
|
214
220
|
# # Variable defined outside of DSL:
|
|
215
221
|
# success_trait = ->(ft) do
|
|
@@ -227,13 +233,12 @@ module ObjectForge
|
|
|
227
233
|
# @yieldreturn [void]
|
|
228
234
|
# @return [Symbol] trait name
|
|
229
235
|
#
|
|
230
|
-
# @raise [
|
|
236
|
+
# @raise [TypeError] if +name+ is not a Symbol
|
|
231
237
|
# @raise [DSLError] if no block is given
|
|
232
238
|
# @raise [DSLError] if called inside of another trait definition
|
|
233
239
|
def trait(name, **nil)
|
|
234
240
|
unless ::Symbol === name
|
|
235
|
-
raise ::
|
|
236
|
-
"trait name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
241
|
+
raise ::TypeError, "trait name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
237
242
|
end
|
|
238
243
|
if @current_trait
|
|
239
244
|
raise DSLError, "can not define trait inside of another trait (in #{name.inspect})"
|
|
@@ -261,12 +266,12 @@ module ObjectForge
|
|
|
261
266
|
|
|
262
267
|
private
|
|
263
268
|
|
|
264
|
-
# Define an attribute (like +name+) or set a
|
|
269
|
+
# Define an attribute (like +name+) or set a option (like +name=+) using a shorthand.
|
|
265
270
|
#
|
|
266
271
|
# Can not be used with reserved names.
|
|
267
272
|
# Trying to use a conflicting name will lead to usual issues
|
|
268
273
|
# with calling random methods.
|
|
269
|
-
# When in doubt, use {#attribute} or {#
|
|
274
|
+
# When in doubt, use {#attribute} or {#option} instead.
|
|
270
275
|
#
|
|
271
276
|
# Reserved names are:
|
|
272
277
|
# - all names ending in +?+, +!+
|
|
@@ -274,16 +279,16 @@ module ObjectForge
|
|
|
274
279
|
# (operators, +`+, +[]+, +[]=+)
|
|
275
280
|
# - +rand+
|
|
276
281
|
#
|
|
277
|
-
# @param name [Symbol] attribute or
|
|
278
|
-
# @param value [Any] value for
|
|
282
|
+
# @param name [Symbol] attribute or option name
|
|
283
|
+
# @param value [Any] value for option
|
|
279
284
|
# @yieldreturn [Any] attribute value
|
|
280
|
-
# @return [Symbol] attribute or
|
|
285
|
+
# @return [Symbol] attribute or option name
|
|
281
286
|
#
|
|
282
287
|
# @raise [DSLError] if a reserved +name+ is used
|
|
283
288
|
def method_missing(name, value = nil, **nil, &)
|
|
284
289
|
return super(name) if frozen?
|
|
285
|
-
if
|
|
286
|
-
return
|
|
290
|
+
if valid_option_method?(name)
|
|
291
|
+
return option(name[...-1].to_sym, value) # steep:ignore NoMethod
|
|
287
292
|
end
|
|
288
293
|
return attribute(name, &) if respond_to_missing?(name, false)
|
|
289
294
|
|
|
@@ -291,12 +296,12 @@ module ObjectForge
|
|
|
291
296
|
end
|
|
292
297
|
|
|
293
298
|
def respond_to_missing?(name, _include_all)
|
|
294
|
-
return
|
|
299
|
+
return super if frozen?
|
|
295
300
|
|
|
296
301
|
!name.end_with?("?", "!") && !name.match?(/\A(?=\p{ASCII})\P{Word}/) && name != :rand
|
|
297
302
|
end
|
|
298
303
|
|
|
299
|
-
def
|
|
304
|
+
def valid_option_method?(name)
|
|
300
305
|
name.match?(/\A\p{Word}.*=\z/)
|
|
301
306
|
end
|
|
302
307
|
end
|
|
@@ -22,12 +22,12 @@ module ObjectForge
|
|
|
22
22
|
# @see Forge.define
|
|
23
23
|
#
|
|
24
24
|
# @param name [Symbol] name to register forge under
|
|
25
|
-
# @param
|
|
26
|
-
# @yieldparam
|
|
25
|
+
# @param forge_target [Class, Any] class or object to forge
|
|
26
|
+
# @yieldparam dsl [ForgeDSL]
|
|
27
27
|
# @yieldreturn [void]
|
|
28
28
|
# @return [Forge] forge
|
|
29
|
-
def define(name,
|
|
30
|
-
register(name, Forge.define(
|
|
29
|
+
def define(name, forge_target, &)
|
|
30
|
+
register(name, Forge.define(forge_target, name: name, &))
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Add a forge under a specified name.
|
|
@@ -59,14 +59,15 @@ module ObjectForge
|
|
|
59
59
|
#
|
|
60
60
|
# @see Forge#forge
|
|
61
61
|
#
|
|
62
|
-
# @
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
62
|
+
# @overload forge(name, *traits, **overrides)
|
|
63
|
+
# @param name [Symbol] name of the forge
|
|
64
|
+
# @param traits [Array<Symbol>] traits to apply
|
|
65
|
+
# @param overrides [Hash{Symbol => Any}] attribute overrides
|
|
66
|
+
# @yieldparam object [Any] forged instance
|
|
67
|
+
# @yieldreturn [void]
|
|
68
|
+
# @return [Any] forged instance
|
|
69
|
+
# @raise [ArgumentError] if a trait name is unknown
|
|
70
|
+
# @raise [KeyError] if forge with the specified name is not registered
|
|
70
71
|
def forge(name, ...)
|
|
71
72
|
@forges.fetch(name).call(...)
|
|
72
73
|
end
|
|
@@ -36,15 +36,15 @@ module ObjectForge
|
|
|
36
36
|
@default_proc = default_proc
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Build a new hash using +
|
|
39
|
+
# Build a new hash using +forge_target.[]+.
|
|
40
40
|
#
|
|
41
41
|
# @see Hash.[]
|
|
42
42
|
#
|
|
43
|
-
# @param
|
|
43
|
+
# @param forge_target [Class] Hash or a subclass of Hash
|
|
44
44
|
# @param attributes [Hash{Symbol => Any}]
|
|
45
45
|
# @return [Hash]
|
|
46
|
-
def call(
|
|
47
|
-
hash =
|
|
46
|
+
def call(forge_target:, attributes:, **_)
|
|
47
|
+
hash = forge_target[attributes]
|
|
48
48
|
hash.default = @default if @default
|
|
49
49
|
hash.default_proc = @default_proc if @default_proc
|
|
50
50
|
hash
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module ObjectForge
|
|
4
4
|
module Molds
|
|
5
|
-
# Basic mold which calls +
|
|
5
|
+
# Basic mold which calls +forge_target.new(**attributes)+.
|
|
6
6
|
#
|
|
7
7
|
# Can be used instead of {SingleArgumentMold}
|
|
8
8
|
# due to how keyword arguments are treated in Ruby,
|
|
@@ -11,13 +11,13 @@ module ObjectForge
|
|
|
11
11
|
# @thread_safety Thread-safe.
|
|
12
12
|
# @since 0.2.0
|
|
13
13
|
class KeywordsMold
|
|
14
|
-
# Instantiate
|
|
14
|
+
# Instantiate forge target with a hash of attributes.
|
|
15
15
|
#
|
|
16
|
-
# @param
|
|
16
|
+
# @param forge_target [Class, #new]
|
|
17
17
|
# @param attributes [Hash{Symbol => Any}]
|
|
18
18
|
# @return [Any]
|
|
19
|
-
def call(
|
|
20
|
-
|
|
19
|
+
def call(forge_target:, attributes:, **_)
|
|
20
|
+
forge_target.new(**attributes)
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module ObjectForge
|
|
4
4
|
module Molds
|
|
5
|
-
# Basic mold which calls +
|
|
5
|
+
# Basic mold which calls +forge_target.new(attributes)+.
|
|
6
6
|
#
|
|
7
7
|
# @thread_safety Thread-safe.
|
|
8
8
|
# @since 0.2.0
|
|
9
9
|
class SingleArgumentMold
|
|
10
|
-
# Instantiate
|
|
10
|
+
# Instantiate forge target with a hash of attributes.
|
|
11
11
|
#
|
|
12
|
-
# @param
|
|
12
|
+
# @param forge_target [Class, #new]
|
|
13
13
|
# @param attributes [Hash{Symbol => Any}]
|
|
14
14
|
# @return [Any]
|
|
15
|
-
def call(
|
|
16
|
-
|
|
15
|
+
def call(forge_target:, attributes:, **_)
|
|
16
|
+
forge_target.new(attributes)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -32,18 +32,22 @@ module ObjectForge
|
|
|
32
32
|
@lax = lax
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Instantiate
|
|
35
|
+
# Instantiate target struct with a hash of attributes.
|
|
36
36
|
#
|
|
37
|
-
# @param
|
|
37
|
+
# @param forge_target [Class] a subclass of Struct
|
|
38
38
|
# @param attributes [Hash{Symbol => Any}]
|
|
39
39
|
# @return [Struct]
|
|
40
|
-
def call(
|
|
41
|
-
if
|
|
42
|
-
lax
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
def call(forge_target:, attributes:, **_)
|
|
41
|
+
if forge_target.keyword_init?
|
|
42
|
+
if lax
|
|
43
|
+
forge_target.new(attributes.slice(*forge_target.members))
|
|
44
|
+
else
|
|
45
|
+
forge_target.new(attributes)
|
|
46
|
+
end
|
|
47
|
+
elsif forge_target.keyword_init? == false
|
|
48
|
+
forge_target.new(*attributes.values_at(*forge_target.members))
|
|
45
49
|
else
|
|
46
|
-
build_struct_with_unspecified_keyword_init(
|
|
50
|
+
build_struct_with_unspecified_keyword_init(forge_target, attributes)
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
|
@@ -51,18 +55,18 @@ module ObjectForge
|
|
|
51
55
|
|
|
52
56
|
if RUBY_FEATURE_AUTO_KEYWORDS
|
|
53
57
|
# Build struct by using keywords to specify member values.
|
|
54
|
-
def build_struct_with_unspecified_keyword_init(
|
|
58
|
+
def build_struct_with_unspecified_keyword_init(forge_target, attributes)
|
|
55
59
|
if lax
|
|
56
|
-
|
|
60
|
+
forge_target.new(**attributes.slice(*forge_target.members))
|
|
57
61
|
else
|
|
58
|
-
|
|
62
|
+
forge_target.new(**attributes)
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
else
|
|
62
66
|
# :nocov:
|
|
63
67
|
# Build struct by using positional arguments to specify member values.
|
|
64
|
-
def build_struct_with_unspecified_keyword_init(
|
|
65
|
-
|
|
68
|
+
def build_struct_with_unspecified_keyword_init(forge_target, attributes)
|
|
69
|
+
forge_target.new(*attributes.values_at(*forge_target.members))
|
|
66
70
|
end
|
|
67
71
|
# :nocov:
|
|
68
72
|
end
|
|
@@ -20,10 +20,10 @@ module ObjectForge
|
|
|
20
20
|
@wrapped_mold = wrapped_mold
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# @overload call(...)
|
|
24
23
|
# Instantiate {wrapped_mold} and call it.
|
|
25
24
|
#
|
|
26
|
-
# @
|
|
25
|
+
# @overload call(...)
|
|
26
|
+
# @return [Any] result of +wrapped_mold.new.call(...)+
|
|
27
27
|
def call(...)
|
|
28
28
|
wrapped_mold.new.call(...)
|
|
29
29
|
end
|