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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94a44e43942c8ff51412a23c866e1f87984d9c2207b044492ea77d07150199d5
4
- data.tar.gz: 2ca03af3ee44ce7005dd8e242266f7fb8409c54ebf560028ebbf8b6727ceaf0e
3
+ metadata.gz: a0800625b2fdd91bc43b347e97fbbdbcad8f7a4953e2a1543bd29b602039ba2c
4
+ data.tar.gz: 3cfea11020226e4052b25127af3763df2fa5db8b80d5261aa0b7c8b3e2b20547
5
5
  SHA512:
6
- metadata.gz: 58221f62b2aef297f053030da5b766518dec5547285c57b8f0330e0e83c15d1a5ed6fba651790900e36d7674c4206a3e70e19d45900645ce633b545a1717509b
7
- data.tar.gz: 39109437f0647115364b504d0d0bca8af66fc76f2c573870c3939c0aca2798c709a52ce920365290a767b30adae5a193271e32a17fd99e3de9eb6edc07b3f2ab
6
+ metadata.gz: 72c01d3be5fe72716a08fa6fb826fc16b4464a9a7086168e0efe708159e867da6d162107a96f6a5f1f9f1712a409c565e5f4da576232f9a2f93e24d979211bdd
7
+ data.tar.gz: bc2e52673353fb0ffc63944abdc864b2668d671921c2184dec62d5f45a2709c0a6e62ec7ebe937b4a9a9c8fa44f9a93bce05501512006d3988344a5492e3e9c1
data/README.md CHANGED
@@ -4,15 +4,17 @@
4
4
  [![CI](https://github.com/trinistr/object_forge/actions/workflows/CI.yaml/badge.svg)](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
- - [Quick start](#quick-start)
29
- - [Basics](#basics)
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)
33
- - [Performance tips](#performance-tips)
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
- # Notice how transient attributes don't require any special syntax:
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(-radius..radius) }
187
- f.y { rand(-radius..radius) }
188
- f.radius { 0.5 }
189
- f.trait :z do f.radius { 0 } end
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 previous example
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
- > [!NOTE]
255
- > If you don't specify a mold, **ObjectForge** will infer one for core data containers, including **Hash**, **Struct**, and **Data** subclasses.
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
- > 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.
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
- If both hook and block are used, the hook runs before the block.
287
-
288
- ### Performance tips
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
- def initialize(attributes)
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)
@@ -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
- resolved_attributes = resolve_attributes(traits, overrides)
106
- instance = @mold.call(forge_target: @forge_target, attributes: resolved_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
- crucible = options[:crucible] || Crucible
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
- @crucible.call(attributes)
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
- # especially in attribute definitions.
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, **nil, &)
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) { instance_exec(seq.next, &) } # steep:ignore BlockTypeMismatch
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}/) && name != :rand
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
@@ -6,7 +6,6 @@ module ObjectForge
6
6
  #
7
7
  # @thread_safety Thread-safe on its own,
8
8
  # but using unshareable default value or block is not thread-safe.
9
- #
10
9
  # @since 0.2.0
11
10
  class HashMold
12
11
  # Default value to be assigned to each produced hash.
@@ -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
- if ::Class === forge_target
135
+ return SingleArgumentMold.new unless ::Class === forge_target
136
+
137
+ mold_class =
135
138
  if forge_target < ::Struct
136
- StructMold.new
139
+ StructMold
137
140
  elsif defined?(::Data) && forge_target < ::Data
138
- KeywordsMold.new
141
+ KeywordsMold
139
142
  elsif forge_target <= ::Hash
140
- HashMold.new
143
+ HashMold
144
+ elsif forge_target <= ::Array
145
+ ArrayMold
141
146
  else
142
- SingleArgumentMold.new
147
+ SingleArgumentMold
143
148
  end
144
- else
145
- SingleArgumentMold.new
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 mold.nil? || mold.respond_to?(:call)
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)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ObjectForge
4
4
  # Current version
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
6
6
  end
data/sig/manifest.yaml ADDED
@@ -0,0 +1,2 @@
1
+ dependencies:
2
+ - name: pp
@@ -28,8 +28,8 @@ module ObjectForge
28
28
  class StructMold
29
29
  interface _StructSubclass[T]
30
30
  def new
31
- : (*untyped) -> T
32
- | (**untyped) -> T
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
- : [T < Hash] (?ObjectForge::attribute default_value) ?{ (T hash, untyped key) -> ObjectForge::attribute} -> void
63
+ : (?ObjectForge::attribute? default_value) ?{ (Hash[untyped, untyped] hash, untyped key) -> ObjectForge::attribute} -> void
64
64
 
65
65
  def call
66
- : [T < Hash] (forge_target: _HashSubclass[T], attributes: Hash[Symbol, ObjectForge::attribute], **untyped) -> T
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 = ::Object & _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::_Crucible
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::_Crucible
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] -> untyped } -> Symbol
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] -> untyped } -> Symbol
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, untyped]) -> Hash[Symbol, untyped]
234
+ : (Hash[Symbol, ObjectForge::attribute], ?yard: ObjectForge::_Yard?) -> Hash[Symbol, ObjectForge::attribute]
196
235
  def self.resolve
197
- : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
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.1
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.4.1
66
- source_code_uri: https://github.com/trinistr/object_forge/tree/v0.4.1
67
- changelog_uri: https://github.com/trinistr/object_forge/blob/v0.4.1/CHANGELOG.md
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, structs
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: []