object_forge 0.1.1 → 0.3.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 +293 -0
- data/lib/object_forge/crucible.rb +1 -2
- data/lib/object_forge/forge.rb +40 -12
- data/lib/object_forge/forge_dsl.rb +55 -12
- data/lib/object_forge/forgeyard.rb +16 -4
- data/lib/object_forge/molds/hash_mold.rb +54 -0
- data/lib/object_forge/molds/keywords_mold.rb +22 -17
- data/lib/object_forge/molds/single_argument_mold.rb +20 -0
- data/lib/object_forge/molds/struct_mold.rb +71 -0
- data/lib/object_forge/molds/wrapped_mold.rb +32 -0
- data/lib/object_forge/molds.rb +119 -0
- data/lib/object_forge/un_basic_object.rb +5 -5
- data/lib/object_forge/version.rb +1 -1
- data/lib/object_forge.rb +10 -6
- data/sig/object_forge/molds.rbs +67 -0
- data/sig/object_forge.rbs +32 -9
- metadata +17 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd0b538c878ddc72f9041187f1612a1d779b55d7d7a4a72bb8be9c2c22d36073
|
|
4
|
+
data.tar.gz: 0cd2ec6ea0594f13f1780eb503220d5313489657d6aa31e771d075d1ceef4c9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63431e04ef6c32990268ba7545564c964a72b72b23b9469315479d696c42a4106b24dd490a80481182f04b41e91cc78614f1de177f3516bb66d965c0f71b7df9
|
|
7
|
+
data.tar.gz: 1ed2a7e317671bb416bad5960d2bb227332ccf911d936835802470f785d98975223f3bf2070ebd7ca212b84932e773d1844bf64a718d857f8fc514c3cf2516e5
|
data/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# ObjectForge
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/object_forge)
|
|
4
|
+
[](https://github.com/trinistr/object_forge/actions/workflows/CI.yaml)
|
|
5
|
+
|
|
6
|
+
> [!TIP]
|
|
7
|
+
> 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
|
+
***
|
|
10
|
+
|
|
11
|
+
**ObjectForge** provides a familiar way to build objects in any context with minimal assumptions about usage environment.
|
|
12
|
+
- It is not connected to any framework and, indeed, has nothing to do with a database.
|
|
13
|
+
- To use, just define some factories and call them wherever you need, be it in tests, console, or application code.
|
|
14
|
+
- If you need, almost any part of the process can be easily replaced with a custom solution.
|
|
15
|
+
|
|
16
|
+
## Table of contents
|
|
17
|
+
|
|
18
|
+
- [Motivation (why *another* another factory gem?)](#motivation-why-another-another-factory-gem)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Usage](#usage)
|
|
21
|
+
- [Basics](#basics)
|
|
22
|
+
- [Separate forgeyards and forges](#separate-forgeyards-and-forges)
|
|
23
|
+
- [Molds: customized forging](#molds-customized-forging)
|
|
24
|
+
- [Performance tips](#performance-tips)
|
|
25
|
+
- [Differences and limitations (compared to FactoryBot)](#differences-and-limitations-compared-to-factorybot)
|
|
26
|
+
- [Current and planned features (roadmap)](#current-and-planned-features-roadmap)
|
|
27
|
+
- [Development](#development)
|
|
28
|
+
- [Contributing](#contributing)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
|
|
31
|
+
## Motivation (why *another* another factory gem?)
|
|
32
|
+
|
|
33
|
+
There are a bunch of gems that provide object generation functionality, chief among them [FactoryBot](https://github.com/thoughtbot/factory_bot) and [Fabrication](https://fabricationgem.org/).
|
|
34
|
+
However, such gems make a lot of assumptions about why, how and what for they will be used, making them complicated and, at the same time, severely limited. Such assumptions commonly are:
|
|
35
|
+
- assuming that every Ruby project is a Rails project;
|
|
36
|
+
- assuming that "generating objects" equates "saving records to database";
|
|
37
|
+
- assuming that objects are mutable and provide attribute writers;
|
|
38
|
+
- assuming that streamlined object generation is only useful for testing;
|
|
39
|
+
- (related to the previous point) assuming that there will never be a need to
|
|
40
|
+
have more than one configuration of a library in the same project;
|
|
41
|
+
- assuming that adding global methods or objects is a good idea.
|
|
42
|
+
|
|
43
|
+
I notice that there is also a problem of thinking that Rails's "convention-over-configuration" approach is always appropriate, but then making configuration convoluted, instead of making it easy for the user to do the things they want in the way they want in the first place.
|
|
44
|
+
|
|
45
|
+
There are some projects that tried to address these issues, like [Progenitor](https://github.com/pavlos/progenitor) (the closest to **ObjectForge**) and [Workbench](https://github.com/leadtune/workbench), but they still didn't manage to go around the pitfalls.
|
|
46
|
+
Most factory projects are also quite dead, having not been updated in *many* years.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
Install with `gem`:
|
|
51
|
+
```sh
|
|
52
|
+
gem install object_forge
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or, if using Bundler, add to your Gemfile:
|
|
56
|
+
```ruby
|
|
57
|
+
gem "object_forge"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
> [!Note]
|
|
63
|
+
> - Latest documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/object_forge).
|
|
64
|
+
> - Documentation for published versions is available on [RubyDoc](https://rubydoc.info/gems/object_forge).
|
|
65
|
+
|
|
66
|
+
### Basics
|
|
67
|
+
|
|
68
|
+
In the simplest cases, **ObjectForge** can be used much like other factory libraries, with definitions living in a global object (`ObjectForge::DEFAULT_YARD`).
|
|
69
|
+
|
|
70
|
+
Forges are defined using a DSL:
|
|
71
|
+
```ruby
|
|
72
|
+
# Example class:
|
|
73
|
+
Point = Struct.new(:id, :x, :y)
|
|
74
|
+
|
|
75
|
+
ObjectForge.define(:point, Point) do |f|
|
|
76
|
+
# Attributes can be defined using `#attribute` method:
|
|
77
|
+
f.attribute(:x) do
|
|
78
|
+
# Inside attribute definitions, other attributes can be referenced by name, in any order!
|
|
79
|
+
rand(-delta..delta)
|
|
80
|
+
end
|
|
81
|
+
# `#[]` is an alias of `#attribute`:
|
|
82
|
+
f[:y] { rand(-delta..delta) }
|
|
83
|
+
# There is also the familiar shortcut using `method_missing`:
|
|
84
|
+
f.delta { 0.5 * amplitude }
|
|
85
|
+
# Notice how transient attributes don't require any special syntax:
|
|
86
|
+
f.amplitude { 1 }
|
|
87
|
+
# `#sequence` defines a sequenced attribute (starting with 1 by default):
|
|
88
|
+
f.sequence(:id, "a")
|
|
89
|
+
# Traits allow to group and reuse related values:
|
|
90
|
+
f.trait :z do
|
|
91
|
+
f.amplitude { 0 }
|
|
92
|
+
# Sequence values are forge-global, but traits can redefine blocks:
|
|
93
|
+
f.sequence(:id) { |id| "Z_#{id}" }
|
|
94
|
+
end
|
|
95
|
+
# Trait's block can receive DSL object as a parameter:
|
|
96
|
+
f.trait :invalid do |tf|
|
|
97
|
+
tf.y { Float::NAN }
|
|
98
|
+
# `#[]` method inside attribute definition can be used to reference attributes:
|
|
99
|
+
tf.id { self[:x] }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A forge builds objects, using attributes hash:
|
|
105
|
+
```ruby
|
|
106
|
+
ObjectForge.call(:point)
|
|
107
|
+
# => #<Point:0x00007f6109dcad40 @id="a", @x=0.17176955469852973, @y=0.3423901951181103>
|
|
108
|
+
# Positional arguments define used traits:
|
|
109
|
+
ObjectForge.build(:point, :z)
|
|
110
|
+
# => #<Point:0x00007f61099e7980 @id="Z_b", @x=0.0, @y=0.0>
|
|
111
|
+
# Attributes can be overridden with keyword arguments:
|
|
112
|
+
ObjectForge.forge(:point, x: 10)
|
|
113
|
+
# => #<Point:0x00007f6109aabf88 @id="c", @x=10, @y=-0.3458802496120402>
|
|
114
|
+
# Traits and overrides are combined in the given order:
|
|
115
|
+
ObjectForge.call(:point, :z, :invalid, id: "NaN")
|
|
116
|
+
# => #<Point:0x00007f6109b82e48 @id="NaN", @x=0.0, @y=NaN>
|
|
117
|
+
# A Proc override behaves the same as an attribute definition:
|
|
118
|
+
ObjectForge.call(:point, :z, x: -> { rand(100..200) + delta })
|
|
119
|
+
# => #<Point:0x00007f6109932418 @id="Z_d", @x=135.0, @y=0.0>
|
|
120
|
+
# A block can be passed to do something with the created object:
|
|
121
|
+
ObjectForge.call(:point, :z) { puts "#{_1.id}: #{_1.x},#{_1.y}" }
|
|
122
|
+
# outputs "Z_e: 0.0,0.0"
|
|
123
|
+
```
|
|
124
|
+
> [!TIP]
|
|
125
|
+
> Forging can be done through any of `#call`, `#forge`, or `#build` methods, they are aliases.
|
|
126
|
+
|
|
127
|
+
### Separate forgeyards and forges
|
|
128
|
+
|
|
129
|
+
It is possible and *encouraged* to create multiple forgeyards, each with its own set of forges:
|
|
130
|
+
```ruby
|
|
131
|
+
forgeyard = ObjectForge::Forgeyard.new
|
|
132
|
+
forgeyard.define(:point, Point) do |f|
|
|
133
|
+
f.sequence(:id, "a")
|
|
134
|
+
f.x { rand(-radius..radius) }
|
|
135
|
+
f.y { rand(-radius..radius) }
|
|
136
|
+
f.radius { 0.5 }
|
|
137
|
+
f.trait :z do f.radius { 0 } end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Now, this forgeyard can be used just like the default one:
|
|
142
|
+
```ruby
|
|
143
|
+
forgeyard.forge(:point, :z, id: "0")
|
|
144
|
+
# => #<Point:0x00007f6109b719e0 @id="0", @x=0, @y=0>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Note how the forge isn't registered in the default forgeyard:
|
|
148
|
+
```ruby
|
|
149
|
+
ObjectForge.forge(:point)
|
|
150
|
+
# ArgumentError: unknown forge: point
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
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:
|
|
154
|
+
```ruby
|
|
155
|
+
forge = ObjectForge::Forge.define(Point) do |f|
|
|
156
|
+
f.sequence(:id, "a")
|
|
157
|
+
f.x { rand(-radius..radius) }
|
|
158
|
+
f.y { rand(-radius..radius) }
|
|
159
|
+
f.radius { 0.5 }
|
|
160
|
+
f.trait :z do f.radius { 0 } end
|
|
161
|
+
end
|
|
162
|
+
forge.(:z, id: "0")
|
|
163
|
+
# => #<Point:0x00007f6109b719e0 @id="0", @x=0, @y=0>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Forge** has the same building interface as a **Forgeyard**, but it doesn't have the name argument:
|
|
167
|
+
```ruby
|
|
168
|
+
forge.build
|
|
169
|
+
# => #<Point:0x00007f610deae578 @id="a", @x=0.3317733939650964, @y=-0.1363936629550252>
|
|
170
|
+
forge.forge(:z)
|
|
171
|
+
# => #<Point:0x00007f61099f6520 @id="b", @x=0, @y=0>
|
|
172
|
+
forge.(radius: 500)
|
|
173
|
+
# => #<Point:0x00007f6109960048 @id="c", @x=-141, @y=109>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Molds: customized forging
|
|
177
|
+
|
|
178
|
+
If you use core Ruby data containers, such as `Struct`, `Data` or even `Hash`, they will "just work". However, if a custom class is used, forging will probably fail, unless your class happens to take a hash of attributes in `#initialize`. It would be *really* bad if **ObjectForge** placed requirements on your classes, and indeed there is a solution.
|
|
179
|
+
|
|
180
|
+
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 specified in forge definition:
|
|
181
|
+
```ruby
|
|
182
|
+
forge = ObjectForge::Forge.define(Point) do |f|
|
|
183
|
+
f.mold = ->(forged:, attributes:, **) do
|
|
184
|
+
puts "Pointing at #{attributes[:x]},#{attributes[:y]}"
|
|
185
|
+
forged.new(attributes[:id], attributes[:x], attributes[:y])
|
|
186
|
+
end
|
|
187
|
+
#... rest of the definition
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Now the specified **mold** will be called to build your objects:
|
|
192
|
+
```ruby
|
|
193
|
+
forge.forge
|
|
194
|
+
# Pointing at 0.3317733939650964,-0.1363936629550252
|
|
195
|
+
# => #<Point:0x00007f610deae578 @id="a", @x=0.3317733939650964, @y=-0.1363936629550252>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Of course, you can abuse this to your heart's content. Look at the documentation for `ObjectForge::Molds` for inspiration.
|
|
199
|
+
|
|
200
|
+
**ObjectForge** comes pre-equipped with a selection of molds for common cases:
|
|
201
|
+
- `ObjectForge::Molds::SingleArgumentMold` (*the default*) — calls `new(attributes)`, suitable for **Dry::Struct**, for example;
|
|
202
|
+
- `ObjectForge::Molds::KeywordsMold` — calls `new(**attributes)`, suitable for **Data** and similar classes;
|
|
203
|
+
- `ObjectForge::Molds::HashMold` allows building **Hash** (including subclasses), providing a way to easily use hashes to carry data;
|
|
204
|
+
- `ObjectForge::Molds::StructMold` handles all possible cases of `keyword_init` for **Struct** subclasses.
|
|
205
|
+
|
|
206
|
+
> [!TIP]
|
|
207
|
+
> **HashMold** and **StructMold** will be used automatically, based on the forged class, if you don't specify any mold.
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
I strongly recommend directly using mold instances and not classes. Doing this prevents memory churn which causes performance issues. Not only that, but having a stateful mold is a code smell and probably represents a significant design issue.
|
|
211
|
+
|
|
212
|
+
### Performance tips
|
|
213
|
+
|
|
214
|
+
**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.
|
|
215
|
+
- 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.
|
|
216
|
+
- 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!).
|
|
217
|
+
- 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.
|
|
218
|
+
|
|
219
|
+
## Differences and limitations (compared to FactoryBot)
|
|
220
|
+
|
|
221
|
+
If you are used to FactoryBot, be aware that there are quite a few differences in specifics.
|
|
222
|
+
|
|
223
|
+
General:
|
|
224
|
+
- The user (you) is responsible for loading forge definitions, there are no search paths. If **ObjectForge** is used in tests, it should be enough to add something like `Dir["spec/forges/**/*.rb].each { require _1 }` to your `spec_helper.rb` (or `rails_helper.rb`).
|
|
225
|
+
- `Forgeyard.define` *is* the forge definition block, you don't need to nest it inside another `factory` block.
|
|
226
|
+
- There is no forge inheritance or nesting, though it may be added in the future.
|
|
227
|
+
|
|
228
|
+
Forge definition:
|
|
229
|
+
- Class specification for a forge is non-optional, there is no assumption about the class name.
|
|
230
|
+
- If the DSL block declares a block argument, `self` context is not changed, and DSL methods can't be called with an implicit receiver.
|
|
231
|
+
|
|
232
|
+
Attributes:
|
|
233
|
+
- For now, transient attributes have no difference to regular ones, they just aren't set in the final object.
|
|
234
|
+
- *There are no associations*. If nested objects are required, they should be created and set in the block for the attribute.
|
|
235
|
+
|
|
236
|
+
Traits:
|
|
237
|
+
- Traits can't be defined inside of other traits. (I feel that nesting is needlessly confusing.)
|
|
238
|
+
- Traits can't be called from other traits. This may change in the future.
|
|
239
|
+
- There are no default traits.
|
|
240
|
+
|
|
241
|
+
Sequences:
|
|
242
|
+
- There is no explicit way to define shared sequences, unless you pass the same object yourself to multiple `sequence` calls.
|
|
243
|
+
- Sequences work with values implementing `#succ`, not `#next`, expressly prohibiting `Enumerator`. This may be relaxed in the future.
|
|
244
|
+
|
|
245
|
+
## Current and planned features (roadmap)
|
|
246
|
+
|
|
247
|
+
```mermaid
|
|
248
|
+
kanban
|
|
249
|
+
[✅ Done]
|
|
250
|
+
[FactoryBot-like DSL: attributes, traits, sequences]
|
|
251
|
+
[Independent forges]
|
|
252
|
+
[Independent forgeyards]
|
|
253
|
+
[Default global forgeyard]
|
|
254
|
+
[Thread-safe behavior]
|
|
255
|
+
[Tapping into built objects for post-processing]
|
|
256
|
+
[Custom builders / molds]
|
|
257
|
+
[Built-in Hash, Struct, Data builders / molds]
|
|
258
|
+
[⚗️ To do]
|
|
259
|
+
[Ability to replace resolver]
|
|
260
|
+
[After-build hook]
|
|
261
|
+
[❔Under consideration]
|
|
262
|
+
[Calling traits from traits]
|
|
263
|
+
[Default traits]
|
|
264
|
+
[Forge inheritance]
|
|
265
|
+
[Premade performance forge: static DSL, epsilon resolver]
|
|
266
|
+
[Enumerator compatibility in sequences]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests, `rake rubocop` to lint code and check style compliance, `rake rbs` to validate signatures or just `rake` to do everything above. There is also `rake steep` to check typing, and `rake docs` to generate YARD documentation.
|
|
272
|
+
|
|
273
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment, or `bin/benchmark` to run a benchmark script and generate a StackProf flamegraph.
|
|
274
|
+
|
|
275
|
+
To install this gem onto your local machine, run `rake install`. To release a new version, run `rake version:{major|minor|patch}`, and then run `rake release`, which will push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
276
|
+
|
|
277
|
+
## Contributing
|
|
278
|
+
|
|
279
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/trinistr/object_forge.
|
|
280
|
+
|
|
281
|
+
**Checklist for a new or updated feature**
|
|
282
|
+
|
|
283
|
+
- Running `rake spec` reports 100% coverage (unless it's impossible to achieve in one run).
|
|
284
|
+
- Running `rake rubocop` reports no offenses.
|
|
285
|
+
- Running `rake steep` reports no new warnings or errors.
|
|
286
|
+
- Tests cover the behavior and its interactions. 100% coverage *is not enough*, as it does not guarantee that all code paths are tested.
|
|
287
|
+
- Documentation is up-to-date: generate it with `rake docs` and read it.
|
|
288
|
+
- "*CHANGELOG.md*" lists the change if it has impact on users.
|
|
289
|
+
- "*README.md*" is updated if the feature should be visible there, including the Kanban board.
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
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).
|
|
@@ -12,10 +12,9 @@ module ObjectForge
|
|
|
12
12
|
#
|
|
13
13
|
# @thread_safety Attribute resolution is idempotent,
|
|
14
14
|
# but modifies instance variables, making it unsafe to share the Crucible
|
|
15
|
-
#
|
|
16
15
|
# @since 0.1.0
|
|
17
16
|
class Crucible < UnBasicObject
|
|
18
|
-
%i[rand].each { |m| private define_method(m, ::
|
|
17
|
+
%i[rand].each { |m| private define_method(m, ::Kernel.instance_method(m)) }
|
|
19
18
|
|
|
20
19
|
# @param attributes [Hash{Symbol => Proc, Any}] initial attributes
|
|
21
20
|
def initialize(attributes)
|
data/lib/object_forge/forge.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "crucible"
|
|
4
4
|
require_relative "forge_dsl"
|
|
5
|
+
require_relative "molds"
|
|
5
6
|
|
|
6
7
|
module ObjectForge
|
|
7
8
|
# Object instantitation forge.
|
|
@@ -19,14 +20,21 @@ module ObjectForge
|
|
|
19
20
|
# @!attribute [r] traits
|
|
20
21
|
# Attributes belonging to traits.
|
|
21
22
|
# @return [Hash{Symbol => Hash{Symbol => Any}}]
|
|
22
|
-
|
|
23
|
+
#
|
|
24
|
+
# @!attribute [r] settings
|
|
25
|
+
# A forge's settings.
|
|
26
|
+
# Must include a +:mold+ key, containing an object that knows how to build the instance
|
|
27
|
+
# with a +call+ method that takes a class and a hash of attributes.
|
|
28
|
+
# @since 0.3.0
|
|
29
|
+
# @return [Hash{Symbol => Any}]
|
|
30
|
+
Parameters = Struct.new(:attributes, :traits, :settings, keyword_init: true)
|
|
23
31
|
|
|
24
32
|
# Define (and create) a forge using DSL.
|
|
25
33
|
#
|
|
26
34
|
# @see ForgeDSL
|
|
27
35
|
# @thread_safety Thread-safe if DSL definition is thread-safe.
|
|
28
36
|
#
|
|
29
|
-
# @param forged [Class] class to forge
|
|
37
|
+
# @param forged [Class, Any] class or object to forge
|
|
30
38
|
# @param name [Symbol, nil] forge name
|
|
31
39
|
# @yieldparam f [ForgeDSL]
|
|
32
40
|
# @yieldreturn [void]
|
|
@@ -38,19 +46,21 @@ module ObjectForge
|
|
|
38
46
|
# @return [Symbol, nil] forge name
|
|
39
47
|
attr_reader :name
|
|
40
48
|
|
|
41
|
-
# @return [Class] class to forge
|
|
49
|
+
# @return [Class, Any] class or object to forge
|
|
42
50
|
attr_reader :forged
|
|
43
51
|
|
|
44
52
|
# @return [Parameters, ForgeDSL] forge parameters
|
|
45
53
|
attr_reader :parameters
|
|
46
54
|
|
|
47
|
-
# @param forged [Class] class to forge
|
|
55
|
+
# @param forged [Class, Any] class or object to forge
|
|
48
56
|
# @param parameters [Parameters, ForgeDSL] forge parameters
|
|
49
|
-
# @param name [Symbol, nil] forge name
|
|
57
|
+
# @param name [Symbol, nil] forge name;
|
|
58
|
+
# only used for identification purposes
|
|
50
59
|
def initialize(forged, parameters, name: nil)
|
|
51
60
|
@name = name
|
|
52
61
|
@forged = forged
|
|
53
62
|
@parameters = parameters
|
|
63
|
+
@mold = determine_mold(forged, parameters.settings[:mold])
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
# Forge a new instance.
|
|
@@ -67,7 +77,7 @@ module ObjectForge
|
|
|
67
77
|
# If a block is given, forged instance is yielded to it after being built.
|
|
68
78
|
#
|
|
69
79
|
# @thread_safety Forging is thread-safe if {#parameters},
|
|
70
|
-
#
|
|
80
|
+
# +traits+ and +overrides+ are thread-safe.
|
|
71
81
|
#
|
|
72
82
|
# @param traits [Array<Symbol>] traits to apply
|
|
73
83
|
# @param overrides [Hash{Symbol => Any}] attribute overrides
|
|
@@ -76,23 +86,41 @@ module ObjectForge
|
|
|
76
86
|
# @return [Any] built instance
|
|
77
87
|
def forge(*traits, **overrides)
|
|
78
88
|
resolved_attributes = resolve_attributes(traits, overrides)
|
|
79
|
-
instance =
|
|
89
|
+
instance = @mold.call(forged: @forged, attributes: resolved_attributes)
|
|
80
90
|
yield instance if block_given?
|
|
81
91
|
instance
|
|
82
92
|
end
|
|
83
93
|
|
|
84
94
|
alias build forge
|
|
85
|
-
alias
|
|
95
|
+
alias call forge
|
|
86
96
|
|
|
87
97
|
private
|
|
88
98
|
|
|
99
|
+
# Get appropriate mold based on parameters.
|
|
100
|
+
#
|
|
101
|
+
# If +mold+ is already set, it will be used directly, or,
|
|
102
|
+
# if it is Class, it will be wrapped in {Molds::WrappedMold} if posssible.
|
|
103
|
+
# If +nil+, a mold will be selected based on +forged+ class.
|
|
104
|
+
#
|
|
105
|
+
# @param forged [Class, Any]
|
|
106
|
+
# @param mold [#call, Class, nil]
|
|
107
|
+
# @return [#call]
|
|
108
|
+
#
|
|
109
|
+
# @raise [MoldError]
|
|
110
|
+
#
|
|
111
|
+
# @since 0.3.0
|
|
112
|
+
def determine_mold(forged, mold)
|
|
113
|
+
Molds.wrap_mold(mold) || Molds.mold_for(forged)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Resolve attributes using default attributes, specified traits and overrides.
|
|
117
|
+
#
|
|
118
|
+
# @param traits [Array<Symbol>]
|
|
119
|
+
# @param overrides [Hash{Symbol => Any}]
|
|
120
|
+
# @return [Hash{Symbol => Any}]
|
|
89
121
|
def resolve_attributes(traits, overrides)
|
|
90
122
|
attributes = @parameters.attributes.merge(*@parameters.traits.values_at(*traits), overrides)
|
|
91
123
|
Crucible.new(attributes).resolve!
|
|
92
124
|
end
|
|
93
|
-
|
|
94
|
-
def build_instance(attributes)
|
|
95
|
-
forged.new(attributes)
|
|
96
|
-
end
|
|
97
125
|
end
|
|
98
126
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "sequence"
|
|
4
4
|
require_relative "un_basic_object"
|
|
5
5
|
|
|
6
|
+
require_relative "molds/wrapped_mold"
|
|
7
|
+
|
|
6
8
|
module ObjectForge
|
|
7
9
|
# DSL for defining a forge.
|
|
8
10
|
#
|
|
@@ -14,7 +16,6 @@ module ObjectForge
|
|
|
14
16
|
# especially in attribute definitions.
|
|
15
17
|
# The instance itself is frozen after initialization,
|
|
16
18
|
# so it should be safe to share.
|
|
17
|
-
#
|
|
18
19
|
# @since 0.1.0
|
|
19
20
|
class ForgeDSL < UnBasicObject
|
|
20
21
|
# @return [Hash{Symbol => Proc}] attribute definitions
|
|
@@ -26,6 +27,9 @@ module ObjectForge
|
|
|
26
27
|
# @return [Hash{Symbol => Hash{Symbol => Proc}}] trait definitions
|
|
27
28
|
attr_reader :traits
|
|
28
29
|
|
|
30
|
+
# @return [Hash{Symbol => Any}] settings for forge, such as mold
|
|
31
|
+
attr_reader :settings
|
|
32
|
+
|
|
29
33
|
# Define forge's parameters through DSL.
|
|
30
34
|
#
|
|
31
35
|
# If the block has a parameter, an object will be yielded,
|
|
@@ -35,6 +39,7 @@ module ObjectForge
|
|
|
35
39
|
#
|
|
36
40
|
# @example with block parameter
|
|
37
41
|
# ForgeDSL.new do |f|
|
|
42
|
+
# f.mold = ObjectForge::Molds::KeywordsMolds.new
|
|
38
43
|
# f.attribute(:name) { "Name" }
|
|
39
44
|
# f[:description] { name.upcase }
|
|
40
45
|
# f.duration { rand(1000) }
|
|
@@ -42,6 +47,7 @@ module ObjectForge
|
|
|
42
47
|
#
|
|
43
48
|
# @example without block parameter
|
|
44
49
|
# ForgeDSL.new do
|
|
50
|
+
# self.mold = ::ObjectForge::Molds::KeywordsMolds.new
|
|
45
51
|
# attribute(:name) { "Name" }
|
|
46
52
|
# self[:description] { name.upcase }
|
|
47
53
|
# duration { rand(1000) }
|
|
@@ -54,26 +60,55 @@ module ObjectForge
|
|
|
54
60
|
@attributes = {}
|
|
55
61
|
@sequences = {}
|
|
56
62
|
@traits = {}
|
|
63
|
+
@settings = {}
|
|
57
64
|
|
|
58
65
|
dsl.arity.zero? ? instance_exec(&dsl) : yield(self)
|
|
59
66
|
|
|
60
67
|
freeze
|
|
61
68
|
end
|
|
62
69
|
|
|
63
|
-
# Freezes the instance, including +attributes+, +sequences+ and +traits+.
|
|
70
|
+
# Freezes the instance, including +settings+, +attributes+, +sequences+ and +traits+.
|
|
64
71
|
# Prevents further responses through +#method_missing+.
|
|
65
72
|
#
|
|
66
73
|
# @note Called automatically in {#initialize}.
|
|
67
74
|
#
|
|
68
75
|
# @return [self]
|
|
69
76
|
def freeze
|
|
70
|
-
::
|
|
77
|
+
::Kernel.instance_method(:freeze).bind_call(self)
|
|
71
78
|
@attributes.freeze
|
|
72
79
|
@sequences.freeze
|
|
73
80
|
@traits.freeze
|
|
81
|
+
@settings.freeze
|
|
74
82
|
self
|
|
75
83
|
end
|
|
76
84
|
|
|
85
|
+
# Set a value for a forge's setting.
|
|
86
|
+
#
|
|
87
|
+
# Possible settings depend on used forge, but for default {Forge} a +mold+ is expected.
|
|
88
|
+
#
|
|
89
|
+
# It is also possible to set settings through +method_missing+, using name with a +=+ suffix.
|
|
90
|
+
#
|
|
91
|
+
# @see Molds
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# f.setting(:mold, ->(forged:, attributes:, **) { forge.new(**attributes) })
|
|
95
|
+
# f.mold = ObjectForge::Molds::SingleArgumentMold.new
|
|
96
|
+
#
|
|
97
|
+
# @param name [Sumbol] setting name
|
|
98
|
+
# @param value [Any] value for the setting
|
|
99
|
+
# @return [Symbol] setting name
|
|
100
|
+
#
|
|
101
|
+
# @raise [ArgumentError] if +name+ is not a Symbol
|
|
102
|
+
def setting(name, value)
|
|
103
|
+
unless ::Symbol === name
|
|
104
|
+
raise ::ArgumentError, "setting name must be a Symbol, #{name.class} given"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
@settings[name] = value
|
|
108
|
+
|
|
109
|
+
name
|
|
110
|
+
end
|
|
111
|
+
|
|
77
112
|
# Define an attribute, possibly transient.
|
|
78
113
|
#
|
|
79
114
|
# DSL does not know or care what attributes the forged class has,
|
|
@@ -226,26 +261,30 @@ module ObjectForge
|
|
|
226
261
|
|
|
227
262
|
private
|
|
228
263
|
|
|
229
|
-
# Define an attribute using a shorthand.
|
|
264
|
+
# Define an attribute (like +name+) or set a setting (like +name=+) using a shorthand.
|
|
230
265
|
#
|
|
231
|
-
# Can not be used
|
|
266
|
+
# Can not be used with reserved names.
|
|
232
267
|
# Trying to use a conflicting name will lead to usual issues
|
|
233
268
|
# with calling random methods.
|
|
234
|
-
# When in doubt, use {#attribute} or {#
|
|
269
|
+
# When in doubt, use {#attribute} or {#setting} instead.
|
|
235
270
|
#
|
|
236
271
|
# Reserved names are:
|
|
237
|
-
# - all names ending in +?+, +!+
|
|
272
|
+
# - all names ending in +?+, +!+
|
|
238
273
|
# - all names starting with a non-word ASCII character
|
|
239
274
|
# (operators, +`+, +[]+, +[]=+)
|
|
240
275
|
# - +rand+
|
|
241
276
|
#
|
|
242
|
-
# @param name [Symbol] attribute name
|
|
277
|
+
# @param name [Symbol] attribute or setting name
|
|
278
|
+
# @param value [Any] value for setting
|
|
243
279
|
# @yieldreturn [Any] attribute value
|
|
244
|
-
# @return [Symbol] attribute name
|
|
280
|
+
# @return [Symbol] attribute or setting name
|
|
245
281
|
#
|
|
246
282
|
# @raise [DSLError] if a reserved +name+ is used
|
|
247
|
-
def method_missing(name, **nil, &)
|
|
248
|
-
return super if frozen?
|
|
283
|
+
def method_missing(name, value = nil, **nil, &)
|
|
284
|
+
return super(name) if frozen?
|
|
285
|
+
if valid_setting_method?(name)
|
|
286
|
+
return setting(name[...-1].to_sym, value) # steep:ignore NoMethod
|
|
287
|
+
end
|
|
249
288
|
return attribute(name, &) if respond_to_missing?(name, false)
|
|
250
289
|
|
|
251
290
|
raise DSLError, "#{name.inspect} is a reserved name (in #{name.inspect})"
|
|
@@ -254,7 +293,11 @@ module ObjectForge
|
|
|
254
293
|
def respond_to_missing?(name, _include_all)
|
|
255
294
|
return false if frozen?
|
|
256
295
|
|
|
257
|
-
!name.end_with?("?", "!"
|
|
296
|
+
!name.end_with?("?", "!") && !name.match?(/\A(?=\p{ASCII})\P{Word}/) && name != :rand
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def valid_setting_method?(name)
|
|
300
|
+
name.match?(/\A\p{Word}.*=\z/)
|
|
258
301
|
end
|
|
259
302
|
end
|
|
260
303
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "concurrent/map"
|
|
4
4
|
|
|
5
|
+
require_relative "forge"
|
|
6
|
+
|
|
5
7
|
module ObjectForge
|
|
6
8
|
# A registry for forges, making them accessible by name.
|
|
7
9
|
#
|
|
@@ -20,7 +22,7 @@ module ObjectForge
|
|
|
20
22
|
# @see Forge.define
|
|
21
23
|
#
|
|
22
24
|
# @param name [Symbol] name to register forge under
|
|
23
|
-
# @param forged [Class] class to forge
|
|
25
|
+
# @param forged [Class, Any] class or object to forge
|
|
24
26
|
# @yieldparam f [ForgeDSL]
|
|
25
27
|
# @yieldreturn [void]
|
|
26
28
|
# @return [Forge] forge
|
|
@@ -43,6 +45,16 @@ module ObjectForge
|
|
|
43
45
|
@forges.put_if_absent(name, forge) || forge
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
# Retrieve a forge by name.
|
|
49
|
+
#
|
|
50
|
+
# @param name [Symbol] name of the forge
|
|
51
|
+
# @return [Forge] registered forge
|
|
52
|
+
#
|
|
53
|
+
# @raise [KeyError] if forge with the specified name is not registered
|
|
54
|
+
def [](name)
|
|
55
|
+
@forges.fetch(name)
|
|
56
|
+
end
|
|
57
|
+
|
|
46
58
|
# Build an instance using a forge.
|
|
47
59
|
#
|
|
48
60
|
# @see Forge#forge
|
|
@@ -55,11 +67,11 @@ module ObjectForge
|
|
|
55
67
|
# @return [Any] built instance
|
|
56
68
|
#
|
|
57
69
|
# @raise [KeyError] if forge with the specified name is not registered
|
|
58
|
-
def forge(name,
|
|
59
|
-
@forges.fetch(name)
|
|
70
|
+
def forge(name, ...)
|
|
71
|
+
@forges.fetch(name).call(...)
|
|
60
72
|
end
|
|
61
73
|
|
|
62
74
|
alias build forge
|
|
63
|
-
alias
|
|
75
|
+
alias call forge
|
|
64
76
|
end
|
|
65
77
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ObjectForge
|
|
4
|
+
module Molds
|
|
5
|
+
# Mold for constructing Hashes.
|
|
6
|
+
#
|
|
7
|
+
# @thread_safety Thread-safe on its own,
|
|
8
|
+
# but using unshareable default value or block is not thread-safe.
|
|
9
|
+
#
|
|
10
|
+
# @since 0.2.0
|
|
11
|
+
class HashMold
|
|
12
|
+
# Default value to be assigned to each produced hash.
|
|
13
|
+
# @return [Any, nil]
|
|
14
|
+
attr_reader :default
|
|
15
|
+
# Default proc to be assigned to each produced hash.
|
|
16
|
+
# @return [Proc, nil]
|
|
17
|
+
attr_reader :default_proc
|
|
18
|
+
|
|
19
|
+
# Initialize new HashMold with default value or default proc
|
|
20
|
+
# to be assigned to each produced hash.
|
|
21
|
+
#
|
|
22
|
+
# The same exact objects are used for each hash.
|
|
23
|
+
# It is not advised to use mutable objects as default values.
|
|
24
|
+
# Be aware that using a default proc with assignment
|
|
25
|
+
# is inherently not safe, see this Ruby issue:
|
|
26
|
+
# https://bugs.ruby-lang.org/issues/19237.
|
|
27
|
+
#
|
|
28
|
+
# @see Hash.new
|
|
29
|
+
#
|
|
30
|
+
# @param default_value [Any]
|
|
31
|
+
# @yieldparam hash [Hash]
|
|
32
|
+
# @yieldparam key [Any]
|
|
33
|
+
# @yieldreturn [Any]
|
|
34
|
+
def initialize(default_value = nil, &default_proc)
|
|
35
|
+
@default = default_value
|
|
36
|
+
@default_proc = default_proc
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Build a new hash using +forged.[]+.
|
|
40
|
+
#
|
|
41
|
+
# @see Hash.[]
|
|
42
|
+
#
|
|
43
|
+
# @param forged [Class] Hash or a subclass of Hash
|
|
44
|
+
# @param attributes [Hash{Symbol => Any}]
|
|
45
|
+
# @return [Hash]
|
|
46
|
+
def call(forged:, attributes:, **_)
|
|
47
|
+
hash = forged[attributes]
|
|
48
|
+
hash.default = @default if @default
|
|
49
|
+
hash.default_proc = @default_proc if @default_proc
|
|
50
|
+
hash
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
3
|
+
module ObjectForge
|
|
4
|
+
module Molds
|
|
5
|
+
# Basic mold which calls +forged.new(**attributes)+.
|
|
6
|
+
#
|
|
7
|
+
# Can be used instead of {SingleArgumentMold}
|
|
8
|
+
# due to how keyword arguments are treated in Ruby,
|
|
9
|
+
# but performance is about 1.5 times worse.
|
|
10
|
+
#
|
|
11
|
+
# @thread_safety Thread-safe.
|
|
12
|
+
# @since 0.2.0
|
|
13
|
+
class KeywordsMold
|
|
14
|
+
# Instantiate +forged+ with a hash of attributes.
|
|
15
|
+
#
|
|
16
|
+
# @param forged [Class, #new]
|
|
17
|
+
# @param attributes [Hash{Symbol => Any}]
|
|
18
|
+
# @return [Any]
|
|
19
|
+
def call(forged:, attributes:, **_)
|
|
20
|
+
forged.new(**attributes)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ObjectForge
|
|
4
|
+
module Molds
|
|
5
|
+
# Basic mold which calls +forged.new(attributes)+.
|
|
6
|
+
#
|
|
7
|
+
# @thread_safety Thread-safe.
|
|
8
|
+
# @since 0.2.0
|
|
9
|
+
class SingleArgumentMold
|
|
10
|
+
# Instantiate +forged+ with a hash of attributes.
|
|
11
|
+
#
|
|
12
|
+
# @param forged [Class, #new]
|
|
13
|
+
# @param attributes [Hash{Symbol => Any}]
|
|
14
|
+
# @return [Any]
|
|
15
|
+
def call(forged:, attributes:, **_)
|
|
16
|
+
forged.new(attributes)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ObjectForge
|
|
4
|
+
module Molds
|
|
5
|
+
# Mold for building Structs.
|
|
6
|
+
#
|
|
7
|
+
# Supports all variations of +keyword_init+.
|
|
8
|
+
#
|
|
9
|
+
# @thread_safety Thread-safe.
|
|
10
|
+
# @since 0.2.0
|
|
11
|
+
class StructMold
|
|
12
|
+
# Does Struct automatically use keyword initialization
|
|
13
|
+
# when +keyword_init+ is not specified / +nil+?
|
|
14
|
+
#
|
|
15
|
+
# This should be true on Ruby 3.2.0 and later.
|
|
16
|
+
#
|
|
17
|
+
# @return [Boolean]
|
|
18
|
+
RUBY_FEATURE_AUTO_KEYWORDS = (::Struct.new(:a, :b).new(a: 1, b: 2).a == 1)
|
|
19
|
+
|
|
20
|
+
# Whether to work around argument hashes with extra keys.
|
|
21
|
+
#
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
attr_reader :lax
|
|
24
|
+
alias lax? lax
|
|
25
|
+
|
|
26
|
+
# @param lax [Boolean]
|
|
27
|
+
# whether to work around argument hashes with extra keys
|
|
28
|
+
# (when keyword_init is false, workaround always happens for technical reasons)
|
|
29
|
+
# - if +true+, arguments can contain extra keys, but building is slower;
|
|
30
|
+
# - if +false+, building may raise an error if extra keys are present;
|
|
31
|
+
def initialize(lax: true)
|
|
32
|
+
@lax = lax
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Instantiate +forged+ struct with a hash of attributes.
|
|
36
|
+
#
|
|
37
|
+
# @param forged [Class] a subclass of Struct
|
|
38
|
+
# @param attributes [Hash{Symbol => Any}]
|
|
39
|
+
# @return [Struct]
|
|
40
|
+
def call(forged:, attributes:, **_)
|
|
41
|
+
if forged.keyword_init?
|
|
42
|
+
lax ? forged.new(attributes.slice(*forged.members)) : forged.new(attributes)
|
|
43
|
+
elsif forged.keyword_init? == false
|
|
44
|
+
forged.new(*attributes.values_at(*forged.members))
|
|
45
|
+
else
|
|
46
|
+
build_struct_with_unspecified_keyword_init(forged, attributes)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
if RUBY_FEATURE_AUTO_KEYWORDS
|
|
53
|
+
# Build struct by using keywords to specify member values.
|
|
54
|
+
def build_struct_with_unspecified_keyword_init(forged, attributes)
|
|
55
|
+
if lax
|
|
56
|
+
forged.new(**attributes.slice(*forged.members))
|
|
57
|
+
else
|
|
58
|
+
forged.new(**attributes)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
# :nocov:
|
|
63
|
+
# Build struct by using positional arguments to specify member values.
|
|
64
|
+
def build_struct_with_unspecified_keyword_init(forged, attributes)
|
|
65
|
+
forged.new(*attributes.values_at(*forged.members))
|
|
66
|
+
end
|
|
67
|
+
# :nocov:
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ObjectForge
|
|
4
|
+
module Molds
|
|
5
|
+
# Mold that wraps a mold class.
|
|
6
|
+
#
|
|
7
|
+
# Wrapping a mold class is useful when its +#call+ is stateful,
|
|
8
|
+
# making it unsafe to use multiple times or in shared environments.
|
|
9
|
+
#
|
|
10
|
+
# This mold is usually automatically used through {Molds.wrap_mold}.
|
|
11
|
+
#
|
|
12
|
+
# @thread_safety Thread-safe if {wrapped_mold} does not use global state.
|
|
13
|
+
# @since 0.2.0
|
|
14
|
+
class WrappedMold
|
|
15
|
+
# @return [Class] wrapped mold class
|
|
16
|
+
attr_reader :wrapped_mold
|
|
17
|
+
|
|
18
|
+
# @param wrapped_mold [Class] class with +#call+ method
|
|
19
|
+
def initialize(wrapped_mold)
|
|
20
|
+
@wrapped_mold = wrapped_mold
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @overload call(...)
|
|
24
|
+
# Instantiate {wrapped_mold} and call it.
|
|
25
|
+
#
|
|
26
|
+
# @return [Any] result of +wrapped_mold.new.call(...)+
|
|
27
|
+
def call(...)
|
|
28
|
+
wrapped_mold.new.call(...)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ObjectForge
|
|
4
|
+
# This module provides a collection of predefined molds to be used in common cases.
|
|
5
|
+
#
|
|
6
|
+
# Mold is an object that knows how to take a hash of attributes
|
|
7
|
+
# and create an object from them. Molds are +#call+able objects
|
|
8
|
+
# responsible for actually building objects produced by factories
|
|
9
|
+
# (or doing other, interesting things with them (truly, only the code review is the limit!)).
|
|
10
|
+
# They are supposed to be immutable, shareable, and persistent:
|
|
11
|
+
# initialize once, use for the whole runtime.
|
|
12
|
+
#
|
|
13
|
+
# A simple mold can easily be just a +Proc+.
|
|
14
|
+
# All molds must have the following +#call+ signature: +call(forged:, attributes:, **)+.
|
|
15
|
+
# The extra keywords are ignored for possibility of future extensions.
|
|
16
|
+
#
|
|
17
|
+
# @example A very basic FactoryBot replacement
|
|
18
|
+
# creator = ->(forged:, attributes:, **) do
|
|
19
|
+
# instance = forged.new
|
|
20
|
+
# attributes.each_pair { instance.public_send(:"#{_1}=", _2) }
|
|
21
|
+
# instance.save!
|
|
22
|
+
# end
|
|
23
|
+
# creator.call(forged: User, attributes: { name: "John", age: 30 })
|
|
24
|
+
# # => <User name="John" age=30>
|
|
25
|
+
# @example Using a mold to serialize collection of objects (contrivedly)
|
|
26
|
+
# dumpy = ->(forged:, attributes:, **) do
|
|
27
|
+
# Enumerator.new(attributes.size) do |y|
|
|
28
|
+
# attributes.each_pair { y << forged.dump(_1 => _2) }
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
# dumpy.call(forged: JSON, attributes: {a:1, b:2}).to_a
|
|
32
|
+
# # => ["{\"a\":1}", "{\"b\":2}"]
|
|
33
|
+
# dumpy.call(forged: YAML, attributes: {a:1, b:2}).to_a
|
|
34
|
+
# # => ["---\n:a: 1\n", "---\n:b: 2\n"]
|
|
35
|
+
# @example Abstract factory pattern (kind of)
|
|
36
|
+
# class FurnitureFactory
|
|
37
|
+
# def call(forged:, attributes:, **)
|
|
38
|
+
# concrete_factory = concrete_factory(forged)
|
|
39
|
+
# attributes[:pieces].map do |piece|
|
|
40
|
+
# concrete_factory.public_send(piece, attributes.dig(:color, piece))
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
# private def concrete_factory(style)
|
|
44
|
+
# case style
|
|
45
|
+
# when :hitech
|
|
46
|
+
# HiTechFactory.new
|
|
47
|
+
# when :retro
|
|
48
|
+
# RetroFactory.new
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
# FurnitureFactory.new.call(forged: :hitech, attributes: {
|
|
53
|
+
# pieces: [:chair, :table], color: { chair: :black, table: :white }
|
|
54
|
+
# })
|
|
55
|
+
# # => [<#HiTech::Chair color=:black>, <#HiTech::Table color=:white>]
|
|
56
|
+
# @example Abusing molds
|
|
57
|
+
# printer = ->(forged:, attributes:, **) { PP.pp(attributes, forged) }
|
|
58
|
+
# printer.call(forged: $stderr, attributes: {a:1, b:2})
|
|
59
|
+
# # outputs "{:a=>1, :b=>2}" to $stderr
|
|
60
|
+
#
|
|
61
|
+
# @since 0.2.0
|
|
62
|
+
module Molds
|
|
63
|
+
Dir["#{__dir__}/molds/*.rb"].each { require_relative _1 }
|
|
64
|
+
|
|
65
|
+
# Get maybe appropriate mold for the given +forged+ class or object.
|
|
66
|
+
#
|
|
67
|
+
# Currently provides specific recognition for:
|
|
68
|
+
# - subclasses of +Struct+ ({StructMold}),
|
|
69
|
+
# - subclasses of +Data+ ({KeywordsMold}),
|
|
70
|
+
# - +Hash+ and subclasses ({HashMold}).
|
|
71
|
+
# Other objects just get {SingleArgumentMold}.
|
|
72
|
+
#
|
|
73
|
+
# @param forged [Class, Any]
|
|
74
|
+
# @return [#call] an instance of a mold
|
|
75
|
+
#
|
|
76
|
+
# @thread_safety Thread-safe.
|
|
77
|
+
# @since 0.3.0
|
|
78
|
+
def self.mold_for(forged)
|
|
79
|
+
if ::Class === forged
|
|
80
|
+
if forged < ::Struct
|
|
81
|
+
StructMold.new
|
|
82
|
+
elsif defined?(::Data) && forged < ::Data
|
|
83
|
+
KeywordsMold.new
|
|
84
|
+
elsif forged <= ::Hash
|
|
85
|
+
HashMold.new
|
|
86
|
+
else
|
|
87
|
+
SingleArgumentMold.new
|
|
88
|
+
end
|
|
89
|
+
else
|
|
90
|
+
SingleArgumentMold.new
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Wrap mold if needed.
|
|
95
|
+
#
|
|
96
|
+
# If +mold+ is +nil+ or a +call+able object, returns it.
|
|
97
|
+
# If it is a Class with +#call+, wraps it in {WrappedMold}.
|
|
98
|
+
# Otherwise, raises an error.
|
|
99
|
+
#
|
|
100
|
+
# @since 0.3.0
|
|
101
|
+
#
|
|
102
|
+
# @param mold [Class, #call, nil]
|
|
103
|
+
# @return [#call, nil]
|
|
104
|
+
#
|
|
105
|
+
# @raise [DSLError] if +mold+ does not respond to or implement +#call+
|
|
106
|
+
#
|
|
107
|
+
# @thread_safety Thread-safe.
|
|
108
|
+
# @since 0.3.0
|
|
109
|
+
def self.wrap_mold(mold)
|
|
110
|
+
if mold.nil? || mold.respond_to?(:call)
|
|
111
|
+
mold # : ObjectForge::mold?
|
|
112
|
+
elsif ::Class === mold && mold.public_method_defined?(:call)
|
|
113
|
+
WrappedMold.new(mold)
|
|
114
|
+
else
|
|
115
|
+
raise MoldError, "mold must respond to or implement #call"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -4,7 +4,6 @@ module ObjectForge
|
|
|
4
4
|
# BasicObject with a few common methods copied from Object.
|
|
5
5
|
#
|
|
6
6
|
# @api private
|
|
7
|
-
#
|
|
8
7
|
# @since 0.1.0
|
|
9
8
|
class UnBasicObject < ::BasicObject
|
|
10
9
|
# @!group Instance methods copied from Object
|
|
@@ -35,13 +34,14 @@ module ObjectForge
|
|
|
35
34
|
# @!method to_s
|
|
36
35
|
# @see Object#to_s
|
|
37
36
|
# @return [String]
|
|
38
|
-
%i[class eql? freeze frozen? hash inspect is_a? respond_to? to_s].each do |m|
|
|
39
|
-
define_method(m, ::
|
|
37
|
+
%i[class eql? freeze frozen? hash inspect is_a? kind_of? respond_to? to_s].each do |m|
|
|
38
|
+
define_method(m, ::Kernel.instance_method(m))
|
|
40
39
|
end
|
|
41
|
-
alias kind_of? is_a?
|
|
42
40
|
# @!endgroup
|
|
43
41
|
|
|
44
|
-
%i[block_given? raise].each
|
|
42
|
+
%i[block_given? raise respond_to_missing?].each do |m|
|
|
43
|
+
private define_method(m, ::Kernel.instance_method(m))
|
|
44
|
+
end
|
|
45
45
|
|
|
46
46
|
# @!macro pp_support
|
|
47
47
|
# Support for +pp+ (and IRB).
|
data/lib/object_forge/version.rb
CHANGED
data/lib/object_forge.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "object_forge/forgeyard"
|
|
4
|
+
require_relative "object_forge/sequence"
|
|
5
|
+
require_relative "object_forge/version"
|
|
4
6
|
|
|
5
7
|
# A simple all-purpose factory library with minimal assumptions.
|
|
6
8
|
#
|
|
@@ -20,6 +22,7 @@ Dir["#{__dir__}/object_forge/**/*.rb"].each { require _1 }
|
|
|
20
22
|
# It allows defining arbitrary attributes (possibly using sequences),
|
|
21
23
|
# with support for traits (collections of attributes with non-default values).
|
|
22
24
|
# - {Crucible} is used to resolve attributes.
|
|
25
|
+
# - {Molds} are objects used to instantiate objects in {Forge}.
|
|
23
26
|
#
|
|
24
27
|
# @example Quick example
|
|
25
28
|
# Frobinator = Struct.new(:frob, :inator, keyword_init: true)
|
|
@@ -36,7 +39,7 @@ Dir["#{__dir__}/object_forge/**/*.rb"].each { require _1 }
|
|
|
36
39
|
# # => #<struct Frobinator frob="Frobinator", inator=#<Proc:...>>
|
|
37
40
|
# ObjectForge.build(:frobber, frob: -> { "Frob" + inator }, inator: "orn")
|
|
38
41
|
# # => #<struct Frobinator frob="Froborn", inator="orn">
|
|
39
|
-
# ObjectForge
|
|
42
|
+
# ObjectForge.call(:frobber, :static, inator: "Value")
|
|
40
43
|
# # => #<struct Frobinator frob="Static", inator="Value">
|
|
41
44
|
module ObjectForge
|
|
42
45
|
# Base error class for ObjectForge.
|
|
@@ -45,6 +48,9 @@ module ObjectForge
|
|
|
45
48
|
# Error raised when a mistake is made in using DSL.
|
|
46
49
|
# @since 0.1.0
|
|
47
50
|
class DSLError < Error; end
|
|
51
|
+
# Error raised when object can not be used as a mold.
|
|
52
|
+
# @since 0.3.0
|
|
53
|
+
class MoldError < Error; end
|
|
48
54
|
|
|
49
55
|
# Default {Forgeyard} that is used by {.define} and {.forge}.
|
|
50
56
|
#
|
|
@@ -76,7 +82,7 @@ module ObjectForge
|
|
|
76
82
|
# @since 0.1.0
|
|
77
83
|
#
|
|
78
84
|
# @param name [Symbol] forge name
|
|
79
|
-
# @param forged [Class] class to forge
|
|
85
|
+
# @param forged [Class, Any] class or object to forge
|
|
80
86
|
# @yieldparam f [ForgeDSL]
|
|
81
87
|
# @yieldreturn [void]
|
|
82
88
|
# @return [Forge] forge
|
|
@@ -102,9 +108,7 @@ module ObjectForge
|
|
|
102
108
|
end
|
|
103
109
|
|
|
104
110
|
class << self
|
|
105
|
-
# @since 0.1.0
|
|
106
111
|
alias build forge
|
|
107
|
-
|
|
108
|
-
alias [] forge
|
|
112
|
+
alias call forge
|
|
109
113
|
end
|
|
110
114
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module ObjectForge
|
|
2
|
+
module Molds
|
|
3
|
+
def self.wrap_mold
|
|
4
|
+
: ((ObjectForge::mold | Class | nil) mold) -> ObjectForge::mold?
|
|
5
|
+
def self.mold_for
|
|
6
|
+
: (ObjectForge::_Forgable forged) -> ObjectForge::mold
|
|
7
|
+
|
|
8
|
+
class SingleArgumentMold
|
|
9
|
+
include ObjectForge::_Mold
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class KeywordsMold
|
|
13
|
+
include ObjectForge::_Mold
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class WrappedMold
|
|
17
|
+
interface _MoldClass[T]
|
|
18
|
+
def new: () -> T
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
include ObjectForge::_Mold
|
|
22
|
+
|
|
23
|
+
attr_reader wrapped_mold: _MoldClass[ObjectForge::mold]
|
|
24
|
+
|
|
25
|
+
def initialize: (_MoldClass[ObjectForge::mold] wrapped_mold) -> void
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class StructMold
|
|
29
|
+
interface _StructSubclass[T]
|
|
30
|
+
def new
|
|
31
|
+
: (*untyped) -> T
|
|
32
|
+
| (**untyped) -> T
|
|
33
|
+
| (Hash[Symbol, untyped]) -> T
|
|
34
|
+
def members: -> Array[Symbol]
|
|
35
|
+
def keyword_init?: -> bool?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
RUBY_FEATURE_AUTO_KEYWORDS: bool
|
|
39
|
+
attr_reader lax: bool
|
|
40
|
+
|
|
41
|
+
def initialize: (?lax: bool) -> void
|
|
42
|
+
|
|
43
|
+
def call
|
|
44
|
+
: [T < Struct] (forged: _StructSubclass[T], attributes: Hash[Symbol, untyped], **untyped) -> T
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_struct_with_unspecified_keyword_init
|
|
49
|
+
: [T < Struct] (_StructSubclass[T] forged, Hash[Symbol, untyped] attributes) -> T
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class HashMold
|
|
53
|
+
interface _HashSubclass[T]
|
|
54
|
+
def []: (Hash[untyped, untyped]) -> T
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader default: untyped?
|
|
58
|
+
attr_reader default_proc: Proc?
|
|
59
|
+
|
|
60
|
+
def initialize
|
|
61
|
+
: (?untyped? default_value) ?{ (Hash[untyped, untyped] hash, untyped key) -> untyped} -> void
|
|
62
|
+
|
|
63
|
+
def call
|
|
64
|
+
: [T < Hash] (forged: _HashSubclass[T], attributes: Hash[Symbol, untyped], **untyped) -> T
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/sig/object_forge.rbs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
module ObjectForge
|
|
2
|
+
type mold = Object & ObjectForge::_Mold
|
|
2
3
|
type sequenceable = ObjectForge::_RespondTo & ObjectForge::_Sequenceable
|
|
3
4
|
|
|
4
5
|
interface _RespondTo
|
|
5
6
|
def respond_to?: (Symbol name, ?bool include_private) -> bool
|
|
6
7
|
def class: -> Class
|
|
7
8
|
end
|
|
8
|
-
interface _Sequenceable
|
|
9
|
+
interface _Sequenceable
|
|
9
10
|
def succ: -> self
|
|
10
11
|
end
|
|
11
12
|
interface _Forgable
|
|
@@ -14,12 +15,19 @@ interface _Sequenceable
|
|
|
14
15
|
interface _ForgeParameters
|
|
15
16
|
def attributes: () -> Hash[Symbol, untyped]
|
|
16
17
|
def traits: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
18
|
+
def settings: () -> Hash[Symbol, untyped]
|
|
19
|
+
end
|
|
20
|
+
interface _Mold
|
|
21
|
+
def call
|
|
22
|
+
: (forged: untyped, attributes: Hash[Symbol, untyped], **untyped) -> untyped
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
class Error < StandardError
|
|
20
26
|
end
|
|
21
27
|
class DSLError < Error
|
|
22
28
|
end
|
|
29
|
+
class MoldError < Error
|
|
30
|
+
end
|
|
23
31
|
|
|
24
32
|
VERSION: String
|
|
25
33
|
DEFAULT_YARD: ObjectForge::Forgeyard
|
|
@@ -33,6 +41,10 @@ interface _Sequenceable
|
|
|
33
41
|
|
|
34
42
|
def self.forge
|
|
35
43
|
: (Symbol name, *Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
44
|
+
def self.build
|
|
45
|
+
: (Symbol name, *Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
46
|
+
def self.call
|
|
47
|
+
: (Symbol name, *Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
class ObjectForge::Sequence
|
|
@@ -61,11 +73,14 @@ class ObjectForge::Forgeyard
|
|
|
61
73
|
|
|
62
74
|
def register
|
|
63
75
|
: (Symbol name, ObjectForge::Forge forge) -> ObjectForge::Forge
|
|
76
|
+
|
|
77
|
+
def []
|
|
78
|
+
: (Symbol name) -> ObjectForge::Forge
|
|
64
79
|
|
|
65
80
|
def forge
|
|
66
81
|
: (Symbol name, *Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
67
82
|
alias build forge
|
|
68
|
-
alias
|
|
83
|
+
alias call forge
|
|
69
84
|
end
|
|
70
85
|
|
|
71
86
|
class ObjectForge::Forge
|
|
@@ -89,15 +104,15 @@ class ObjectForge::Forge
|
|
|
89
104
|
def forge
|
|
90
105
|
: (*Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
91
106
|
alias build forge
|
|
92
|
-
alias
|
|
107
|
+
alias call forge
|
|
93
108
|
|
|
94
109
|
private
|
|
95
110
|
|
|
111
|
+
def determine_mold
|
|
112
|
+
: (ObjectForge::_Forgable forged, ObjectForge::mold mold) -> ObjectForge::mold
|
|
113
|
+
|
|
96
114
|
def resolve_attributes
|
|
97
115
|
: (Array[Symbol] traits, Hash[Symbol, untyped] overrides) -> Hash[Symbol, untyped]
|
|
98
|
-
|
|
99
|
-
def build_instance
|
|
100
|
-
: (Hash[Symbol, untyped] attributes) -> ObjectForge::_Forgable
|
|
101
116
|
end
|
|
102
117
|
|
|
103
118
|
class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
@@ -108,6 +123,7 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
108
123
|
@attributes: Hash[Symbol, Proc]
|
|
109
124
|
@sequences: Hash[Symbol, ObjectForge::Sequence]
|
|
110
125
|
@traits: Hash[Symbol, Hash[Symbol, Proc]]
|
|
126
|
+
@settings: Hash[Symbol, untyped]
|
|
111
127
|
|
|
112
128
|
def initialize
|
|
113
129
|
: () { (ObjectForge::ForgeDSL) -> void } -> void
|
|
@@ -115,6 +131,9 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
115
131
|
|
|
116
132
|
def freeze: -> self
|
|
117
133
|
|
|
134
|
+
def setting
|
|
135
|
+
: (Symbol name, untyped value) -> Symbol
|
|
136
|
+
|
|
118
137
|
def attribute
|
|
119
138
|
: (Symbol name) { [self: ObjectForge::Crucible] -> untyped } -> Symbol
|
|
120
139
|
alias [] attribute
|
|
@@ -136,8 +155,9 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
136
155
|
|
|
137
156
|
def respond_to_missing?
|
|
138
157
|
: (Symbol name, bool include_all) -> bool
|
|
139
|
-
|
|
140
|
-
def
|
|
158
|
+
|
|
159
|
+
def valid_setting_method?
|
|
160
|
+
: (Symbol name) -> bool
|
|
141
161
|
end
|
|
142
162
|
|
|
143
163
|
class ObjectForge::Crucible < ObjectForge::UnBasicObject
|
|
@@ -174,8 +194,9 @@ class ObjectForge::UnBasicObject < BasicObject
|
|
|
174
194
|
def inspect: -> String
|
|
175
195
|
|
|
176
196
|
def is_a?: (Module klass) -> bool
|
|
197
|
+
def kind_of?: (Module klass) -> bool
|
|
177
198
|
|
|
178
|
-
def respond_to?: (Symbol name, ?bool
|
|
199
|
+
def respond_to?: (Symbol name, ?bool include_all) -> bool
|
|
179
200
|
|
|
180
201
|
def to_s: -> String
|
|
181
202
|
|
|
@@ -188,4 +209,6 @@ class ObjectForge::UnBasicObject < BasicObject
|
|
|
188
209
|
def block_given?: -> bool
|
|
189
210
|
|
|
190
211
|
def raise: (_Exception exception, ?String message, ?Array[String] backtrace, ?cause: _Exception) -> void
|
|
212
|
+
|
|
213
|
+
def respond_to_missing?: (Symbol name, bool include_all) -> bool
|
|
191
214
|
end
|
metadata
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: object_forge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Alexander Bulancov
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
@@ -32,31 +32,41 @@ description: |
|
|
|
32
32
|
If needed, almost any part of the process can be easily replaced with a custom solution.
|
|
33
33
|
executables: []
|
|
34
34
|
extensions: []
|
|
35
|
-
extra_rdoc_files:
|
|
35
|
+
extra_rdoc_files:
|
|
36
|
+
- README.md
|
|
36
37
|
files:
|
|
38
|
+
- README.md
|
|
37
39
|
- lib/object_forge.rb
|
|
38
40
|
- lib/object_forge/crucible.rb
|
|
39
41
|
- lib/object_forge/forge.rb
|
|
40
42
|
- lib/object_forge/forge_dsl.rb
|
|
41
43
|
- lib/object_forge/forgeyard.rb
|
|
44
|
+
- lib/object_forge/molds.rb
|
|
45
|
+
- lib/object_forge/molds/hash_mold.rb
|
|
42
46
|
- lib/object_forge/molds/keywords_mold.rb
|
|
47
|
+
- lib/object_forge/molds/single_argument_mold.rb
|
|
48
|
+
- lib/object_forge/molds/struct_mold.rb
|
|
49
|
+
- lib/object_forge/molds/wrapped_mold.rb
|
|
43
50
|
- lib/object_forge/sequence.rb
|
|
44
51
|
- lib/object_forge/un_basic_object.rb
|
|
45
52
|
- lib/object_forge/version.rb
|
|
46
53
|
- sig/object_forge.rbs
|
|
54
|
+
- sig/object_forge/molds.rbs
|
|
47
55
|
homepage: https://github.com/trinistr/object_forge
|
|
48
56
|
licenses:
|
|
49
57
|
- MIT
|
|
50
58
|
metadata:
|
|
51
59
|
homepage_uri: https://github.com/trinistr/object_forge
|
|
52
60
|
bug_tracker_uri: https://github.com/trinistr/object_forge/issues
|
|
53
|
-
documentation_uri: https://rubydoc.info/gems/object_forge/0.
|
|
54
|
-
source_code_uri: https://github.com/trinistr/object_forge/tree/v0.
|
|
55
|
-
changelog_uri: https://github.com/trinistr/object_forge/blob/v0.
|
|
61
|
+
documentation_uri: https://rubydoc.info/gems/object_forge/0.3.0
|
|
62
|
+
source_code_uri: https://github.com/trinistr/object_forge/tree/v0.3.0
|
|
63
|
+
changelog_uri: https://github.com/trinistr/object_forge/blob/v0.3.0/CHANGELOG.md
|
|
56
64
|
rubygems_mfa_required: 'true'
|
|
57
65
|
rdoc_options:
|
|
58
66
|
- "--tag"
|
|
59
67
|
- thread_safety:Thread safety
|
|
68
|
+
- "--main"
|
|
69
|
+
- README.md
|
|
60
70
|
require_paths:
|
|
61
71
|
- lib
|
|
62
72
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
@@ -70,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
80
|
- !ruby/object:Gem::Version
|
|
71
81
|
version: '0'
|
|
72
82
|
requirements: []
|
|
73
|
-
rubygems_version: 3.
|
|
83
|
+
rubygems_version: 3.6.9
|
|
74
84
|
specification_version: 4
|
|
75
85
|
summary: A simple factory for objects with minimal assumptions.
|
|
76
86
|
test_files: []
|