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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9481504e1456ec667e1c0182ecef5456cb2c9094b62ba6e09da62f0c1c79a00e
|
|
4
|
+
data.tar.gz: d2c2fdcfd6807211ea82987c998f4d2d8c6e0b1bbc334178330f97dfd8d7df7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85b341cae5396f0eee11f00e6084bfbdc4561070c4657d3e7265570b48f195789c4a8763220d81f38f2dd4c31573a36064aecf346f9e6ffac8e07dbcbcf86720
|
|
7
|
+
data.tar.gz: dc7faa45a80ce51ac2b4f57f7a88134b8c6a44ad3889f06b896962aa6de711c7fee36b70b7952ba80e19b95d036e838c169abebc375712a76b29e21e7ff54815
|
data/README.md
CHANGED
|
@@ -8,19 +8,28 @@
|
|
|
8
8
|
|
|
9
9
|
***
|
|
10
10
|
|
|
11
|
-
**ObjectForge**
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
11
|
+
**ObjectForge** is a small factory library for Ruby objects with minimal assumptions about framework, persistence, or runtime environment.
|
|
12
|
+
|
|
13
|
+
It is designed for cases where factory-style object construction is useful, but Rails-oriented or database-oriented tooling is a poor fit. **ObjectForge** works well with plain Ruby objects, hashes, structs, and custom build flows.
|
|
14
|
+
|
|
15
|
+
The library focuses on:
|
|
16
|
+
- explicit configuration over hidden conventions
|
|
17
|
+
- support for independent registries and standalone factories
|
|
18
|
+
- replaceable components based on simple interfaces
|
|
19
|
+
- usefulness both outside of tests and inside them
|
|
20
|
+
|
|
21
|
+
If you need factory-style object generation without coupling it to Rails, ActiveRecord, or a particular application structure, **ObjectForge** might be for you.
|
|
15
22
|
|
|
16
23
|
## Table of contents
|
|
17
24
|
|
|
18
|
-
- [Motivation
|
|
25
|
+
- [Motivation](#motivation)
|
|
19
26
|
- [Installation](#installation)
|
|
20
27
|
- [Usage](#usage)
|
|
28
|
+
- [Quick start](#quick-start)
|
|
21
29
|
- [Basics](#basics)
|
|
22
|
-
- [
|
|
23
|
-
- [Molds:
|
|
30
|
+
- [Independent forgeyards and forges](#independent-forgeyards-and-forges)
|
|
31
|
+
- [Molds: configuring object construction](#molds-configuring-object-construction)
|
|
32
|
+
- [After-build customization](#after-build-customization)
|
|
24
33
|
- [Performance tips](#performance-tips)
|
|
25
34
|
- [Differences and limitations (compared to FactoryBot)](#differences-and-limitations-compared-to-factorybot)
|
|
26
35
|
- [Current and planned features (roadmap)](#current-and-planned-features-roadmap)
|
|
@@ -28,22 +37,25 @@
|
|
|
28
37
|
- [Contributing](#contributing)
|
|
29
38
|
- [License](#license)
|
|
30
39
|
|
|
31
|
-
## Motivation
|
|
40
|
+
## Motivation
|
|
41
|
+
|
|
42
|
+
Ruby already has well-known factory libraries, especially FactoryBot and Fabrication. Those tools are effective in many projects, particularly when working in Rails applications and persistence-oriented test setups.
|
|
43
|
+
|
|
44
|
+
**ObjectForge** aims at a different problem space: building objects with a factory-style workflow while making as few assumptions as possible about framework, storage, object lifecycle, or application structure.
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
- assuming that streamlined object generation is only useful for testing;
|
|
39
|
-
- (related to the previous point) assuming that there will never be a need to
|
|
40
|
-
have more than one configuration of a library in the same project;
|
|
41
|
-
- assuming that adding global methods or objects is a good idea.
|
|
46
|
+
**ObjectForge** is particularly useful when:
|
|
47
|
+
- the objects being built are plain Ruby objects rather than database-backed records
|
|
48
|
+
- object generation is needed outside of tests, such as in services, scripts, or fixtures
|
|
49
|
+
- multiple independent sets of factories need to coexist in the same project
|
|
50
|
+
- construction behavior should be explicit and configurable rather than hidden behind framework conventions
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
The project is intentionally small in scope. Rather than trying to model every style of factory workflow, it focuses on a compact, understandable core:
|
|
53
|
+
- a DSL for defining attributes, sequences, and traits
|
|
54
|
+
- forges (factories) and forgeyards (registries)
|
|
55
|
+
- several object molds (constructors)
|
|
56
|
+
- a couple other helper components
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
Most factory projects are also quite dead, having not been updated in *many* years.
|
|
58
|
+
The goal is to have a simple, composable tool that you can easily reach for when heavier libraries don't fit or feel like overkill.
|
|
47
59
|
|
|
48
60
|
## Installation
|
|
49
61
|
|
|
@@ -56,6 +68,7 @@ Or, if using Bundler, add to your Gemfile:
|
|
|
56
68
|
```ruby
|
|
57
69
|
gem "object_forge"
|
|
58
70
|
```
|
|
71
|
+
and run `bundle install`.
|
|
59
72
|
|
|
60
73
|
## Usage
|
|
61
74
|
|
|
@@ -63,9 +76,48 @@ gem "object_forge"
|
|
|
63
76
|
> - Latest documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/object_forge).
|
|
64
77
|
> - Documentation for published versions is available on [RubyDoc](https://rubydoc.info/gems/object_forge).
|
|
65
78
|
|
|
79
|
+
### Quick start
|
|
80
|
+
|
|
81
|
+
Create your domain logic class:
|
|
82
|
+
```ruby
|
|
83
|
+
class Rectangle
|
|
84
|
+
def initialize(length:, width:)
|
|
85
|
+
@length = length
|
|
86
|
+
@width = width
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def area = @length * @width
|
|
90
|
+
|
|
91
|
+
def inspect = "[#{@length}x#{@width}]"
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Define a forge:
|
|
96
|
+
```ruby
|
|
97
|
+
require "object_forge"
|
|
98
|
+
ObjectForge.define(:rectangle, Rectangle) do |f|
|
|
99
|
+
f.mold = ObjectForge::Molds::KeywordsMold.new
|
|
100
|
+
|
|
101
|
+
f.length { rand(1..100) }
|
|
102
|
+
f.width { rand(1..100) }
|
|
103
|
+
|
|
104
|
+
f.trait :square do |t|
|
|
105
|
+
t.width { length }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Forge some objects!
|
|
111
|
+
```ruby
|
|
112
|
+
ObjectForge.forge(:rectangle) # => [63x27]
|
|
113
|
+
ObjectForge.forge(:rectangle, :square) # => [56x56]
|
|
114
|
+
ObjectForge.forge(:rectangle, width: 3333) # => [79x3333]
|
|
115
|
+
ObjectForge.forge(:rectangle, :square, length: 123) # => [123x123]
|
|
116
|
+
```
|
|
117
|
+
|
|
66
118
|
### Basics
|
|
67
119
|
|
|
68
|
-
In the simplest cases, **ObjectForge** can be used much like other factory libraries, with definitions living in a global object (`ObjectForge::DEFAULT_YARD`).
|
|
120
|
+
In the simplest cases, **ObjectForge** can be used much like other factory libraries, with definitions living in a global object (`ObjectForge::DEFAULT_YARD`). In this case, methods are called directly on `ObjectForge` module.
|
|
69
121
|
|
|
70
122
|
Forges are defined using a DSL:
|
|
71
123
|
```ruby
|
|
@@ -104,19 +156,19 @@ end
|
|
|
104
156
|
A forge builds objects, using attributes hash:
|
|
105
157
|
```ruby
|
|
106
158
|
ObjectForge.call(:point)
|
|
107
|
-
# => #<Point
|
|
159
|
+
# => #<struct Point id="a", x=0.17176955469852973, y=0.3423901951181103>
|
|
108
160
|
# Positional arguments define used traits:
|
|
109
161
|
ObjectForge.build(:point, :z)
|
|
110
|
-
# => #<Point
|
|
162
|
+
# => #<struct Point id="Z_b", x=0.0, y=0.0>
|
|
111
163
|
# Attributes can be overridden with keyword arguments:
|
|
112
164
|
ObjectForge.forge(:point, x: 10)
|
|
113
|
-
# => #<Point
|
|
165
|
+
# => #<struct Point id="c", x=10, y=-0.3458802496120402>
|
|
114
166
|
# Traits and overrides are combined in the given order:
|
|
115
167
|
ObjectForge.call(:point, :z, :invalid, id: "NaN")
|
|
116
|
-
# => #<Point
|
|
168
|
+
# => #<struct Point id="NaN", x=0.0, y=NaN>
|
|
117
169
|
# A Proc override behaves the same as an attribute definition:
|
|
118
170
|
ObjectForge.call(:point, :z, x: -> { rand(100..200) + delta })
|
|
119
|
-
# => #<Point
|
|
171
|
+
# => #<struct Point id="Z_d", x=135.0, y=0.0>
|
|
120
172
|
# A block can be passed to do something with the created object:
|
|
121
173
|
ObjectForge.call(:point, :z) { puts "#{_1.id}: #{_1.x},#{_1.y}" }
|
|
122
174
|
# outputs "Z_e: 0.0,0.0"
|
|
@@ -124,12 +176,12 @@ ObjectForge.call(:point, :z) { puts "#{_1.id}: #{_1.x},#{_1.y}" }
|
|
|
124
176
|
> [!TIP]
|
|
125
177
|
> Forging can be done through any of `#call`, `#forge`, or `#build` methods, they are aliases.
|
|
126
178
|
|
|
127
|
-
###
|
|
179
|
+
### Independent forgeyards and forges
|
|
128
180
|
|
|
129
181
|
It is possible and *encouraged* to create multiple forgeyards, each with its own set of forges:
|
|
130
182
|
```ruby
|
|
131
183
|
forgeyard = ObjectForge::Forgeyard.new
|
|
132
|
-
forgeyard.define(:
|
|
184
|
+
forgeyard.define(:dot, Point) do |f|
|
|
133
185
|
f.sequence(:id, "a")
|
|
134
186
|
f.x { rand(-radius..radius) }
|
|
135
187
|
f.y { rand(-radius..radius) }
|
|
@@ -140,14 +192,14 @@ end
|
|
|
140
192
|
|
|
141
193
|
Now, this forgeyard can be used just like the default one:
|
|
142
194
|
```ruby
|
|
143
|
-
forgeyard.forge(:
|
|
144
|
-
# => #<Point
|
|
195
|
+
forgeyard.forge(:dot, :z, id: "0")
|
|
196
|
+
# => #<struct Point id="0", x=0, y=0>
|
|
145
197
|
```
|
|
146
198
|
|
|
147
199
|
Note how the forge isn't registered in the default forgeyard:
|
|
148
200
|
```ruby
|
|
149
|
-
ObjectForge.forge(:
|
|
150
|
-
#
|
|
201
|
+
ObjectForge.forge(:dot)
|
|
202
|
+
# KeyError: key not found
|
|
151
203
|
```
|
|
152
204
|
|
|
153
205
|
If you find it more convenient not to use a forgeyard (for example, if you only need a single forge for your service), you can create individual forges:
|
|
@@ -159,55 +211,79 @@ forge = ObjectForge::Forge.define(Point) do |f|
|
|
|
159
211
|
f.radius { 0.5 }
|
|
160
212
|
f.trait :z do f.radius { 0 } end
|
|
161
213
|
end
|
|
162
|
-
forge.(:z, id: "0")
|
|
163
|
-
# => #<Point:0x00007f6109b719e0 @id="0", @x=0, @y=0>
|
|
164
214
|
```
|
|
165
215
|
|
|
166
216
|
**Forge** has the same building interface as a **Forgeyard**, but it doesn't have the name argument:
|
|
167
217
|
```ruby
|
|
168
218
|
forge.build
|
|
169
|
-
# => #<Point
|
|
219
|
+
# => #<struct Point id="a", x=0.3317733939650964, y=-0.1363936629550252>
|
|
170
220
|
forge.forge(:z)
|
|
171
|
-
# => #<Point
|
|
221
|
+
# => #<struct Point id="b", x=0, y=0>
|
|
172
222
|
forge.(radius: 500)
|
|
173
|
-
# => #<Point
|
|
223
|
+
# => #<struct Point id="c", x=-141, y=109>
|
|
174
224
|
```
|
|
175
225
|
|
|
176
|
-
### Molds:
|
|
226
|
+
### Molds: configuring object construction
|
|
177
227
|
|
|
178
|
-
If you use core Ruby data containers, such as `Struct`, `Data` or even `Hash`, they will "just work". However, if a custom class is used, forging will probably fail, unless your class happens to take a hash of attributes in `#initialize`. It would be
|
|
228
|
+
If you use core Ruby data containers, such as `Struct`, `Data` or even `Hash`, they will "just work". However, if a custom class is used, forging will probably fail, unless your class happens to take a hash of attributes in `#initialize`. It would be against the goal of **ObjectForge** to place requirements on your classes, and indeed there is a solution.
|
|
179
229
|
|
|
180
|
-
Whenever you need to change how your objects are built, you specify a *mold*. Molds are just
|
|
230
|
+
Whenever you need to change how your objects are built, you specify a *mold*. Molds are just `call`able objects (including `Proc`s!) with specific arguments. They are set in forge definition:
|
|
181
231
|
```ruby
|
|
182
232
|
forge = ObjectForge::Forge.define(Point) do |f|
|
|
183
|
-
f.mold = ->(
|
|
184
|
-
|
|
185
|
-
forged.new(attributes[:id], attributes[:x], attributes[:y])
|
|
233
|
+
f.mold = ->(forge_target:, attributes:, **) do
|
|
234
|
+
forge_target.new(attributes[:id], attributes[:x].round(3), attributes[:y].round(3))
|
|
186
235
|
end
|
|
187
|
-
#... rest of the definition
|
|
236
|
+
#... rest of the definition from the previous example
|
|
188
237
|
end
|
|
189
238
|
```
|
|
190
239
|
|
|
191
240
|
Now the specified **mold** will be called to build your objects:
|
|
192
241
|
```ruby
|
|
193
242
|
forge.forge
|
|
194
|
-
#
|
|
195
|
-
# => #<Point:0x00007f610deae578 @id="a", @x=0.3317733939650964, @y=-0.1363936629550252>
|
|
243
|
+
# => #<struct Point id="a", x=0.331, y=-0.136>
|
|
196
244
|
```
|
|
197
245
|
|
|
198
246
|
Of course, you can abuse this to your heart's content. Look at the documentation for `ObjectForge::Molds` for inspiration.
|
|
199
247
|
|
|
200
248
|
**ObjectForge** comes pre-equipped with a selection of molds for common cases:
|
|
201
|
-
- `ObjectForge::Molds::SingleArgumentMold` (*the default*)
|
|
202
|
-
- `ObjectForge::Molds::KeywordsMold`
|
|
203
|
-
- `ObjectForge::Molds::HashMold` allows building **Hash** (including subclasses), providing a way to easily use hashes
|
|
204
|
-
- `ObjectForge::Molds::StructMold` handles all possible cases of `keyword_init` for **Struct**
|
|
249
|
+
- `ObjectForge::Molds::SingleArgumentMold` (*the default*) calls `new(attributes)`, suitable for **ActiveModel**-style objects and **Dry::Struct**, as an example
|
|
250
|
+
- `ObjectForge::Molds::KeywordsMold` calls `new(**attributes)`, suitable for **Data** and similar classes
|
|
251
|
+
- `ObjectForge::Molds::HashMold` allows building **Hash** (including subclasses), providing a way to easily use build hashes
|
|
252
|
+
- `ObjectForge::Molds::StructMold` handles all possible cases of `keyword_init` for **Struct**s
|
|
253
|
+
|
|
254
|
+
> [!NOTE]
|
|
255
|
+
> If you don't specify a mold, **ObjectForge** will infer one for core data containers, including **Hash**, **Struct**, and **Data** subclasses.
|
|
205
256
|
|
|
206
257
|
> [!TIP]
|
|
207
|
-
>
|
|
258
|
+
> It is recommended to use molds instances directly. Using classes causes memory churn and lowered performance. Not only that, but having a stateful mold is a code smell and probably represents a significant design issue.
|
|
259
|
+
|
|
260
|
+
### After-build customization
|
|
261
|
+
|
|
262
|
+
If there is a need to modify the object or perform additional actions after it is forged, there are two mechanisms you can employ:
|
|
263
|
+
- after-forge hook
|
|
264
|
+
- customization block
|
|
265
|
+
|
|
266
|
+
After-forge hook is a `call`able object specified as part of forge definition. It runs every time forging happens:
|
|
267
|
+
```ruby
|
|
268
|
+
forge = ObjectForge::Forge.define(Rectangle) do |f|
|
|
269
|
+
# can also be specified as `after_build`
|
|
270
|
+
f.after_forge = ->(rect) { puts "Used #{rect.area} sq. units" }
|
|
271
|
+
#... rest of the definition from the Quick start example
|
|
272
|
+
end
|
|
273
|
+
forge.forge
|
|
274
|
+
# Used 621 sq. units
|
|
275
|
+
# => [23x27]
|
|
276
|
+
```
|
|
208
277
|
|
|
278
|
+
Customization block is an optional block argument to `#forge` and is only executed in that specific invocation:
|
|
279
|
+
```ruby
|
|
280
|
+
forge.forge { |rect| RectangleRepository.save(rect); puts "persisted!" }
|
|
281
|
+
# Used 621 sq. units
|
|
282
|
+
# persisted!
|
|
283
|
+
# => [23x27]
|
|
284
|
+
```
|
|
209
285
|
|
|
210
|
-
|
|
286
|
+
If both hook and block are used, the hook runs before the block.
|
|
211
287
|
|
|
212
288
|
### Performance tips
|
|
213
289
|
|
|
@@ -222,24 +298,24 @@ If you are used to FactoryBot, be aware that there are quite a few differences i
|
|
|
222
298
|
|
|
223
299
|
General:
|
|
224
300
|
- The user (you) is responsible for loading forge definitions, there are no search paths. If **ObjectForge** is used in tests, it should be enough to add something like `Dir["spec/forges/**/*.rb].each { require _1 }` to your `spec_helper.rb` (or `rails_helper.rb`).
|
|
225
|
-
- `Forgeyard.define` *is* the forge definition block,
|
|
226
|
-
- There is no forge inheritance or nesting, though it may be added in the future.
|
|
301
|
+
- `Forgeyard.define` *is* the forge definition block, there is no separate `factory` block.
|
|
227
302
|
|
|
228
303
|
Forge definition:
|
|
229
304
|
- Class specification for a forge is non-optional, there is no assumption about the class name.
|
|
230
305
|
- If the DSL block declares a block argument, `self` context is not changed, and DSL methods can't be called with an implicit receiver.
|
|
306
|
+
- There is no forge inheritance or nesting.
|
|
231
307
|
|
|
232
308
|
Attributes:
|
|
233
|
-
-
|
|
309
|
+
- Currently, there is no concept of transient attributes. Attribute selection needs to be handled by the mold.
|
|
234
310
|
- *There are no associations*. If nested objects are required, they should be created and set in the block for the attribute.
|
|
235
311
|
|
|
236
312
|
Traits:
|
|
237
|
-
- Traits can't be defined inside of other traits.
|
|
313
|
+
- Traits can't be defined inside of other traits.
|
|
238
314
|
- Traits can't be called from other traits. This may change in the future.
|
|
239
315
|
- There are no default traits.
|
|
240
316
|
|
|
241
317
|
Sequences:
|
|
242
|
-
- There is no explicit way to define shared sequences,
|
|
318
|
+
- There is no explicit way to define shared sequences, but a freestanding `Sequence` can be created manually and passed into `sequence` calls.
|
|
243
319
|
- Sequences work with values implementing `#succ`, not `#next`, expressly prohibiting `Enumerator`. This may be relaxed in the future.
|
|
244
320
|
|
|
245
321
|
## Current and planned features (roadmap)
|
|
@@ -255,10 +331,11 @@ kanban
|
|
|
255
331
|
[Tapping into built objects for post-processing]
|
|
256
332
|
[Custom builders / molds]
|
|
257
333
|
[Built-in Hash, Struct, Data builders / molds]
|
|
258
|
-
[⚗️ To do]
|
|
259
334
|
[Ability to replace resolver]
|
|
260
335
|
[After-build hook]
|
|
261
|
-
[
|
|
336
|
+
[⚗️ To do]
|
|
337
|
+
[Transient attributes / attribute filtering]
|
|
338
|
+
[❔ Maybe, maybe not]
|
|
262
339
|
[Calling traits from traits]
|
|
263
340
|
[Default traits]
|
|
264
341
|
[Forge inheritance]
|
|
@@ -268,7 +345,9 @@ kanban
|
|
|
268
345
|
|
|
269
346
|
## Development
|
|
270
347
|
|
|
271
|
-
After checking out the repo, run `bundle install` to install dependencies.
|
|
348
|
+
After checking out the repo, run `bundle install` to install dependencies. If you will be running typing checks (RBS/Steep), also execute `rbs collection install`.
|
|
349
|
+
|
|
350
|
+
Then, run `rake spec` to run the tests, `rake rubocop` to lint code and check style compliance, `rake rbs` to validate signatures or just `rake` to do everything above. There is also `rake steep` to check typing, and `rake docs` to generate YARD documentation.
|
|
272
351
|
|
|
273
352
|
You can also run `bin/console` for an interactive prompt that will allow you to experiment, or `bin/benchmark` to run a benchmark script and generate a StackProf flamegraph.
|
|
274
353
|
|
|
@@ -11,9 +11,26 @@ module ObjectForge
|
|
|
11
11
|
# but it's not a private API.
|
|
12
12
|
#
|
|
13
13
|
# @thread_safety Attribute resolution is idempotent,
|
|
14
|
-
#
|
|
14
|
+
# and using {.call} is thread-safe.
|
|
15
15
|
# @since 0.1.0
|
|
16
16
|
class Crucible < UnBasicObject
|
|
17
|
+
class << self
|
|
18
|
+
# Resolve all attributes by calling their +Proc+s,
|
|
19
|
+
# using a new instance as evaluation context.
|
|
20
|
+
#
|
|
21
|
+
# @note This method destructively modifies initial attributes.
|
|
22
|
+
# @see #resolve!
|
|
23
|
+
# @thread_safety This method is thread-safe.
|
|
24
|
+
#
|
|
25
|
+
# @param attributes [Hash{Symbol => Proc, Any}] initial attributes
|
|
26
|
+
# @return [Hash{Symbol => Any}] resolved attributes
|
|
27
|
+
def call(attributes)
|
|
28
|
+
new(attributes).resolve!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias resolve call
|
|
32
|
+
end
|
|
33
|
+
|
|
17
34
|
%i[rand].each { |m| private define_method(m, ::Kernel.instance_method(m)) }
|
|
18
35
|
|
|
19
36
|
# @param attributes [Hash{Symbol => Proc, Any}] initial attributes
|
|
@@ -21,6 +38,7 @@ module ObjectForge
|
|
|
21
38
|
super()
|
|
22
39
|
@attributes = attributes
|
|
23
40
|
@resolved_attributes = ::Set.new
|
|
41
|
+
@resolving_attributes = []
|
|
24
42
|
end
|
|
25
43
|
|
|
26
44
|
# Resolve all attributes by calling their +Proc+s,
|
|
@@ -29,11 +47,13 @@ module ObjectForge
|
|
|
29
47
|
# Attributes can freely refer to each other inside +Proc+s
|
|
30
48
|
# through bareword names or +#[]+.
|
|
31
49
|
# However, make sure to avoid cyclic dependencies:
|
|
32
|
-
# they
|
|
50
|
+
# they can't be resolved and will raise {CircularAttributeDependencyError}.
|
|
33
51
|
#
|
|
34
52
|
# @note This method destructively modifies initial attributes.
|
|
35
53
|
#
|
|
36
54
|
# @return [Hash{Symbol => Any}] resolved attributes
|
|
55
|
+
#
|
|
56
|
+
# @raise [CircularAttributeDependencyError] if a dependency cycle is detected
|
|
37
57
|
def resolve!
|
|
38
58
|
@attributes.each_key { |name| method_missing(name) }
|
|
39
59
|
@attributes
|
|
@@ -52,27 +72,36 @@ module ObjectForge
|
|
|
52
72
|
# description: -> { name.downcase },
|
|
53
73
|
# duration: -> { rand(1000) }
|
|
54
74
|
# }
|
|
55
|
-
# Crucible.
|
|
56
|
-
#
|
|
75
|
+
# Crucible.call(attrs)
|
|
76
|
+
# # => { name: "Name", description: "name", duration: 123 }
|
|
77
|
+
#
|
|
57
78
|
# @example using conflicting and reserved names
|
|
58
79
|
# attrs = {
|
|
59
80
|
# "[]": -> { "Brackets" },
|
|
60
81
|
# "[]=": -> { "#{self[:[]]} are brackets" },
|
|
61
82
|
# "!": -> { "#{self[:[]=]}!" }
|
|
62
83
|
# }
|
|
63
|
-
# Crucible.
|
|
64
|
-
#
|
|
84
|
+
# Crucible.resolve(attrs)
|
|
85
|
+
# # => { "[]": "Brackets", "[]=": "Brackets are brackets", "!": "Brackets are brackets!" }
|
|
65
86
|
#
|
|
66
87
|
# @param name [Symbol]
|
|
67
88
|
# @return [Any]
|
|
68
|
-
|
|
89
|
+
#
|
|
90
|
+
# @raise [CircularAttributeDependencyError] if a dependency cycle is detected
|
|
91
|
+
def method_missing(name) # rubocop:disable Metrics/MethodLength
|
|
69
92
|
if @attributes.key?(name)
|
|
70
|
-
if @
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
if @resolving_attributes.include?(name)
|
|
94
|
+
raise_circular_dependency_error!(name)
|
|
95
|
+
elsif !@resolved_attributes.include?(name) && (::Proc === @attributes[name])
|
|
96
|
+
begin
|
|
97
|
+
@resolving_attributes << name
|
|
98
|
+
@attributes[name] = instance_exec(&@attributes[name])
|
|
99
|
+
@resolved_attributes << name
|
|
100
|
+
ensure
|
|
101
|
+
@resolving_attributes.pop
|
|
102
|
+
end
|
|
75
103
|
end
|
|
104
|
+
@attributes[name]
|
|
76
105
|
else
|
|
77
106
|
super
|
|
78
107
|
end
|
|
@@ -81,7 +110,14 @@ module ObjectForge
|
|
|
81
110
|
alias [] method_missing
|
|
82
111
|
|
|
83
112
|
def respond_to_missing?(name, _include_all)
|
|
84
|
-
@attributes.key?(name)
|
|
113
|
+
@attributes.key?(name) || super
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def raise_circular_dependency_error!(name)
|
|
117
|
+
loop_start = @resolving_attributes.index(name)
|
|
118
|
+
loop = @resolving_attributes[loop_start..] # : Array[Symbol]
|
|
119
|
+
raise CircularAttributeDependencyError,
|
|
120
|
+
"attribute depends on itself: #{loop.join(" -> ")} -> #{name}"
|
|
85
121
|
end
|
|
86
122
|
end
|
|
87
123
|
end
|