object_forge 0.4.1 → 0.5.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 +178 -37
- data/lib/object_forge/crucible.rb +14 -4
- data/lib/object_forge/forge.rb +82 -12
- data/lib/object_forge/forge_dsl.rb +45 -10
- data/lib/object_forge/forgeyard.rb +9 -1
- data/lib/object_forge/molds/array_mold.rb +27 -0
- data/lib/object_forge/molds/hash_mold.rb +0 -1
- data/lib/object_forge/molds.rb +14 -10
- data/lib/object_forge/version.rb +1 -1
- data/sig/manifest.yaml +2 -0
- data/sig/object_forge/molds.rbs +15 -4
- data/sig/object_forge.rbs +57 -16
- metadata +9 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0800625b2fdd91bc43b347e97fbbdbcad8f7a4953e2a1543bd29b602039ba2c
|
|
4
|
+
data.tar.gz: 3cfea11020226e4052b25127af3763df2fa5db8b80d5261aa0b7c8b3e2b20547
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 72c01d3be5fe72716a08fa6fb826fc16b4464a9a7086168e0efe708159e867da6d162107a96f6a5f1f9f1712a409c565e5f4da576232f9a2f93e24d979211bdd
|
|
7
|
+
data.tar.gz: bc2e52673353fb0ffc63944abdc864b2668d671921c2184dec62d5f45a2709c0a6e62ec7ebe937b4a9a9c8fa44f9a93bce05501512006d3988344a5492e3e9c1
|
data/README.md
CHANGED
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
[](https://github.com/trinistr/object_forge/actions/workflows/CI.yaml)
|
|
5
5
|
|
|
6
6
|
> [!TIP]
|
|
7
|
+
>
|
|
7
8
|
> You may be viewing documentation for an older (or newer) version of the gem than intended. Look at [Changelog](https://github.com/trinistr/object_forge/blob/main/CHANGELOG.md) to see all versions, including unreleased changes.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
---
|
|
10
11
|
|
|
11
12
|
**ObjectForge** is a small factory library for Ruby objects with minimal assumptions about framework, persistence, or runtime environment.
|
|
12
13
|
|
|
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
|
+
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, arrays, structs, and custom build flows.
|
|
14
15
|
|
|
15
16
|
The library focuses on:
|
|
17
|
+
|
|
16
18
|
- explicit configuration over hidden conventions
|
|
17
19
|
- support for independent registries and standalone factories
|
|
18
20
|
- replaceable components based on simple interfaces
|
|
@@ -25,12 +27,13 @@ If you need factory-style object generation without coupling it to Rails, Active
|
|
|
25
27
|
- [Motivation](#motivation)
|
|
26
28
|
- [Installation](#installation)
|
|
27
29
|
- [Usage](#usage)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
- [Quick start](#quick-start)
|
|
31
|
+
- [Basics](#basics)
|
|
32
|
+
- [Independent forgeyards and forges](#independent-forgeyards-and-forges)
|
|
33
|
+
- [Association-like nested objects](#association-like-nested-objects)
|
|
34
|
+
- [Defining final attribute list](#defining-final-attribute-list)
|
|
35
|
+
- [Molds: configuring object construction](#molds-configuring-object-construction)
|
|
36
|
+
- [After-build customization](#after-build-customization)
|
|
34
37
|
- [Differences and limitations (compared to FactoryBot)](#differences-and-limitations-compared-to-factorybot)
|
|
35
38
|
- [Current and planned features (roadmap)](#current-and-planned-features-roadmap)
|
|
36
39
|
- [Development](#development)
|
|
@@ -44,12 +47,14 @@ Ruby already has well-known factory libraries, especially FactoryBot and Fabrica
|
|
|
44
47
|
**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.
|
|
45
48
|
|
|
46
49
|
**ObjectForge** is particularly useful when:
|
|
50
|
+
|
|
47
51
|
- the objects being built are plain Ruby objects rather than database-backed records
|
|
48
52
|
- object generation is needed outside of tests, such as in services, scripts, or fixtures
|
|
49
53
|
- multiple independent sets of factories need to coexist in the same project
|
|
50
54
|
- construction behavior should be explicit and configurable rather than hidden behind framework conventions
|
|
51
55
|
|
|
52
56
|
The project is intentionally small in scope. Rather than trying to model every style of factory workflow, it focuses on a compact, understandable core:
|
|
57
|
+
|
|
53
58
|
- a DSL for defining attributes, sequences, and traits
|
|
54
59
|
- forges (factories) and forgeyards (registries)
|
|
55
60
|
- several object molds (constructors)
|
|
@@ -60,25 +65,30 @@ The goal is to have a simple, composable tool that you can easily reach for when
|
|
|
60
65
|
## Installation
|
|
61
66
|
|
|
62
67
|
Install with `gem`:
|
|
68
|
+
|
|
63
69
|
```sh
|
|
64
70
|
gem install object_forge
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
Or, if using Bundler, add to your Gemfile:
|
|
74
|
+
|
|
68
75
|
```ruby
|
|
69
76
|
gem "object_forge"
|
|
70
77
|
```
|
|
78
|
+
|
|
71
79
|
and run `bundle install`.
|
|
72
80
|
|
|
73
81
|
## Usage
|
|
74
82
|
|
|
75
83
|
> [!Note]
|
|
84
|
+
>
|
|
76
85
|
> - Latest documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/object_forge).
|
|
77
86
|
> - Documentation for published versions is available on [RubyDoc](https://rubydoc.info/gems/object_forge).
|
|
78
87
|
|
|
79
88
|
### Quick start
|
|
80
89
|
|
|
81
90
|
Create your domain logic class:
|
|
91
|
+
|
|
82
92
|
```ruby
|
|
83
93
|
class Rectangle
|
|
84
94
|
def initialize(length:, width:)
|
|
@@ -93,8 +103,10 @@ end
|
|
|
93
103
|
```
|
|
94
104
|
|
|
95
105
|
Define a forge:
|
|
106
|
+
|
|
96
107
|
```ruby
|
|
97
108
|
require "object_forge"
|
|
109
|
+
|
|
98
110
|
ObjectForge.define(:rectangle, Rectangle) do |f|
|
|
99
111
|
f.mold = ObjectForge::Molds::KeywordsMold.new
|
|
100
112
|
|
|
@@ -108,6 +120,7 @@ end
|
|
|
108
120
|
```
|
|
109
121
|
|
|
110
122
|
Forge some objects!
|
|
123
|
+
|
|
111
124
|
```ruby
|
|
112
125
|
ObjectForge.forge(:rectangle) # => [63x27]
|
|
113
126
|
ObjectForge.forge(:rectangle, :square) # => [56x56]
|
|
@@ -120,6 +133,7 @@ ObjectForge.forge(:rectangle, :square, length: 123) # => [123x123]
|
|
|
120
133
|
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.
|
|
121
134
|
|
|
122
135
|
Forges are defined using a DSL:
|
|
136
|
+
|
|
123
137
|
```ruby
|
|
124
138
|
# Example class:
|
|
125
139
|
Point = Struct.new(:id, :x, :y)
|
|
@@ -134,8 +148,8 @@ ObjectForge.define(:point, Point) do |f|
|
|
|
134
148
|
f[:y] { rand(-delta..delta) }
|
|
135
149
|
# There is also the familiar shortcut using `method_missing`:
|
|
136
150
|
f.delta { 0.5 * amplitude }
|
|
137
|
-
#
|
|
138
|
-
f.amplitude { 1 }
|
|
151
|
+
# Depending on the class, transient attributes may need to be explicitly marked:
|
|
152
|
+
f.transient(:amplitude) { 1 }
|
|
139
153
|
# `#sequence` defines a sequenced attribute (starting with 1 by default):
|
|
140
154
|
f.sequence(:id, "a")
|
|
141
155
|
# Traits allow to group and reuse related values:
|
|
@@ -154,6 +168,7 @@ end
|
|
|
154
168
|
```
|
|
155
169
|
|
|
156
170
|
A forge builds objects, using attributes hash:
|
|
171
|
+
|
|
157
172
|
```ruby
|
|
158
173
|
ObjectForge.call(:point)
|
|
159
174
|
# => #<struct Point id="a", x=0.17176955469852973, y=0.3423901951181103>
|
|
@@ -173,36 +188,53 @@ ObjectForge.call(:point, :z, x: -> { rand(100..200) + delta })
|
|
|
173
188
|
ObjectForge.call(:point, :z) { puts "#{_1.id}: #{_1.x},#{_1.y}" }
|
|
174
189
|
# outputs "Z_e: 0.0,0.0"
|
|
175
190
|
```
|
|
191
|
+
|
|
176
192
|
> [!TIP]
|
|
193
|
+
>
|
|
177
194
|
> Forging can be done through any of `#call`, `#forge`, or `#build` methods, they are aliases.
|
|
178
195
|
|
|
179
196
|
### Independent forgeyards and forges
|
|
180
197
|
|
|
181
198
|
It is possible and *encouraged* to create multiple forgeyards, each with its own set of forges:
|
|
199
|
+
|
|
182
200
|
```ruby
|
|
183
201
|
forgeyard = ObjectForge::Forgeyard.new
|
|
202
|
+
|
|
184
203
|
forgeyard.define(:dot, Point) do |f|
|
|
185
204
|
f.sequence(:id, "a")
|
|
186
|
-
f.x { rand(-
|
|
187
|
-
f.y { rand(-
|
|
188
|
-
|
|
189
|
-
f.
|
|
205
|
+
f.x { rand(-variance..variance) }
|
|
206
|
+
f.y { rand(-variance..variance) }
|
|
207
|
+
|
|
208
|
+
f.variance { 0.5 }
|
|
209
|
+
|
|
210
|
+
f.trait :z do f.variance { 0 } end
|
|
190
211
|
end
|
|
191
212
|
```
|
|
192
213
|
|
|
193
214
|
Now, this forgeyard can be used just like the default one:
|
|
215
|
+
|
|
194
216
|
```ruby
|
|
195
217
|
forgeyard.forge(:dot, :z, id: "0")
|
|
196
218
|
# => #<struct Point id="0", x=0, y=0>
|
|
197
219
|
```
|
|
198
220
|
|
|
221
|
+
And the forge can be referenced on its own:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
dot_forge = forgeyard[:dot]
|
|
225
|
+
dot_forge.forge
|
|
226
|
+
# => #<struct Point id="a", x=0.3958959145276243, y=-0.04519596671967796>
|
|
227
|
+
```
|
|
228
|
+
|
|
199
229
|
Note how the forge isn't registered in the default forgeyard:
|
|
230
|
+
|
|
200
231
|
```ruby
|
|
201
232
|
ObjectForge.forge(:dot)
|
|
202
233
|
# KeyError: key not found
|
|
203
234
|
```
|
|
204
235
|
|
|
205
236
|
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:
|
|
237
|
+
|
|
206
238
|
```ruby
|
|
207
239
|
forge = ObjectForge::Forge.define(Point) do |f|
|
|
208
240
|
f.sequence(:id, "a")
|
|
@@ -214,6 +246,7 @@ end
|
|
|
214
246
|
```
|
|
215
247
|
|
|
216
248
|
**Forge** has the same building interface as a **Forgeyard**, but it doesn't have the name argument:
|
|
249
|
+
|
|
217
250
|
```ruby
|
|
218
251
|
forge.build
|
|
219
252
|
# => #<struct Point id="a", x=0.3317733939650964, y=-0.1363936629550252>
|
|
@@ -223,21 +256,122 @@ forge.(radius: 500)
|
|
|
223
256
|
# => #<struct Point id="c", x=-141, y=109>
|
|
224
257
|
```
|
|
225
258
|
|
|
259
|
+
> [!TIP]
|
|
260
|
+
>
|
|
261
|
+
> Calling a **Forge** directly, instead of through **Forgeyard**, is faster due to not needing argument forwarding.
|
|
262
|
+
|
|
263
|
+
### Association-like nested objects
|
|
264
|
+
|
|
265
|
+
Nested objects can naturally be constructed in attribute definitions. However, forges defined through forgeyards provide a more convenient way to refer to their forgeyard in attribute definitions. It is fairly simple to build object graphs by putting related forges in the same `yard`:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
geometric_yard = ObjectForge::Forgeyard.new
|
|
269
|
+
geometric_yard.define(:point, Point) do |f|
|
|
270
|
+
# ... reusing the Point forge from the forgeyard example above.
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
Ellipse = Data.define(:center, :major_semiaxis, :minor_semiaxis)
|
|
274
|
+
geometric_yard.define(:circle, Ellipse) do |f|
|
|
275
|
+
# Current forge's forgeyard is available as `yard` pseudo-attribute:
|
|
276
|
+
f.center { yard.forge(:point) }
|
|
277
|
+
f.transient(:radius) { 1.0 }
|
|
278
|
+
|
|
279
|
+
f.major_semiaxis { radius }
|
|
280
|
+
f.minor_semiaxis { radius }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
geometric_yard.forge(:circle, radius: 4.0)
|
|
284
|
+
# => #<data Ellipse center=#<struct Point id="a", x=0.3487797161039954, y=-0.11378243307810132>, major_semiaxis=4.0, minor_semiaxis=4.0>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
There is an alternative way to refer to `yard`'s forges if no attribute has the same name — just mention it by name directly:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
Line = Data.define(:a, :b)
|
|
291
|
+
geometric_yard.define(:segment, Line) do |f|
|
|
292
|
+
# There is no "point" attribute, so `point` refers to the forge:
|
|
293
|
+
f.a { point.forge(:z) }
|
|
294
|
+
f.b { point.forge }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
geometric_yard.forge(:segment)
|
|
298
|
+
# => #<data Line a=#<struct Point id="b", x=0, y=0>, b=#<struct Point id="c", x=0.2701288765117644, y=0.008413574136415414>>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
> [!IMPORTANT]
|
|
302
|
+
>
|
|
303
|
+
> Referring to a related forge by name is not special syntax the same way **FactoryBot**'s _associations_ are; it just returns the forge object, which then needs to be called to construct an instance.
|
|
304
|
+
|
|
305
|
+
As objects are not initialized before attributes are resolved and set, it can be tricky to create circular references. If you find yourself needing to do this, an [after-forge hook](#after-build-customization) can be used to modify objects after building them.
|
|
306
|
+
|
|
307
|
+
### Defining final attribute list
|
|
308
|
+
|
|
309
|
+
Depending on what you are forging and the [mold](#molds-configuring-object-construction) used, you may need to limit the attributes that are passed to the forged instance. This can be done by using either `transient` attributes or the `attribute_list` option in the forge definition. Both options are equivalent in the end, so the choice is yours.
|
|
310
|
+
|
|
311
|
+
#### Transient attributes
|
|
312
|
+
|
|
313
|
+
Transient attributes can be defined using the `transient` method or `transient: true` argument. This automatically sets up attribute list to exclude the attribute, but otherwise doesn't change the behavior.
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
# Note that this forge is forging a Hash, not a Struct.
|
|
317
|
+
ObjectForge.define(:point, Hash) do |f|
|
|
318
|
+
# Transient "radius" is excluded from final attribute list:
|
|
319
|
+
f.transient(:radius) { 0.5 }
|
|
320
|
+
# Sequences can be transient too:
|
|
321
|
+
f.sequence(:s, transient: true) { |s| s * 30 }
|
|
322
|
+
|
|
323
|
+
f.x { s + rand(-radius..radius) }
|
|
324
|
+
f.y { s + rand(-radius..radius) }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
ObjectForge.forge(:point)
|
|
328
|
+
# => {x: 30.092699961573118, y: 29.71344463733288}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### Attribute list
|
|
332
|
+
|
|
333
|
+
`transient` attributes are really just a convenient shortcut to specifying `attribute_list` option. Manually setting the list can be handy if uniform attribute definitions are desired, it is semantically meaningful to allowlist attributes rather than deny individually, or transient attributes don't appear in the definition. `attribute_list` can also be useful to define attribute ordering.
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
ObjectForge.define(:point, Hash) do |f|
|
|
337
|
+
f.attribute_list = %i[x y z]
|
|
338
|
+
# Parameters:
|
|
339
|
+
f.unit { :m } # Meters by default
|
|
340
|
+
f.conversion { { mm: 10.0**0, m: 10.0**3, km: 10.0**6 } } # Conversion multipliers table
|
|
341
|
+
# Final attribute calculations:
|
|
342
|
+
f.x { position[0] * conversion[unit] }
|
|
343
|
+
f.z { position[1] * conversion[unit] }
|
|
344
|
+
f.y { altitude * conversion[unit] }
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Note how `y` comes out as the second attribute, not the third:
|
|
348
|
+
ObjectForge.forge(:point, position: [10, 13.4], altitude: 5)
|
|
349
|
+
# => {x: 10000.0, y: 5000.0, z: 13400.0}
|
|
350
|
+
ObjectForge.forge(:point, position: [10, 13.4], altitude: 5, unit: :mm)
|
|
351
|
+
# => {x: 10.0, y: 5.0, z: 13.4}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
> [!NOTE]
|
|
355
|
+
>
|
|
356
|
+
> `attribute_list` and `transient` attributes can be used in the same definition. However, transient attributes **can't** appear in attribute list; this will raise an error.
|
|
357
|
+
|
|
226
358
|
### Molds: configuring object construction
|
|
227
359
|
|
|
228
360
|
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.
|
|
229
361
|
|
|
230
362
|
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:
|
|
363
|
+
|
|
231
364
|
```ruby
|
|
232
365
|
forge = ObjectForge::Forge.define(Point) do |f|
|
|
233
366
|
f.mold = ->(forge_target:, attributes:, **) do
|
|
234
367
|
forge_target.new(attributes[:id], attributes[:x].round(3), attributes[:y].round(3))
|
|
235
368
|
end
|
|
236
|
-
#... rest of the definition from the
|
|
369
|
+
#... rest of the definition from the Basics example
|
|
237
370
|
end
|
|
238
371
|
```
|
|
239
372
|
|
|
240
373
|
Now the specified **mold** will be called to build your objects:
|
|
374
|
+
|
|
241
375
|
```ruby
|
|
242
376
|
forge.forge
|
|
243
377
|
# => #<struct Point id="a", x=0.331, y=-0.136>
|
|
@@ -245,25 +379,33 @@ forge.forge
|
|
|
245
379
|
|
|
246
380
|
Of course, you can abuse this to your heart's content. Look at the documentation for `ObjectForge::Molds` for inspiration.
|
|
247
381
|
|
|
382
|
+
> [!NOTE]
|
|
383
|
+
>
|
|
384
|
+
> If you don't specify a mold, **ObjectForge** will infer one for core data containers including **Hash**, **Array**, **Struct**, and **Data** subclasses.
|
|
385
|
+
|
|
248
386
|
**ObjectForge** comes pre-equipped with a selection of molds for common cases:
|
|
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
387
|
|
|
254
|
-
|
|
255
|
-
|
|
388
|
+
- `ObjectForge::Molds::SingleArgumentMold` (*the default*) calls `new(attributes)`, suitable for **ActiveModel**-style objects and **Dry::Struct**, as an example.
|
|
389
|
+
- `ObjectForge::Molds::KeywordsMold` calls `new(**attributes)`, suitable for **Data** and similar classes.
|
|
390
|
+
- `ObjectForge::Molds::StructMold` handles all possible cases of `keyword_init` for **Struct**s.
|
|
391
|
+
- `ObjectForge::Molds::HashMold` allows building **Hash** (including subclasses), including setting `default` and `default_proc` values.
|
|
392
|
+
- `ObjectForge::Molds::ArrayMold` allows building **Array** (including subclasses), based on attribute ordering.
|
|
393
|
+
|
|
394
|
+
You can also set a Class with a `#call` method as a mold. It will be instantiated on every call, providing a clean mold object.
|
|
256
395
|
|
|
257
396
|
> [!TIP]
|
|
258
|
-
>
|
|
397
|
+
>
|
|
398
|
+
> It is recommended to use mold instances. Using classes causes memory churn and lowers performance. Not only that, but having a stateful mold is a code smell.
|
|
259
399
|
|
|
260
400
|
### After-build customization
|
|
261
401
|
|
|
262
402
|
If there is a need to modify the object or perform additional actions after it is forged, there are two mechanisms you can employ:
|
|
403
|
+
|
|
263
404
|
- after-forge hook
|
|
264
405
|
- customization block
|
|
265
406
|
|
|
266
407
|
After-forge hook is a `call`able object specified as part of forge definition. It runs every time forging happens:
|
|
408
|
+
|
|
267
409
|
```ruby
|
|
268
410
|
forge = ObjectForge::Forge.define(Rectangle) do |f|
|
|
269
411
|
# can also be specified as `after_build`
|
|
@@ -276,6 +418,7 @@ forge.forge
|
|
|
276
418
|
```
|
|
277
419
|
|
|
278
420
|
Customization block is an optional block argument to `#forge` and is only executed in that specific invocation:
|
|
421
|
+
|
|
279
422
|
```ruby
|
|
280
423
|
forge.forge { |rect| RectangleRepository.save(rect); puts "persisted!" }
|
|
281
424
|
# Used 621 sq. units
|
|
@@ -283,38 +426,34 @@ forge.forge { |rect| RectangleRepository.save(rect); puts "persisted!" }
|
|
|
283
426
|
# => [23x27]
|
|
284
427
|
```
|
|
285
428
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
**ObjectForge** is pretty fast for what it is. However, if you are worried, there are certain things that can be done to make it faster.
|
|
291
|
-
- The easiest thing is to enable [**YJIT**](https://docs.ruby-lang.org/en/master/yjit/yjit_md.html). It will probably speed up your whole application, but be aware that it is not always suitable and may even degrade performance on some workloads. It *is* considered production-ready though.
|
|
292
|
-
- Calling a **Forge** directly, instead of through **Forgeyard**, is faster due to not needing argument forwarding. This is consistent (but check on your system anyway!).
|
|
293
|
-
- Using `self[:name]` instead of plain `name` inside attribute definitions does not engage dynamic method dispatch, which *should* be faster. However, micro-benchmarking does not show conclusive results.
|
|
429
|
+
> [!NOTE]
|
|
430
|
+
>
|
|
431
|
+
> If both hook and block are used, the hook runs before the block.
|
|
294
432
|
|
|
295
433
|
## Differences and limitations (compared to FactoryBot)
|
|
296
434
|
|
|
297
435
|
If you are used to FactoryBot, be aware that there are quite a few differences in specifics.
|
|
298
436
|
|
|
299
437
|
General:
|
|
438
|
+
|
|
300
439
|
- 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`).
|
|
301
440
|
- `Forgeyard.define` *is* the forge definition block, there is no separate `factory` block.
|
|
441
|
+
- There is no concept of associations, or magic association methods. Forgeyards provide similar functionality, but it is more explicit.
|
|
302
442
|
|
|
303
443
|
Forge definition:
|
|
444
|
+
|
|
304
445
|
- Class specification for a forge is non-optional, there is no assumption about the class name.
|
|
305
446
|
- 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
447
|
- There is no forge inheritance or nesting.
|
|
307
448
|
|
|
308
|
-
Attributes:
|
|
309
|
-
- Currently, there is no concept of transient attributes. Attribute selection needs to be handled by the mold.
|
|
310
|
-
- *There are no associations*. If nested objects are required, they should be created and set in the block for the attribute.
|
|
311
|
-
|
|
312
449
|
Traits:
|
|
450
|
+
|
|
313
451
|
- Traits can't be defined inside of other traits.
|
|
314
452
|
- Traits can't be called from other traits. This may change in the future.
|
|
315
453
|
- There are no default traits.
|
|
316
454
|
|
|
317
455
|
Sequences:
|
|
456
|
+
|
|
318
457
|
- There is no explicit way to define shared sequences, but a freestanding `Sequence` can be created manually and passed into `sequence` calls.
|
|
319
458
|
- Sequences work with values implementing `#succ`, not `#next`, expressly prohibiting `Enumerator`. This may be relaxed in the future.
|
|
320
459
|
|
|
@@ -330,11 +469,13 @@ kanban
|
|
|
330
469
|
[Thread-safe behavior]
|
|
331
470
|
[Tapping into built objects for post-processing]
|
|
332
471
|
[Custom builders / molds]
|
|
333
|
-
[Built-in Hash, Struct, Data builders / molds]
|
|
472
|
+
[Built-in Hash, Array, Struct, Data builders / molds]
|
|
334
473
|
[Ability to replace resolver]
|
|
335
474
|
[After-build hook]
|
|
336
|
-
[⚗️ To do]
|
|
337
475
|
[Transient attributes / attribute filtering]
|
|
476
|
+
[Reference to forgeyard in forge / crucible resolution]
|
|
477
|
+
[⚗️ To do]
|
|
478
|
+
[Equality comparisons]
|
|
338
479
|
[❔ Maybe, maybe not]
|
|
339
480
|
[Calling traits from traits]
|
|
340
481
|
[Default traits]
|
|
@@ -369,4 +510,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/trinis
|
|
|
369
510
|
|
|
370
511
|
## License
|
|
371
512
|
|
|
372
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT), see [LICENSE.txt](https://github.com/trinistr/object_forge/blob/main/LICENSE.txt).
|
|
513
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT), see [LICENSE.txt](https://github.com/trinistr/object_forge/blob/main/LICENSE.txt).
|
|
@@ -14,6 +14,8 @@ module ObjectForge
|
|
|
14
14
|
# and using {.call} is thread-safe.
|
|
15
15
|
# @since 0.1.0
|
|
16
16
|
class Crucible < UnBasicObject
|
|
17
|
+
EMPTY_YARD = {}.freeze # steep:ignore UnannotatedEmptyCollection
|
|
18
|
+
|
|
17
19
|
class << self
|
|
18
20
|
# Resolve all attributes by calling their +Proc+s,
|
|
19
21
|
# using a new instance as evaluation context.
|
|
@@ -23,9 +25,10 @@ module ObjectForge
|
|
|
23
25
|
# @thread_safety This method is thread-safe.
|
|
24
26
|
#
|
|
25
27
|
# @param attributes [Hash{Symbol => Proc, Any}] initial attributes
|
|
28
|
+
# @param yard [Hash{Symbol => Any}, nil] additional context for the crucible
|
|
26
29
|
# @return [Hash{Symbol => Any}] resolved attributes
|
|
27
|
-
def call(attributes)
|
|
28
|
-
new(attributes).resolve!
|
|
30
|
+
def call(attributes, yard: EMPTY_YARD)
|
|
31
|
+
new(attributes, yard:).resolve!
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
alias resolve call
|
|
@@ -33,10 +36,15 @@ module ObjectForge
|
|
|
33
36
|
|
|
34
37
|
%i[rand].each { |m| private define_method(m, ::Kernel.instance_method(m)) }
|
|
35
38
|
|
|
39
|
+
# @return [Hash{Symbol => Any}, Forgeyard] additional context for the crucible
|
|
40
|
+
attr_reader :yard
|
|
41
|
+
|
|
36
42
|
# @param attributes [Hash{Symbol => Proc, Any}] initial attributes
|
|
37
|
-
|
|
43
|
+
# @param yard [Hash{Symbol => Any}, nil] additional context for the crucible
|
|
44
|
+
def initialize(attributes, yard: EMPTY_YARD)
|
|
38
45
|
super()
|
|
39
46
|
@attributes = attributes
|
|
47
|
+
@yard = yard || EMPTY_YARD
|
|
40
48
|
@resolved_attributes = ::Set.new
|
|
41
49
|
@resolving_attributes = []
|
|
42
50
|
end
|
|
@@ -93,6 +101,8 @@ module ObjectForge
|
|
|
93
101
|
raise_circular_dependency_error!(name) if @resolving_attributes.include?(name)
|
|
94
102
|
resolve_attribute!(name) unless @resolved_attributes.include?(name)
|
|
95
103
|
@attributes[name]
|
|
104
|
+
elsif @yard.key?(name)
|
|
105
|
+
@yard[name]
|
|
96
106
|
else
|
|
97
107
|
super
|
|
98
108
|
end
|
|
@@ -101,7 +111,7 @@ module ObjectForge
|
|
|
101
111
|
alias [] method_missing
|
|
102
112
|
|
|
103
113
|
def respond_to_missing?(name, _include_all)
|
|
104
|
-
@attributes.key?(name) || super
|
|
114
|
+
@attributes.key?(name) || @yard.key?(name) || super
|
|
105
115
|
end
|
|
106
116
|
|
|
107
117
|
def raise_circular_dependency_error!(name)
|
data/lib/object_forge/forge.rb
CHANGED
|
@@ -29,10 +29,12 @@ module ObjectForge
|
|
|
29
29
|
# @!attribute [r] options
|
|
30
30
|
# A forge's options.
|
|
31
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
32
|
# - +:crucible+ — a +call+able object that knows how to resolve attributes,
|
|
35
|
-
# taking a hash of initial attributes.
|
|
33
|
+
# taking a hash of initial attributes and possibly a yard (see {Crucible}).
|
|
34
|
+
# - +:attribute_list+ — an array of attribute names to filter and sort by
|
|
35
|
+
# before passing attributes to the mold.
|
|
36
|
+
# - +:mold+ — a +call+able object that knows how to build the instance,
|
|
37
|
+
# taking a class and a hash of attributes (see {Molds}).
|
|
36
38
|
# - +:after_forge+/+:after_build+ — a +call+able object that is passed
|
|
37
39
|
# the forged instance and can do anything with it.
|
|
38
40
|
# @since 0.3.0
|
|
@@ -46,16 +48,20 @@ module ObjectForge
|
|
|
46
48
|
#
|
|
47
49
|
# @param forge_target [Class, Any] class or object to forge
|
|
48
50
|
# @param name [Symbol, nil] forge name
|
|
51
|
+
# @param yard [Forgeyard, nil] forgeyard this forge belongs to
|
|
49
52
|
# @yieldparam dsl [ForgeDSL]
|
|
50
53
|
# @yieldreturn [void]
|
|
51
54
|
# @return [Forge] forge
|
|
52
|
-
def self.define(forge_target, name: nil, &)
|
|
53
|
-
new(forge_target, ForgeDSL.new(&), name:)
|
|
55
|
+
def self.define(forge_target, name: nil, yard: nil, &)
|
|
56
|
+
new(forge_target, ForgeDSL.new(&), name:, yard:)
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
# @return [Symbol, nil] forge name, only used for identification purposes
|
|
57
60
|
attr_reader :name
|
|
58
61
|
|
|
62
|
+
# @return [Forgeyard, nil] forgeyard this forge belongs to
|
|
63
|
+
attr_reader :yard
|
|
64
|
+
|
|
59
65
|
# @return [Class, Any] class or object to forge
|
|
60
66
|
# @since 0.4.0
|
|
61
67
|
attr_reader :forge_target
|
|
@@ -68,11 +74,13 @@ module ObjectForge
|
|
|
68
74
|
# will be passed to mold as +forge_target+ argument
|
|
69
75
|
# @param parameters [Parameters, ForgeDSL] forge parameters
|
|
70
76
|
# @param name [Symbol, nil] forge name
|
|
77
|
+
# @param yard [Forgeyard, nil] forgeyard this forge belongs to
|
|
71
78
|
#
|
|
72
79
|
# @raise [ObjectInterfaceError] if forge options do not have expected interface;
|
|
73
80
|
# see {Parameters#options} for details
|
|
74
|
-
def initialize(forge_target, parameters, name: nil)
|
|
81
|
+
def initialize(forge_target, parameters, name: nil, yard: nil)
|
|
75
82
|
@name = name
|
|
83
|
+
@yard = yard
|
|
76
84
|
@forge_target = forge_target
|
|
77
85
|
@parameters = parameters
|
|
78
86
|
|
|
@@ -80,12 +88,12 @@ module ObjectForge
|
|
|
80
88
|
@crucible = determine_crucible(options)
|
|
81
89
|
@mold = determine_mold(forge_target, options)
|
|
82
90
|
@after_forge_hook = determine_after_forge_hook(options)
|
|
91
|
+
validate_other_options(options)
|
|
83
92
|
end
|
|
84
93
|
|
|
85
94
|
# Forge a new instance, applying attributes to forge target.
|
|
86
95
|
#
|
|
87
96
|
# Positional arguments are taken as trait names, keyword arguments as attribute overrides.
|
|
88
|
-
#
|
|
89
97
|
# All traits and overrides are applied in argument order,
|
|
90
98
|
# with overrides always applied after traits.
|
|
91
99
|
#
|
|
@@ -102,8 +110,8 @@ module ObjectForge
|
|
|
102
110
|
#
|
|
103
111
|
# @raise [ArgumentError] if a trait name is unknown
|
|
104
112
|
def forge(*traits, **overrides)
|
|
105
|
-
|
|
106
|
-
instance = @mold.call(forge_target: @forge_target, attributes:
|
|
113
|
+
attributes = build_attribute_hash(traits, overrides)
|
|
114
|
+
instance = @mold.call(forge_target: @forge_target, attributes: attributes)
|
|
107
115
|
@after_forge_hook&.call(instance)
|
|
108
116
|
yield instance if block_given?
|
|
109
117
|
instance
|
|
@@ -115,9 +123,10 @@ module ObjectForge
|
|
|
115
123
|
private
|
|
116
124
|
|
|
117
125
|
# Get a crucible object based on parameters.
|
|
118
|
-
#
|
|
119
126
|
# It's either the object provided in options, or {Crucible}.
|
|
120
127
|
#
|
|
128
|
+
# This method also determines whether the crucible accepts a +yard+ parameter.
|
|
129
|
+
#
|
|
121
130
|
# @param options [Hash]
|
|
122
131
|
# @option options [#call, nil] :crucible
|
|
123
132
|
# @return [#call]
|
|
@@ -126,15 +135,30 @@ module ObjectForge
|
|
|
126
135
|
#
|
|
127
136
|
# @since 0.4.0
|
|
128
137
|
def determine_crucible(options)
|
|
129
|
-
|
|
138
|
+
@crucible_takes_yard = true and return Crucible unless options[:crucible]
|
|
130
139
|
|
|
140
|
+
crucible = options[:crucible]
|
|
131
141
|
unless crucible.respond_to?(:call)
|
|
132
142
|
raise ObjectInterfaceError, "crucible must respond to #call"
|
|
133
143
|
end
|
|
134
144
|
|
|
145
|
+
@crucible_takes_yard = crucible_takes_yard_parameter?(crucible)
|
|
135
146
|
crucible
|
|
136
147
|
end
|
|
137
148
|
|
|
149
|
+
# Check if a crucible accepts a +yard+ keyword parameter.
|
|
150
|
+
#
|
|
151
|
+
# @param crucible [#call]
|
|
152
|
+
# @return [Boolean]
|
|
153
|
+
#
|
|
154
|
+
# @since 0.5.0
|
|
155
|
+
def crucible_takes_yard_parameter?(crucible)
|
|
156
|
+
parameters = (Proc === crucible) ? crucible.parameters : crucible.method(:call).parameters
|
|
157
|
+
parameters.any? do |type, name|
|
|
158
|
+
type == :keyrest || ((type == :keyreq || type == :key) && name == :yard)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
138
162
|
# Get appropriate mold based on parameters.
|
|
139
163
|
#
|
|
140
164
|
# If +mold+ is already set, it will be used directly, or,
|
|
@@ -176,6 +200,35 @@ module ObjectForge
|
|
|
176
200
|
hook
|
|
177
201
|
end
|
|
178
202
|
|
|
203
|
+
# Validate options that will only be used at runtime.
|
|
204
|
+
#
|
|
205
|
+
# @param options [Hash]
|
|
206
|
+
# @option options [Array<Symbol>, nil] :attribute_list
|
|
207
|
+
#
|
|
208
|
+
# @raise [TypeError]
|
|
209
|
+
#
|
|
210
|
+
# @since 0.5.0
|
|
211
|
+
def validate_other_options(options)
|
|
212
|
+
return if options[:attribute_list].nil?
|
|
213
|
+
unless Array === options[:attribute_list] && options[:attribute_list].all? { Symbol === _1 }
|
|
214
|
+
raise TypeError, "attribute_list must be an Array of Symbol"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Build final attribute hash using default attributes, specified traits and overrides,
|
|
219
|
+
# sorted and filtered by attribute list.
|
|
220
|
+
#
|
|
221
|
+
# @param traits [Array<Symbol>]
|
|
222
|
+
# @param overrides [Hash{Symbol => Proc, Any}]
|
|
223
|
+
# @return [Hash{Symbol => Any}]
|
|
224
|
+
#
|
|
225
|
+
# @raise [ArgumentError]
|
|
226
|
+
#
|
|
227
|
+
# @since 0.5.0
|
|
228
|
+
def build_attribute_hash(traits, overrides)
|
|
229
|
+
apply_attribute_list(resolve_attributes(traits, overrides))
|
|
230
|
+
end
|
|
231
|
+
|
|
179
232
|
# Resolve attributes using default attributes, specified traits and overrides.
|
|
180
233
|
#
|
|
181
234
|
# @param traits [Array<Symbol>]
|
|
@@ -191,7 +244,24 @@ module ObjectForge
|
|
|
191
244
|
|
|
192
245
|
trait_attributes = @parameters.traits.values_at(*traits) # : Array[Hash[Symbol, ObjectForge::attribute]]
|
|
193
246
|
attributes = @parameters.attributes.merge(*trait_attributes, overrides)
|
|
194
|
-
|
|
247
|
+
|
|
248
|
+
if @crucible_takes_yard
|
|
249
|
+
@crucible.call(attributes, yard: @yard) # steep:ignore UnexpectedKeywordArgument
|
|
250
|
+
else
|
|
251
|
+
@crucible.call(attributes)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Filter and sort attributes based on the attribute list.
|
|
256
|
+
#
|
|
257
|
+
# @param attributes [Hash{Symbol => Any}]
|
|
258
|
+
# @return [Hash{Symbol => Any}]
|
|
259
|
+
#
|
|
260
|
+
# @since 0.5.0
|
|
261
|
+
def apply_attribute_list(attributes)
|
|
262
|
+
return attributes unless (attribute_list = @parameters.options[:attribute_list])
|
|
263
|
+
|
|
264
|
+
attributes.slice(*attribute_list)
|
|
195
265
|
end
|
|
196
266
|
end
|
|
197
267
|
end
|
|
@@ -12,12 +12,11 @@ module ObjectForge
|
|
|
12
12
|
# but it's not a private API.
|
|
13
13
|
#
|
|
14
14
|
# @thread_safety DSL is not thread-safe.
|
|
15
|
-
# Take care not to introduce side effects,
|
|
16
|
-
#
|
|
17
|
-
# The instance itself is frozen after initialization,
|
|
15
|
+
# Take care not to introduce side effects, especially in attribute definitions.
|
|
16
|
+
# The instance itself and its attributes are frozen after initialization,
|
|
18
17
|
# so it should be safe to share.
|
|
19
18
|
# @since 0.1.0
|
|
20
|
-
class ForgeDSL < UnBasicObject
|
|
19
|
+
class ForgeDSL < UnBasicObject # rubocop:disable Metrics/ClassLength
|
|
21
20
|
# @return [Hash{Symbol => Proc}] attribute definitions
|
|
22
21
|
attr_reader :attributes
|
|
23
22
|
|
|
@@ -61,9 +60,11 @@ module ObjectForge
|
|
|
61
60
|
@sequences = {}
|
|
62
61
|
@traits = {}
|
|
63
62
|
@options = {}
|
|
63
|
+
@transient_attributes = []
|
|
64
64
|
|
|
65
65
|
dsl.arity.zero? ? instance_exec(&dsl) : yield(self)
|
|
66
66
|
|
|
67
|
+
shape_attribute_list!
|
|
67
68
|
freeze
|
|
68
69
|
end
|
|
69
70
|
|
|
@@ -79,6 +80,7 @@ module ObjectForge
|
|
|
79
80
|
@sequences.freeze
|
|
80
81
|
@traits.freeze
|
|
81
82
|
@options.freeze
|
|
83
|
+
@options[:attribute_list].freeze
|
|
82
84
|
self
|
|
83
85
|
end
|
|
84
86
|
|
|
@@ -134,13 +136,20 @@ module ObjectForge
|
|
|
134
136
|
# f.attribute(:[]=) { "#{self[:[]]} are brackets" }
|
|
135
137
|
# f.attribute(:!) { "#{self[:[]=]}!" }
|
|
136
138
|
#
|
|
139
|
+
# @example using transient attributes
|
|
140
|
+
# f.attribute(:range) { (base - delta)..(base + delta) }
|
|
141
|
+
# f.attribute(:base, transient: true) { 1 }
|
|
142
|
+
# f.transient(:delta) { 0.05 }
|
|
143
|
+
#
|
|
137
144
|
# @param name [Symbol] attribute name
|
|
145
|
+
# @param transient [Boolean] whether the attribute is transient
|
|
146
|
+
# (automatically excluded from attribute list)
|
|
138
147
|
# @yieldreturn [Any] attribute value
|
|
139
148
|
# @return [Symbol] attribute name
|
|
140
149
|
#
|
|
141
150
|
# @raise [TypeError] if +name+ is not a Symbol
|
|
142
151
|
# @raise [DSLError] if no block is given
|
|
143
|
-
def attribute(name, &definition)
|
|
152
|
+
def attribute(name, transient: false, &definition)
|
|
144
153
|
unless ::Symbol === name
|
|
145
154
|
raise ::TypeError,
|
|
146
155
|
"attribute name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
@@ -154,12 +163,20 @@ module ObjectForge
|
|
|
154
163
|
else
|
|
155
164
|
@attributes[name] = definition
|
|
156
165
|
end
|
|
166
|
+
@transient_attributes << name if transient
|
|
157
167
|
|
|
158
168
|
name
|
|
159
169
|
end
|
|
160
170
|
|
|
161
171
|
alias [] attribute
|
|
162
172
|
|
|
173
|
+
# Define a transient attribute.
|
|
174
|
+
#
|
|
175
|
+
# @see #attribute
|
|
176
|
+
def transient(name, &)
|
|
177
|
+
attribute(name, transient: true, &)
|
|
178
|
+
end
|
|
179
|
+
|
|
163
180
|
# Define an attribute, using a sequence.
|
|
164
181
|
#
|
|
165
182
|
# +name+ is used for both attribute and sequence, for the whole forge.
|
|
@@ -183,13 +200,15 @@ module ObjectForge
|
|
|
183
200
|
#
|
|
184
201
|
# @param name [Symbol] attribute name
|
|
185
202
|
# @param initial [Sequence, #succ] existing sequence, or initial value for a new sequence
|
|
203
|
+
# @param transient [Boolean] whether the attribute is transient
|
|
204
|
+
# (automatically excluded from attribute list)
|
|
186
205
|
# @yieldparam value [#succ] current value of the sequence to calculate attribute value
|
|
187
206
|
# @yieldreturn [Any] attribute value
|
|
188
207
|
# @return [Symbol] attribute name
|
|
189
208
|
#
|
|
190
209
|
# @raise [TypeError] if +name+ is not a Symbol
|
|
191
210
|
# @raise [ObjectInterfaceError] if +initial+ does not respond to #succ and is not a {Sequence}
|
|
192
|
-
def sequence(name, initial = 1,
|
|
211
|
+
def sequence(name, initial = 1, transient: false, &)
|
|
193
212
|
unless ::Symbol === name
|
|
194
213
|
raise ::TypeError,
|
|
195
214
|
"sequence name must be a Symbol, #{name.class} given (in #{name.inspect})"
|
|
@@ -198,9 +217,11 @@ module ObjectForge
|
|
|
198
217
|
seq = @sequences[name] ||= Sequence.new(initial)
|
|
199
218
|
|
|
200
219
|
if block_given?
|
|
201
|
-
attribute(name
|
|
220
|
+
attribute(name, transient: transient) do
|
|
221
|
+
instance_exec(seq.next, &) # steep:ignore BlockTypeMismatch
|
|
222
|
+
end
|
|
202
223
|
else
|
|
203
|
-
attribute(name) { seq.next }
|
|
224
|
+
attribute(name, transient: transient) { seq.next }
|
|
204
225
|
end
|
|
205
226
|
|
|
206
227
|
name
|
|
@@ -278,7 +299,7 @@ module ObjectForge
|
|
|
278
299
|
# - all names ending in +?+, +!+
|
|
279
300
|
# - all names starting with a non-word ASCII character
|
|
280
301
|
# (operators, +`+, +[]+, +[]=+)
|
|
281
|
-
# - +rand+
|
|
302
|
+
# - +rand+ and +yard+
|
|
282
303
|
#
|
|
283
304
|
# @param name [Symbol] attribute or option name
|
|
284
305
|
# @param value [Any] value for option
|
|
@@ -302,11 +323,25 @@ module ObjectForge
|
|
|
302
323
|
def respond_to_missing?(name, _include_all)
|
|
303
324
|
return super if frozen?
|
|
304
325
|
|
|
305
|
-
!name.end_with?("?", "!") && !name.match?(/\A(?=\p{ASCII})\P{Word}/) &&
|
|
326
|
+
!name.end_with?("?", "!") && !name.match?(/\A(?=\p{ASCII})\P{Word}/) &&
|
|
327
|
+
!name.match?(/\A(?:rand|yard)\z/)
|
|
306
328
|
end
|
|
307
329
|
|
|
308
330
|
def valid_option_method?(name)
|
|
309
331
|
name.match?(/\A\p{Word}.*=\z/)
|
|
310
332
|
end
|
|
333
|
+
|
|
334
|
+
def shape_attribute_list!
|
|
335
|
+
return if @transient_attributes.empty?
|
|
336
|
+
|
|
337
|
+
if @options.key?(:attribute_list)
|
|
338
|
+
return if (conflicting_attrs = @transient_attributes & @options[:attribute_list]).empty?
|
|
339
|
+
|
|
340
|
+
list = conflicting_attrs.map(&:inspect).join(", ")
|
|
341
|
+
raise DSLError, "attribute_list must not include transient attributes (#{list})"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
@options[:attribute_list] = @attributes.merge(*@traits.values).keys - @transient_attributes
|
|
345
|
+
end
|
|
311
346
|
end
|
|
312
347
|
end
|
|
@@ -27,7 +27,7 @@ module ObjectForge
|
|
|
27
27
|
# @yieldreturn [void]
|
|
28
28
|
# @return [Forge] forge
|
|
29
29
|
def define(name, forge_target, &)
|
|
30
|
-
register(name, Forge.define(forge_target, name: name, &))
|
|
30
|
+
register(name, Forge.define(forge_target, name: name, yard: self, &))
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Add a forge under a specified name.
|
|
@@ -55,6 +55,14 @@ module ObjectForge
|
|
|
55
55
|
@forges.fetch(name)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# Check if a forge is registered under a given name.
|
|
59
|
+
#
|
|
60
|
+
# @param name [Symbol] name of the forge
|
|
61
|
+
# @return [Boolean] whether a forge is registered under the given name
|
|
62
|
+
def key?(name)
|
|
63
|
+
!!@forges.get(name)
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
# Build an instance using a forge.
|
|
59
67
|
#
|
|
60
68
|
# @see Forge#forge
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ObjectForge
|
|
4
|
+
module Molds
|
|
5
|
+
# Mold for constructing Arrays.
|
|
6
|
+
#
|
|
7
|
+
# @thread_safety Thread-safe.
|
|
8
|
+
# @since 0.5.0
|
|
9
|
+
class ArrayMold
|
|
10
|
+
# Build a new array from attributes' values.
|
|
11
|
+
#
|
|
12
|
+
# If +forge_target+ is +Array+, result is built directly by calling +attributes.values+.
|
|
13
|
+
# If it is a different class, its +.new+ method is called with the values array.
|
|
14
|
+
#
|
|
15
|
+
# @see Array.new
|
|
16
|
+
#
|
|
17
|
+
# @param forge_target [Class] Array or a subclass of Array
|
|
18
|
+
# @param attributes [Hash{Symbol => Any}]
|
|
19
|
+
# @return [Array]
|
|
20
|
+
def call(forge_target:, attributes:, **_)
|
|
21
|
+
return attributes.values if Array == forge_target # rubocop:disable Style/YodaCondition
|
|
22
|
+
|
|
23
|
+
forge_target.new(attributes.values)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/object_forge/molds.rb
CHANGED
|
@@ -122,7 +122,8 @@ module ObjectForge
|
|
|
122
122
|
# Currently provides specific recognition for:
|
|
123
123
|
# - subclasses of +Struct+ ({StructMold}),
|
|
124
124
|
# - subclasses of +Data+ ({KeywordsMold}),
|
|
125
|
-
# - +Hash+ and subclasses ({HashMold})
|
|
125
|
+
# - +Hash+ and subclasses ({HashMold}),
|
|
126
|
+
# - +Array+ and subclasses ({ArrayMold}).
|
|
126
127
|
# Other objects just get {SingleArgumentMold}.
|
|
127
128
|
#
|
|
128
129
|
# @param forge_target [Class, Any]
|
|
@@ -131,19 +132,22 @@ module ObjectForge
|
|
|
131
132
|
# @thread_safety Thread-safe.
|
|
132
133
|
# @since 0.3.0
|
|
133
134
|
def self.mold_for(forge_target)
|
|
134
|
-
|
|
135
|
+
return SingleArgumentMold.new unless ::Class === forge_target
|
|
136
|
+
|
|
137
|
+
mold_class =
|
|
135
138
|
if forge_target < ::Struct
|
|
136
|
-
StructMold
|
|
139
|
+
StructMold
|
|
137
140
|
elsif defined?(::Data) && forge_target < ::Data
|
|
138
|
-
KeywordsMold
|
|
141
|
+
KeywordsMold
|
|
139
142
|
elsif forge_target <= ::Hash
|
|
140
|
-
HashMold
|
|
143
|
+
HashMold
|
|
144
|
+
elsif forge_target <= ::Array
|
|
145
|
+
ArrayMold
|
|
141
146
|
else
|
|
142
|
-
SingleArgumentMold
|
|
147
|
+
SingleArgumentMold
|
|
143
148
|
end
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
149
|
+
|
|
150
|
+
mold_class.new
|
|
147
151
|
end
|
|
148
152
|
|
|
149
153
|
# Wrap mold if needed.
|
|
@@ -160,7 +164,7 @@ module ObjectForge
|
|
|
160
164
|
# @thread_safety Thread-safe.
|
|
161
165
|
# @since 0.3.0
|
|
162
166
|
def self.wrap_mold(mold)
|
|
163
|
-
if
|
|
167
|
+
if nil == mold || mold.respond_to?(:call) # rubocop:disable Style/YodaCondition
|
|
164
168
|
mold # : ObjectForge::mold?
|
|
165
169
|
elsif ::Class === mold && mold.public_method_defined?(:call)
|
|
166
170
|
WrappedMold.new(mold)
|
data/lib/object_forge/version.rb
CHANGED
data/sig/manifest.yaml
ADDED
data/sig/object_forge/molds.rbs
CHANGED
|
@@ -28,8 +28,8 @@ module ObjectForge
|
|
|
28
28
|
class StructMold
|
|
29
29
|
interface _StructSubclass[T]
|
|
30
30
|
def new
|
|
31
|
-
: (*
|
|
32
|
-
| (**
|
|
31
|
+
: (*ObjectForge::attribute) -> T
|
|
32
|
+
| (**ObjectForge::attribute) -> T
|
|
33
33
|
| (Hash[Symbol, ObjectForge::attribute]) -> T
|
|
34
34
|
def members: -> Array[Symbol]
|
|
35
35
|
def keyword_init?: -> bool?
|
|
@@ -60,10 +60,21 @@ module ObjectForge
|
|
|
60
60
|
attr_reader default_proc: Proc?
|
|
61
61
|
|
|
62
62
|
def initialize
|
|
63
|
-
:
|
|
63
|
+
: (?ObjectForge::attribute? default_value) ?{ (Hash[untyped, untyped] hash, untyped key) -> ObjectForge::attribute} -> void
|
|
64
64
|
|
|
65
65
|
def call
|
|
66
|
-
:
|
|
66
|
+
: (forge_target: singleton(Hash), attributes: Hash[Symbol, ObjectForge::attribute], **untyped) -> Hash[Symbol, ObjectForge::attribute]
|
|
67
|
+
| [T < Hash[Symbol, ObjectForge::attribute]] (forge_target: _HashSubclass[T], attributes: Hash[Symbol, ObjectForge::attribute], **untyped) -> T
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class ArrayMold
|
|
71
|
+
interface _ArraySubclass[T]
|
|
72
|
+
def new: (Array[ObjectForge::attribute]) -> T
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def call
|
|
76
|
+
: (forge_target: singleton(Array), attributes: Hash[Symbol, ObjectForge::attribute], **untyped) -> Array[ObjectForge::attribute]
|
|
77
|
+
| [T < Array[ObjectForge::attribute]] (forge_target: _ArraySubclass[T], attributes: Hash[Symbol, ObjectForge::attribute], **untyped) -> T
|
|
67
78
|
end
|
|
68
79
|
end
|
|
69
80
|
end
|
data/sig/object_forge.rbs
CHANGED
|
@@ -3,11 +3,13 @@ module ObjectForge
|
|
|
3
3
|
type forge_result = untyped
|
|
4
4
|
type attribute = untyped
|
|
5
5
|
|
|
6
|
-
type mold =
|
|
6
|
+
type mold = _RespondTo & _Mold
|
|
7
|
+
type crucible = _RespondTo & (_Crucible | _CrucibleWithYard)
|
|
7
8
|
type sequenceable = _RespondTo & _Sequenceable
|
|
8
9
|
|
|
9
10
|
interface _RespondTo
|
|
10
11
|
def respond_to?: (Symbol name, ?bool include_private) -> bool
|
|
12
|
+
def method: (Symbol name) -> ::Method
|
|
11
13
|
def class: -> Class
|
|
12
14
|
end
|
|
13
15
|
interface _Sequenceable
|
|
@@ -26,10 +28,21 @@ module ObjectForge
|
|
|
26
28
|
def call
|
|
27
29
|
: (Hash[Symbol, ObjectForge::attribute]) -> Hash[Symbol, ObjectForge::attribute]
|
|
28
30
|
end
|
|
31
|
+
interface _CrucibleWithYard
|
|
32
|
+
def call
|
|
33
|
+
: (Hash[Symbol, ObjectForge::attribute], ?yard: ObjectForge::_Yard?) -> Hash[Symbol, ObjectForge::attribute]
|
|
34
|
+
end
|
|
29
35
|
interface _Hook
|
|
30
36
|
def call
|
|
31
37
|
: (ObjectForge::forge_result) -> void
|
|
32
38
|
end
|
|
39
|
+
interface _Yard
|
|
40
|
+
def []
|
|
41
|
+
: (Symbol name) -> untyped
|
|
42
|
+
|
|
43
|
+
def key?
|
|
44
|
+
: (Symbol name) -> bool
|
|
45
|
+
end
|
|
33
46
|
|
|
34
47
|
class Error < StandardError
|
|
35
48
|
end
|
|
@@ -87,6 +100,9 @@ class ObjectForge::Forgeyard
|
|
|
87
100
|
|
|
88
101
|
def []
|
|
89
102
|
: (Symbol name) -> ObjectForge::Forge
|
|
103
|
+
|
|
104
|
+
def key?
|
|
105
|
+
: (Symbol name) -> bool
|
|
90
106
|
|
|
91
107
|
def forge
|
|
92
108
|
: (Symbol name, *Symbol traits, **ObjectForge::attribute overrides) ?{ (ObjectForge::forge_result) -> void } -> ObjectForge::forge_result
|
|
@@ -103,23 +119,25 @@ class ObjectForge::Forge
|
|
|
103
119
|
: (attributes: Hash[Symbol, ObjectForge::attribute], traits: Hash[Symbol, Hash[Symbol, ObjectForge::attribute]], options: Hash[Symbol, untyped]) -> void
|
|
104
120
|
end
|
|
105
121
|
|
|
122
|
+
attr_reader name: Symbol?
|
|
123
|
+
|
|
124
|
+
attr_reader yard: ObjectForge::_Yard?
|
|
125
|
+
|
|
106
126
|
attr_reader forge_target: ObjectForge::forge_target
|
|
107
127
|
alias target forge_target
|
|
108
128
|
|
|
109
|
-
attr_reader name: Symbol?
|
|
110
|
-
|
|
111
129
|
attr_reader parameters: ObjectForge::_ForgeParameters
|
|
112
130
|
|
|
113
|
-
@crucible: ObjectForge::
|
|
131
|
+
@crucible: ObjectForge::crucible
|
|
114
132
|
@mold: ObjectForge::_Mold
|
|
115
133
|
@after_forge_hook: ObjectForge::_Hook?
|
|
116
134
|
|
|
117
135
|
def self.define
|
|
118
|
-
: (ObjectForge::forge_target, ?name: Symbol?) { (ObjectForge::ForgeDSL) -> void } -> ObjectForge::Forge
|
|
119
|
-
| (ObjectForge::forge_target, ?name: Symbol?) { [self: ObjectForge::ForgeDSL] -> void } -> ObjectForge::Forge
|
|
136
|
+
: (ObjectForge::forge_target, ?name: Symbol?, ?yard: ObjectForge::_Yard?) { (ObjectForge::ForgeDSL) -> void } -> ObjectForge::Forge
|
|
137
|
+
| (ObjectForge::forge_target, ?name: Symbol?, ?yard: ObjectForge::_Yard?) { [self: ObjectForge::ForgeDSL] -> void } -> ObjectForge::Forge
|
|
120
138
|
|
|
121
139
|
def initialize
|
|
122
|
-
: (ObjectForge::forge_target, ObjectForge::_ForgeParameters parameters, ?name: Symbol?) -> void
|
|
140
|
+
: (ObjectForge::forge_target, ObjectForge::_ForgeParameters parameters, ?name: Symbol?, ?yard: ObjectForge::_Yard?) -> void
|
|
123
141
|
|
|
124
142
|
def forge
|
|
125
143
|
: (*Symbol traits, **ObjectForge::attribute overrides) ?{ (ObjectForge::forge_result) -> void } -> ObjectForge::forge_result
|
|
@@ -129,16 +147,28 @@ class ObjectForge::Forge
|
|
|
129
147
|
private
|
|
130
148
|
|
|
131
149
|
def determine_crucible
|
|
132
|
-
: (Hash[Symbol, untyped]) -> ObjectForge::
|
|
150
|
+
: (Hash[Symbol, untyped]) -> ObjectForge::crucible
|
|
151
|
+
|
|
152
|
+
def crucible_takes_yard_parameter?
|
|
153
|
+
: (ObjectForge::crucible) -> bool
|
|
133
154
|
|
|
134
155
|
def determine_mold
|
|
135
156
|
: (ObjectForge::forge_target, Hash[Symbol, untyped]) -> ObjectForge::mold
|
|
136
157
|
|
|
137
158
|
def determine_after_forge_hook
|
|
138
159
|
: (Hash[Symbol, untyped] options) -> ObjectForge::_Hook?
|
|
160
|
+
|
|
161
|
+
def validate_other_options
|
|
162
|
+
: (Hash[Symbol, untyped] options) -> void
|
|
163
|
+
|
|
164
|
+
def build_attribute_hash
|
|
165
|
+
: (Array[Symbol] traits, Hash[Symbol, ObjectForge::attribute] overrides) -> Hash[Symbol, ObjectForge::attribute]
|
|
139
166
|
|
|
140
167
|
def resolve_attributes
|
|
141
168
|
: (Array[Symbol] traits, Hash[Symbol, ObjectForge::attribute] overrides) -> Hash[Symbol, ObjectForge::attribute]
|
|
169
|
+
|
|
170
|
+
def apply_attribute_list
|
|
171
|
+
: (Hash[Symbol, ObjectForge::attribute] attributes) -> Hash[Symbol, ObjectForge::attribute]
|
|
142
172
|
end
|
|
143
173
|
|
|
144
174
|
class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
@@ -150,6 +180,7 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
150
180
|
@sequences: Hash[Symbol, ObjectForge::Sequence]
|
|
151
181
|
@traits: Hash[Symbol, Hash[Symbol, Proc]]
|
|
152
182
|
@options: Hash[Symbol, untyped]
|
|
183
|
+
@transient_attributes: Array[Symbol]
|
|
153
184
|
|
|
154
185
|
def initialize
|
|
155
186
|
: () { (ObjectForge::ForgeDSL) -> void } -> void
|
|
@@ -161,12 +192,15 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
161
192
|
: (Symbol name, untyped value) -> Symbol
|
|
162
193
|
|
|
163
194
|
def attribute
|
|
164
|
-
: (Symbol name) { [self: ObjectForge::Crucible] -> ObjectForge::attribute } -> Symbol
|
|
195
|
+
: (Symbol name, ?transient: bool) { [self: ObjectForge::Crucible] -> ObjectForge::attribute } -> Symbol
|
|
165
196
|
alias [] attribute
|
|
166
197
|
|
|
198
|
+
def transient
|
|
199
|
+
: (Symbol name) { [self: ObjectForge::Crucible] -> ObjectForge::attribute } -> Symbol
|
|
200
|
+
|
|
167
201
|
def sequence
|
|
168
|
-
: (Symbol name, ?(ObjectForge::sequenceable | ObjectForge::Sequence) initial) { (ObjectForge::sequenceable) [self: ObjectForge::Crucible] ->
|
|
169
|
-
| (Symbol name, ?(ObjectForge::sequenceable | ObjectForge::Sequence) initial) -> Symbol
|
|
202
|
+
: (Symbol name, ?(ObjectForge::sequenceable | ObjectForge::Sequence) initial, ?transient: bool) { (ObjectForge::sequenceable) [self: ObjectForge::Crucible] -> ObjectForge::attribute } -> Symbol
|
|
203
|
+
| (Symbol name, ?(ObjectForge::sequenceable | ObjectForge::Sequence) initial, ?transient: bool) -> Symbol
|
|
170
204
|
|
|
171
205
|
def trait
|
|
172
206
|
: (Symbol name) { (self) -> void } -> Symbol
|
|
@@ -177,31 +211,38 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
177
211
|
|
|
178
212
|
def method_missing
|
|
179
213
|
# Attribute shortcut:
|
|
180
|
-
: (Symbol name) { [self: ObjectForge::Crucible] ->
|
|
214
|
+
: (Symbol name) { [self: ObjectForge::Crucible] -> ObjectForge::attribute } -> Symbol
|
|
181
215
|
# Option shortcut:
|
|
182
216
|
| (Symbol name, untyped value) -> Symbol
|
|
183
217
|
# After freezing:
|
|
184
|
-
| (Symbol name) { -> untyped } -> void
|
|
218
|
+
| (Symbol name) ?{ -> untyped } -> void
|
|
185
219
|
|
|
186
220
|
def respond_to_missing?
|
|
187
221
|
: (Symbol name, bool include_all) -> bool
|
|
188
222
|
|
|
189
223
|
def valid_option_method?
|
|
190
224
|
: (Symbol name) -> bool
|
|
225
|
+
|
|
226
|
+
def shape_attribute_list!
|
|
227
|
+
: -> void
|
|
191
228
|
end
|
|
192
229
|
|
|
193
230
|
class ObjectForge::Crucible < ObjectForge::UnBasicObject
|
|
231
|
+
EMPTY_YARD: Hash[Symbol, untyped]
|
|
232
|
+
|
|
194
233
|
def self.call
|
|
195
|
-
: (Hash[Symbol,
|
|
234
|
+
: (Hash[Symbol, ObjectForge::attribute], ?yard: ObjectForge::_Yard?) -> Hash[Symbol, ObjectForge::attribute]
|
|
196
235
|
def self.resolve
|
|
197
|
-
: (Hash[Symbol,
|
|
236
|
+
: (Hash[Symbol, ObjectForge::attribute], ?yard: ObjectForge::_Yard?) -> Hash[Symbol, ObjectForge::attribute]
|
|
198
237
|
|
|
199
238
|
@attributes: Hash[Symbol, ObjectForge::attribute]
|
|
200
239
|
@resolved_attributes: Set[Symbol]
|
|
201
240
|
@resolving_attributes: Array[Symbol]
|
|
202
241
|
|
|
242
|
+
attr_reader yard: ObjectForge::_Yard
|
|
243
|
+
|
|
203
244
|
def initialize
|
|
204
|
-
: (Hash[Symbol, ObjectForge::attribute] attributes) -> void
|
|
245
|
+
: (Hash[Symbol, ObjectForge::attribute] attributes, ?yard: ObjectForge::_Yard?) -> void
|
|
205
246
|
|
|
206
247
|
def resolve!
|
|
207
248
|
: -> Hash[Symbol, ObjectForge::attribute]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: object_forge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Bulancov
|
|
@@ -29,7 +29,7 @@ description: |
|
|
|
29
29
|
|
|
30
30
|
It is designed for cases where factory-style object construction is useful,
|
|
31
31
|
but Rails-oriented or database-oriented tooling is a poor fit. ObjectForge
|
|
32
|
-
works well with plain Ruby objects, hashes, structs, and custom build flows.
|
|
32
|
+
works well with plain Ruby objects, hashes, arrays, structs, and custom build flows.
|
|
33
33
|
|
|
34
34
|
The library focuses on explicit configuration, independent registries and factories,
|
|
35
35
|
and replaceable components. It aims to provide a familiar workflow without
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- lib/object_forge/forge_dsl.rb
|
|
47
47
|
- lib/object_forge/forgeyard.rb
|
|
48
48
|
- lib/object_forge/molds.rb
|
|
49
|
+
- lib/object_forge/molds/array_mold.rb
|
|
49
50
|
- lib/object_forge/molds/hash_mold.rb
|
|
50
51
|
- lib/object_forge/molds/keywords_mold.rb
|
|
51
52
|
- lib/object_forge/molds/single_argument_mold.rb
|
|
@@ -54,6 +55,7 @@ files:
|
|
|
54
55
|
- lib/object_forge/sequence.rb
|
|
55
56
|
- lib/object_forge/un_basic_object.rb
|
|
56
57
|
- lib/object_forge/version.rb
|
|
58
|
+
- sig/manifest.yaml
|
|
57
59
|
- sig/object_forge.rbs
|
|
58
60
|
- sig/object_forge/molds.rbs
|
|
59
61
|
homepage: https://github.com/trinistr/object_forge
|
|
@@ -62,9 +64,9 @@ licenses:
|
|
|
62
64
|
metadata:
|
|
63
65
|
homepage_uri: https://github.com/trinistr/object_forge
|
|
64
66
|
bug_tracker_uri: https://github.com/trinistr/object_forge/issues
|
|
65
|
-
documentation_uri: https://rubydoc.info/gems/object_forge/0.
|
|
66
|
-
source_code_uri: https://github.com/trinistr/object_forge/tree/v0.
|
|
67
|
-
changelog_uri: https://github.com/trinistr/object_forge/blob/v0.
|
|
67
|
+
documentation_uri: https://rubydoc.info/gems/object_forge/0.5.0
|
|
68
|
+
source_code_uri: https://github.com/trinistr/object_forge/tree/v0.5.0
|
|
69
|
+
changelog_uri: https://github.com/trinistr/object_forge/blob/v0.5.0/CHANGELOG.md
|
|
68
70
|
rubygems_mfa_required: 'true'
|
|
69
71
|
rdoc_options:
|
|
70
72
|
- "--tag"
|
|
@@ -86,6 +88,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
86
88
|
requirements: []
|
|
87
89
|
rubygems_version: 3.6.9
|
|
88
90
|
specification_version: 4
|
|
89
|
-
summary: A small, flexible factory library for plain Ruby objects, hashes,
|
|
90
|
-
and custom build flows.
|
|
91
|
+
summary: A small, flexible factory library for plain Ruby objects, hashes, arrays,
|
|
92
|
+
structs and custom build flows.
|
|
91
93
|
test_files: []
|