object_forge 0.3.0 → 0.4.1
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 +50 -13
- data/lib/object_forge/forge.rb +112 -41
- data/lib/object_forge/forge_dsl.rb +48 -39
- 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 +18 -16
- data/sig/object_forge.rbs +83 -41
- 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,84 @@ 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)
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
|
|
192
|
+
trait_attributes = @parameters.traits.values_at(*traits) # : Array[Hash[Symbol, ObjectForge::attribute]]
|
|
193
|
+
attributes = @parameters.attributes.merge(*trait_attributes, overrides)
|
|
194
|
+
@crucible.call(attributes)
|
|
124
195
|
end
|
|
125
196
|
end
|
|
126
197
|
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,43 @@ 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 (in #{name.inspect})"
|
|
105
107
|
end
|
|
108
|
+
raise DSLError, "option definition does not take a block (in #{name.inspect})" if block_given?
|
|
106
109
|
|
|
107
|
-
@
|
|
110
|
+
@options[name] = value
|
|
108
111
|
|
|
109
112
|
name
|
|
110
113
|
end
|
|
111
114
|
|
|
112
115
|
# Define an attribute, possibly transient.
|
|
113
116
|
#
|
|
114
|
-
# DSL does not know or care what attributes the
|
|
117
|
+
# DSL does not know or care what attributes the target class has,
|
|
115
118
|
# so the only difference between "real" and "transient" attributes
|
|
116
119
|
# is how the class itself treats them.
|
|
117
120
|
#
|
|
@@ -125,6 +128,7 @@ module ObjectForge
|
|
|
125
128
|
# f.attribute(:name) { "Name" }
|
|
126
129
|
# f[:description] { name.downcase }
|
|
127
130
|
# f.duration { rand(1000) }
|
|
131
|
+
#
|
|
128
132
|
# @example using conflicting and reserved names
|
|
129
133
|
# f.attribute(:[]) { "Brackets" }
|
|
130
134
|
# f.attribute(:[]=) { "#{self[:[]]} are brackets" }
|
|
@@ -134,11 +138,11 @@ module ObjectForge
|
|
|
134
138
|
# @yieldreturn [Any] attribute value
|
|
135
139
|
# @return [Symbol] attribute name
|
|
136
140
|
#
|
|
137
|
-
# @raise [
|
|
141
|
+
# @raise [TypeError] if +name+ is not a Symbol
|
|
138
142
|
# @raise [DSLError] if no block is given
|
|
139
143
|
def attribute(name, &definition)
|
|
140
144
|
unless ::Symbol === name
|
|
141
|
-
raise ::
|
|
145
|
+
raise ::TypeError,
|
|
142
146
|
"attribute name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
143
147
|
end
|
|
144
148
|
unless block_given?
|
|
@@ -166,9 +170,11 @@ module ObjectForge
|
|
|
166
170
|
# f.sequence(:date, Date.today)
|
|
167
171
|
# f.sequence(:id) { _1.to_s }
|
|
168
172
|
# f.sequence(:dated_id, 10) { |n| "#{Date.today}/#{n}-#{id}" }
|
|
173
|
+
#
|
|
169
174
|
# @example using external sequence
|
|
170
175
|
# seq = Sequence.new(1)
|
|
171
176
|
# f.sequence(:global_id, seq)
|
|
177
|
+
#
|
|
172
178
|
# @example sequence reuse
|
|
173
179
|
# f.sequence(:id, "a") # => "a", "b", ...
|
|
174
180
|
# f.trait :new_id do
|
|
@@ -181,18 +187,18 @@ module ObjectForge
|
|
|
181
187
|
# @yieldreturn [Any] attribute value
|
|
182
188
|
# @return [Symbol] attribute name
|
|
183
189
|
#
|
|
184
|
-
# @raise [
|
|
185
|
-
# @raise [
|
|
190
|
+
# @raise [TypeError] if +name+ is not a Symbol
|
|
191
|
+
# @raise [ObjectInterfaceError] if +initial+ does not respond to #succ and is not a {Sequence}
|
|
186
192
|
def sequence(name, initial = 1, **nil, &)
|
|
187
193
|
unless ::Symbol === name
|
|
188
|
-
raise ::
|
|
194
|
+
raise ::TypeError,
|
|
189
195
|
"sequence name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
190
196
|
end
|
|
191
197
|
|
|
192
198
|
seq = @sequences[name] ||= Sequence.new(initial)
|
|
193
199
|
|
|
194
200
|
if block_given?
|
|
195
|
-
attribute(name) { instance_exec(seq.next, &) }
|
|
201
|
+
attribute(name) { instance_exec(seq.next, &) } # steep:ignore BlockTypeMismatch
|
|
196
202
|
else
|
|
197
203
|
attribute(name) { seq.next }
|
|
198
204
|
end
|
|
@@ -210,6 +216,7 @@ module ObjectForge
|
|
|
210
216
|
# f.name { "***xXxSPECIALxXx***" }
|
|
211
217
|
# f.sequence(:special_id) { "~~~ SpEcIaL #{_1} ~~~" }
|
|
212
218
|
# end
|
|
219
|
+
#
|
|
213
220
|
# @example externally defined trait
|
|
214
221
|
# # Variable defined outside of DSL:
|
|
215
222
|
# success_trait = ->(ft) do
|
|
@@ -227,13 +234,12 @@ module ObjectForge
|
|
|
227
234
|
# @yieldreturn [void]
|
|
228
235
|
# @return [Symbol] trait name
|
|
229
236
|
#
|
|
230
|
-
# @raise [
|
|
237
|
+
# @raise [TypeError] if +name+ is not a Symbol
|
|
231
238
|
# @raise [DSLError] if no block is given
|
|
232
239
|
# @raise [DSLError] if called inside of another trait definition
|
|
233
240
|
def trait(name, **nil)
|
|
234
241
|
unless ::Symbol === name
|
|
235
|
-
raise ::
|
|
236
|
-
"trait name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
242
|
+
raise ::TypeError, "trait name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
237
243
|
end
|
|
238
244
|
if @current_trait
|
|
239
245
|
raise DSLError, "can not define trait inside of another trait (in #{name.inspect})"
|
|
@@ -261,12 +267,12 @@ module ObjectForge
|
|
|
261
267
|
|
|
262
268
|
private
|
|
263
269
|
|
|
264
|
-
# Define an attribute (like +name+) or set a
|
|
270
|
+
# Define an attribute (like +name+) or set a option (like +name=+) using a shorthand.
|
|
265
271
|
#
|
|
266
272
|
# Can not be used with reserved names.
|
|
267
273
|
# Trying to use a conflicting name will lead to usual issues
|
|
268
274
|
# with calling random methods.
|
|
269
|
-
# When in doubt, use {#attribute} or {#
|
|
275
|
+
# When in doubt, use {#attribute} or {#option} instead.
|
|
270
276
|
#
|
|
271
277
|
# Reserved names are:
|
|
272
278
|
# - all names ending in +?+, +!+
|
|
@@ -274,29 +280,32 @@ module ObjectForge
|
|
|
274
280
|
# (operators, +`+, +[]+, +[]=+)
|
|
275
281
|
# - +rand+
|
|
276
282
|
#
|
|
277
|
-
# @param name [Symbol] attribute or
|
|
278
|
-
# @param value [Any] value for
|
|
283
|
+
# @param name [Symbol] attribute or option name
|
|
284
|
+
# @param value [Any] value for option
|
|
279
285
|
# @yieldreturn [Any] attribute value
|
|
280
|
-
# @return [Symbol] attribute or
|
|
286
|
+
# @return [Symbol] attribute or option name
|
|
281
287
|
#
|
|
282
288
|
# @raise [DSLError] if a reserved +name+ is used
|
|
283
289
|
def method_missing(name, value = nil, **nil, &)
|
|
284
290
|
return super(name) if frozen?
|
|
285
|
-
|
|
286
|
-
|
|
291
|
+
|
|
292
|
+
if valid_option_method?(name)
|
|
293
|
+
# Intentionally passing block to `option` to trigger DSLError if it is present.
|
|
294
|
+
return option(name[...-1].to_sym, value, &) # steep:ignore NoMethod, UnexpectedBlockGiven
|
|
287
295
|
end
|
|
288
|
-
|
|
296
|
+
# Block can be missing, but `attribute` will raise if it is.
|
|
297
|
+
return attribute(name, &) if respond_to_missing?(name, false) # steep:ignore BlockTypeMismatch
|
|
289
298
|
|
|
290
299
|
raise DSLError, "#{name.inspect} is a reserved name (in #{name.inspect})"
|
|
291
300
|
end
|
|
292
301
|
|
|
293
302
|
def respond_to_missing?(name, _include_all)
|
|
294
|
-
return
|
|
303
|
+
return super if frozen?
|
|
295
304
|
|
|
296
305
|
!name.end_with?("?", "!") && !name.match?(/\A(?=\p{ASCII})\P{Word}/) && name != :rand
|
|
297
306
|
end
|
|
298
307
|
|
|
299
|
-
def
|
|
308
|
+
def valid_option_method?(name)
|
|
300
309
|
name.match?(/\A\p{Word}.*=\z/)
|
|
301
310
|
end
|
|
302
311
|
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
|