object_forge 0.2.0 → 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 -1
- data/lib/object_forge/forge.rb +33 -11
- data/lib/object_forge/forge_dsl.rb +45 -31
- data/lib/object_forge/forgeyard.rb +12 -2
- data/lib/object_forge/molds/keywords_mold.rb +2 -1
- data/lib/object_forge/molds/struct_mold.rb +2 -0
- data/lib/object_forge/molds/wrapped_mold.rb +2 -0
- data/lib/object_forge/molds.rb +58 -2
- data/lib/object_forge/un_basic_object.rb +5 -4
- data/lib/object_forge/version.rb +1 -1
- data/lib/object_forge.rb +6 -4
- data/sig/object_forge/molds.rbs +4 -9
- data/sig/object_forge.rbs +30 -11
- metadata +12 -13
- data/lib/object_forge/molds/mold_mold.rb +0 -40
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).
|
|
@@ -14,7 +14,7 @@ module ObjectForge
|
|
|
14
14
|
# but modifies instance variables, making it unsafe to share the Crucible
|
|
15
15
|
# @since 0.1.0
|
|
16
16
|
class Crucible < UnBasicObject
|
|
17
|
-
%i[rand].each { |m| private define_method(m, ::
|
|
17
|
+
%i[rand].each { |m| private define_method(m, ::Kernel.instance_method(m)) }
|
|
18
18
|
|
|
19
19
|
# @param attributes [Hash{Symbol => Proc, Any}] initial attributes
|
|
20
20
|
def initialize(attributes)
|
data/lib/object_forge/forge.rb
CHANGED
|
@@ -21,14 +21,13 @@ module ObjectForge
|
|
|
21
21
|
# Attributes belonging to traits.
|
|
22
22
|
# @return [Hash{Symbol => Hash{Symbol => Any}}]
|
|
23
23
|
#
|
|
24
|
-
# @!attribute [r]
|
|
25
|
-
#
|
|
26
|
-
# Must
|
|
27
|
-
#
|
|
28
|
-
# @
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
MOLD_MOLD = Molds::MoldMold.new.freeze
|
|
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)
|
|
32
31
|
|
|
33
32
|
# Define (and create) a forge using DSL.
|
|
34
33
|
#
|
|
@@ -55,12 +54,13 @@ module ObjectForge
|
|
|
55
54
|
|
|
56
55
|
# @param forged [Class, Any] class or object to forge
|
|
57
56
|
# @param parameters [Parameters, ForgeDSL] forge parameters
|
|
58
|
-
# @param name [Symbol, nil] forge name
|
|
57
|
+
# @param name [Symbol, nil] forge name;
|
|
58
|
+
# only used for identification purposes
|
|
59
59
|
def initialize(forged, parameters, name: nil)
|
|
60
60
|
@name = name
|
|
61
61
|
@forged = forged
|
|
62
62
|
@parameters = parameters
|
|
63
|
-
@mold = parameters.mold
|
|
63
|
+
@mold = determine_mold(forged, parameters.settings[:mold])
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Forge a new instance.
|
|
@@ -92,10 +92,32 @@ module ObjectForge
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
alias build forge
|
|
95
|
-
alias
|
|
95
|
+
alias call forge
|
|
96
96
|
|
|
97
97
|
private
|
|
98
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}]
|
|
99
121
|
def resolve_attributes(traits, overrides)
|
|
100
122
|
attributes = @parameters.attributes.merge(*@parameters.traits.values_at(*traits), overrides)
|
|
101
123
|
Crucible.new(attributes).resolve!
|
|
@@ -27,8 +27,8 @@ module ObjectForge
|
|
|
27
27
|
# @return [Hash{Symbol => Hash{Symbol => Proc}}] trait definitions
|
|
28
28
|
attr_reader :traits
|
|
29
29
|
|
|
30
|
-
# @return [
|
|
31
|
-
attr_reader :
|
|
30
|
+
# @return [Hash{Symbol => Any}] settings for forge, such as mold
|
|
31
|
+
attr_reader :settings
|
|
32
32
|
|
|
33
33
|
# Define forge's parameters through DSL.
|
|
34
34
|
#
|
|
@@ -39,6 +39,7 @@ module ObjectForge
|
|
|
39
39
|
#
|
|
40
40
|
# @example with block parameter
|
|
41
41
|
# ForgeDSL.new do |f|
|
|
42
|
+
# f.mold = ObjectForge::Molds::KeywordsMolds.new
|
|
42
43
|
# f.attribute(:name) { "Name" }
|
|
43
44
|
# f[:description] { name.upcase }
|
|
44
45
|
# f.duration { rand(1000) }
|
|
@@ -46,6 +47,7 @@ module ObjectForge
|
|
|
46
47
|
#
|
|
47
48
|
# @example without block parameter
|
|
48
49
|
# ForgeDSL.new do
|
|
50
|
+
# self.mold = ::ObjectForge::Molds::KeywordsMolds.new
|
|
49
51
|
# attribute(:name) { "Name" }
|
|
50
52
|
# self[:description] { name.upcase }
|
|
51
53
|
# duration { rand(1000) }
|
|
@@ -58,49 +60,53 @@ module ObjectForge
|
|
|
58
60
|
@attributes = {}
|
|
59
61
|
@sequences = {}
|
|
60
62
|
@traits = {}
|
|
63
|
+
@settings = {}
|
|
61
64
|
|
|
62
65
|
dsl.arity.zero? ? instance_exec(&dsl) : yield(self)
|
|
63
66
|
|
|
64
67
|
freeze
|
|
65
68
|
end
|
|
66
69
|
|
|
67
|
-
# Freezes the instance, including +attributes+, +sequences+ and +traits+.
|
|
70
|
+
# Freezes the instance, including +settings+, +attributes+, +sequences+ and +traits+.
|
|
68
71
|
# Prevents further responses through +#method_missing+.
|
|
69
72
|
#
|
|
70
73
|
# @note Called automatically in {#initialize}.
|
|
71
74
|
#
|
|
72
75
|
# @return [self]
|
|
73
76
|
def freeze
|
|
74
|
-
::
|
|
77
|
+
::Kernel.instance_method(:freeze).bind_call(self)
|
|
75
78
|
@attributes.freeze
|
|
76
79
|
@sequences.freeze
|
|
77
80
|
@traits.freeze
|
|
78
|
-
@
|
|
81
|
+
@settings.freeze
|
|
79
82
|
self
|
|
80
83
|
end
|
|
81
84
|
|
|
82
|
-
# Set
|
|
85
|
+
# Set a value for a forge's setting.
|
|
83
86
|
#
|
|
84
|
-
#
|
|
85
|
-
# and create an object from them.
|
|
86
|
-
# It can also be a class with +#call+, in which case a new mold will be instantiated
|
|
87
|
-
# automatically for each build. If a single instance is enough,
|
|
88
|
-
# please call +.new+ yourself once.
|
|
87
|
+
# Possible settings depend on used forge, but for default {Forge} a +mold+ is expected.
|
|
89
88
|
#
|
|
90
|
-
#
|
|
89
|
+
# It is also possible to set settings through +method_missing+, using name with a +=+ suffix.
|
|
91
90
|
#
|
|
92
|
-
# @
|
|
93
|
-
# @return [Class, #call, nil]
|
|
91
|
+
# @see Molds
|
|
94
92
|
#
|
|
95
|
-
# @
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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"
|
|
103
105
|
end
|
|
106
|
+
|
|
107
|
+
@settings[name] = value
|
|
108
|
+
|
|
109
|
+
name
|
|
104
110
|
end
|
|
105
111
|
|
|
106
112
|
# Define an attribute, possibly transient.
|
|
@@ -255,26 +261,30 @@ module ObjectForge
|
|
|
255
261
|
|
|
256
262
|
private
|
|
257
263
|
|
|
258
|
-
# Define an attribute using a shorthand.
|
|
264
|
+
# Define an attribute (like +name+) or set a setting (like +name=+) using a shorthand.
|
|
259
265
|
#
|
|
260
|
-
# Can not be used
|
|
266
|
+
# Can not be used with reserved names.
|
|
261
267
|
# Trying to use a conflicting name will lead to usual issues
|
|
262
268
|
# with calling random methods.
|
|
263
|
-
# When in doubt, use {#attribute} or {#
|
|
269
|
+
# When in doubt, use {#attribute} or {#setting} instead.
|
|
264
270
|
#
|
|
265
271
|
# Reserved names are:
|
|
266
|
-
# - all names ending in +?+, +!+
|
|
272
|
+
# - all names ending in +?+, +!+
|
|
267
273
|
# - all names starting with a non-word ASCII character
|
|
268
274
|
# (operators, +`+, +[]+, +[]=+)
|
|
269
275
|
# - +rand+
|
|
270
276
|
#
|
|
271
|
-
# @param name [Symbol] attribute name
|
|
277
|
+
# @param name [Symbol] attribute or setting name
|
|
278
|
+
# @param value [Any] value for setting
|
|
272
279
|
# @yieldreturn [Any] attribute value
|
|
273
|
-
# @return [Symbol] attribute name
|
|
280
|
+
# @return [Symbol] attribute or setting name
|
|
274
281
|
#
|
|
275
282
|
# @raise [DSLError] if a reserved +name+ is used
|
|
276
|
-
def method_missing(name, **nil, &)
|
|
277
|
-
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
|
|
278
288
|
return attribute(name, &) if respond_to_missing?(name, false)
|
|
279
289
|
|
|
280
290
|
raise DSLError, "#{name.inspect} is a reserved name (in #{name.inspect})"
|
|
@@ -283,7 +293,11 @@ module ObjectForge
|
|
|
283
293
|
def respond_to_missing?(name, _include_all)
|
|
284
294
|
return false if frozen?
|
|
285
295
|
|
|
286
|
-
!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/)
|
|
287
301
|
end
|
|
288
302
|
end
|
|
289
303
|
end
|
|
@@ -45,6 +45,16 @@ module ObjectForge
|
|
|
45
45
|
@forges.put_if_absent(name, forge) || forge
|
|
46
46
|
end
|
|
47
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
|
+
|
|
48
58
|
# Build an instance using a forge.
|
|
49
59
|
#
|
|
50
60
|
# @see Forge#forge
|
|
@@ -58,10 +68,10 @@ module ObjectForge
|
|
|
58
68
|
#
|
|
59
69
|
# @raise [KeyError] if forge with the specified name is not registered
|
|
60
70
|
def forge(name, ...)
|
|
61
|
-
@forges.fetch(name).
|
|
71
|
+
@forges.fetch(name).call(...)
|
|
62
72
|
end
|
|
63
73
|
|
|
64
74
|
alias build forge
|
|
65
|
-
alias
|
|
75
|
+
alias call forge
|
|
66
76
|
end
|
|
67
77
|
end
|
|
@@ -4,7 +4,8 @@ module ObjectForge
|
|
|
4
4
|
module Molds
|
|
5
5
|
# Basic mold which calls +forged.new(**attributes)+.
|
|
6
6
|
#
|
|
7
|
-
# Can be used instead of {SingleArgumentMold}
|
|
7
|
+
# Can be used instead of {SingleArgumentMold}
|
|
8
|
+
# due to how keyword arguments are treated in Ruby,
|
|
8
9
|
# but performance is about 1.5 times worse.
|
|
9
10
|
#
|
|
10
11
|
# @thread_safety Thread-safe.
|
|
@@ -12,6 +12,8 @@ module ObjectForge
|
|
|
12
12
|
# Does Struct automatically use keyword initialization
|
|
13
13
|
# when +keyword_init+ is not specified / +nil+?
|
|
14
14
|
#
|
|
15
|
+
# This should be true on Ruby 3.2.0 and later.
|
|
16
|
+
#
|
|
15
17
|
# @return [Boolean]
|
|
16
18
|
RUBY_FEATURE_AUTO_KEYWORDS = (::Struct.new(:a, :b).new(a: 1, b: 2).a == 1)
|
|
17
19
|
|
|
@@ -7,6 +7,8 @@ module ObjectForge
|
|
|
7
7
|
# Wrapping a mold class is useful when its +#call+ is stateful,
|
|
8
8
|
# making it unsafe to use multiple times or in shared environments.
|
|
9
9
|
#
|
|
10
|
+
# This mold is usually automatically used through {Molds.wrap_mold}.
|
|
11
|
+
#
|
|
10
12
|
# @thread_safety Thread-safe if {wrapped_mold} does not use global state.
|
|
11
13
|
# @since 0.2.0
|
|
12
14
|
class WrappedMold
|
data/lib/object_forge/molds.rb
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
module ObjectForge
|
|
4
4
|
# This module provides a collection of predefined molds to be used in common cases.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
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
|
|
7
9
|
# (or doing other, interesting things with them (truly, only the code review is the limit!)).
|
|
8
10
|
# They are supposed to be immutable, shareable, and persistent:
|
|
9
11
|
# initialize once, use for the whole runtime.
|
|
10
12
|
#
|
|
11
13
|
# A simple mold can easily be just a +Proc+.
|
|
12
14
|
# All molds must have the following +#call+ signature: +call(forged:, attributes:, **)+.
|
|
13
|
-
# The extra keywords are for future extensions.
|
|
15
|
+
# The extra keywords are ignored for possibility of future extensions.
|
|
14
16
|
#
|
|
15
17
|
# @example A very basic FactoryBot replacement
|
|
16
18
|
# creator = ->(forged:, attributes:, **) do
|
|
@@ -59,5 +61,59 @@ module ObjectForge
|
|
|
59
61
|
# @since 0.2.0
|
|
60
62
|
module Molds
|
|
61
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
|
|
62
118
|
end
|
|
63
119
|
end
|
|
@@ -34,13 +34,14 @@ module ObjectForge
|
|
|
34
34
|
# @!method to_s
|
|
35
35
|
# @see Object#to_s
|
|
36
36
|
# @return [String]
|
|
37
|
-
%i[class eql? freeze frozen? hash inspect is_a? respond_to? to_s].each do |m|
|
|
38
|
-
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))
|
|
39
39
|
end
|
|
40
|
-
alias kind_of? is_a?
|
|
41
40
|
# @!endgroup
|
|
42
41
|
|
|
43
|
-
%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
|
|
44
45
|
|
|
45
46
|
# @!macro pp_support
|
|
46
47
|
# Support for +pp+ (and IRB).
|
data/lib/object_forge/version.rb
CHANGED
data/lib/object_forge.rb
CHANGED
|
@@ -22,6 +22,7 @@ require_relative "object_forge/version"
|
|
|
22
22
|
# It allows defining arbitrary attributes (possibly using sequences),
|
|
23
23
|
# with support for traits (collections of attributes with non-default values).
|
|
24
24
|
# - {Crucible} is used to resolve attributes.
|
|
25
|
+
# - {Molds} are objects used to instantiate objects in {Forge}.
|
|
25
26
|
#
|
|
26
27
|
# @example Quick example
|
|
27
28
|
# Frobinator = Struct.new(:frob, :inator, keyword_init: true)
|
|
@@ -38,7 +39,7 @@ require_relative "object_forge/version"
|
|
|
38
39
|
# # => #<struct Frobinator frob="Frobinator", inator=#<Proc:...>>
|
|
39
40
|
# ObjectForge.build(:frobber, frob: -> { "Frob" + inator }, inator: "orn")
|
|
40
41
|
# # => #<struct Frobinator frob="Froborn", inator="orn">
|
|
41
|
-
# ObjectForge
|
|
42
|
+
# ObjectForge.call(:frobber, :static, inator: "Value")
|
|
42
43
|
# # => #<struct Frobinator frob="Static", inator="Value">
|
|
43
44
|
module ObjectForge
|
|
44
45
|
# Base error class for ObjectForge.
|
|
@@ -47,6 +48,9 @@ module ObjectForge
|
|
|
47
48
|
# Error raised when a mistake is made in using DSL.
|
|
48
49
|
# @since 0.1.0
|
|
49
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
|
|
50
54
|
|
|
51
55
|
# Default {Forgeyard} that is used by {.define} and {.forge}.
|
|
52
56
|
#
|
|
@@ -104,9 +108,7 @@ module ObjectForge
|
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
class << self
|
|
107
|
-
# @since 0.1.0
|
|
108
111
|
alias build forge
|
|
109
|
-
|
|
110
|
-
alias [] forge
|
|
112
|
+
alias call forge
|
|
111
113
|
end
|
|
112
114
|
end
|
data/sig/object_forge/molds.rbs
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
module ObjectForge
|
|
2
|
-
interface _Mold
|
|
3
|
-
def call
|
|
4
|
-
: (forged: untyped, attributes: Hash[Symbol, untyped], **untyped) -> untyped
|
|
5
|
-
end
|
|
6
|
-
|
|
7
2
|
module Molds
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
def self.wrap_mold
|
|
4
|
+
: ((ObjectForge::mold | Class | nil) mold) -> ObjectForge::mold?
|
|
5
|
+
def self.mold_for
|
|
6
|
+
: (ObjectForge::_Forgable forged) -> ObjectForge::mold
|
|
12
7
|
|
|
13
8
|
class SingleArgumentMold
|
|
14
9
|
include ObjectForge::_Mold
|
data/sig/object_forge.rbs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module ObjectForge
|
|
2
|
-
type mold =
|
|
2
|
+
type mold = Object & ObjectForge::_Mold
|
|
3
3
|
type sequenceable = ObjectForge::_RespondTo & ObjectForge::_Sequenceable
|
|
4
4
|
|
|
5
5
|
interface _RespondTo
|
|
@@ -15,13 +15,19 @@ module ObjectForge
|
|
|
15
15
|
interface _ForgeParameters
|
|
16
16
|
def attributes: () -> Hash[Symbol, untyped]
|
|
17
17
|
def traits: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
18
|
-
def
|
|
18
|
+
def settings: () -> Hash[Symbol, untyped]
|
|
19
|
+
end
|
|
20
|
+
interface _Mold
|
|
21
|
+
def call
|
|
22
|
+
: (forged: untyped, attributes: Hash[Symbol, untyped], **untyped) -> untyped
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
class Error < StandardError
|
|
22
26
|
end
|
|
23
27
|
class DSLError < Error
|
|
24
28
|
end
|
|
29
|
+
class MoldError < Error
|
|
30
|
+
end
|
|
25
31
|
|
|
26
32
|
VERSION: String
|
|
27
33
|
DEFAULT_YARD: ObjectForge::Forgeyard
|
|
@@ -35,6 +41,10 @@ module ObjectForge
|
|
|
35
41
|
|
|
36
42
|
def self.forge
|
|
37
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
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
class ObjectForge::Sequence
|
|
@@ -63,11 +73,14 @@ class ObjectForge::Forgeyard
|
|
|
63
73
|
|
|
64
74
|
def register
|
|
65
75
|
: (Symbol name, ObjectForge::Forge forge) -> ObjectForge::Forge
|
|
76
|
+
|
|
77
|
+
def []
|
|
78
|
+
: (Symbol name) -> ObjectForge::Forge
|
|
66
79
|
|
|
67
80
|
def forge
|
|
68
81
|
: (Symbol name, *Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
69
82
|
alias build forge
|
|
70
|
-
alias
|
|
83
|
+
alias call forge
|
|
71
84
|
end
|
|
72
85
|
|
|
73
86
|
class ObjectForge::Forge
|
|
@@ -78,8 +91,6 @@ class ObjectForge::Forge
|
|
|
78
91
|
: (attributes: Hash[Symbol, untyped], traits: Hash[Symbol, Hash[Symbol, untyped]]) -> void
|
|
79
92
|
end
|
|
80
93
|
|
|
81
|
-
MOLD_MOLD: ObjectForge::Molds::MoldMold
|
|
82
|
-
|
|
83
94
|
attr_reader forged: ObjectForge::_Forgable
|
|
84
95
|
attr_reader name: Symbol
|
|
85
96
|
|
|
@@ -93,10 +104,13 @@ class ObjectForge::Forge
|
|
|
93
104
|
def forge
|
|
94
105
|
: (*Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
95
106
|
alias build forge
|
|
96
|
-
alias
|
|
107
|
+
alias call forge
|
|
97
108
|
|
|
98
109
|
private
|
|
99
110
|
|
|
111
|
+
def determine_mold
|
|
112
|
+
: (ObjectForge::_Forgable forged, ObjectForge::mold mold) -> ObjectForge::mold
|
|
113
|
+
|
|
100
114
|
def resolve_attributes
|
|
101
115
|
: (Array[Symbol] traits, Hash[Symbol, untyped] overrides) -> Hash[Symbol, untyped]
|
|
102
116
|
end
|
|
@@ -109,6 +123,7 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
109
123
|
@attributes: Hash[Symbol, Proc]
|
|
110
124
|
@sequences: Hash[Symbol, ObjectForge::Sequence]
|
|
111
125
|
@traits: Hash[Symbol, Hash[Symbol, Proc]]
|
|
126
|
+
@settings: Hash[Symbol, untyped]
|
|
112
127
|
|
|
113
128
|
def initialize
|
|
114
129
|
: () { (ObjectForge::ForgeDSL) -> void } -> void
|
|
@@ -116,8 +131,8 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
116
131
|
|
|
117
132
|
def freeze: -> self
|
|
118
133
|
|
|
119
|
-
def
|
|
120
|
-
: (
|
|
134
|
+
def setting
|
|
135
|
+
: (Symbol name, untyped value) -> Symbol
|
|
121
136
|
|
|
122
137
|
def attribute
|
|
123
138
|
: (Symbol name) { [self: ObjectForge::Crucible] -> untyped } -> Symbol
|
|
@@ -140,8 +155,9 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
140
155
|
|
|
141
156
|
def respond_to_missing?
|
|
142
157
|
: (Symbol name, bool include_all) -> bool
|
|
143
|
-
|
|
144
|
-
def
|
|
158
|
+
|
|
159
|
+
def valid_setting_method?
|
|
160
|
+
: (Symbol name) -> bool
|
|
145
161
|
end
|
|
146
162
|
|
|
147
163
|
class ObjectForge::Crucible < ObjectForge::UnBasicObject
|
|
@@ -178,8 +194,9 @@ class ObjectForge::UnBasicObject < BasicObject
|
|
|
178
194
|
def inspect: -> String
|
|
179
195
|
|
|
180
196
|
def is_a?: (Module klass) -> bool
|
|
197
|
+
def kind_of?: (Module klass) -> bool
|
|
181
198
|
|
|
182
|
-
def respond_to?: (Symbol name, ?bool
|
|
199
|
+
def respond_to?: (Symbol name, ?bool include_all) -> bool
|
|
183
200
|
|
|
184
201
|
def to_s: -> String
|
|
185
202
|
|
|
@@ -192,4 +209,6 @@ class ObjectForge::UnBasicObject < BasicObject
|
|
|
192
209
|
def block_given?: -> bool
|
|
193
210
|
|
|
194
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
|
|
195
214
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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
|
-
-
|
|
8
|
-
autorequire:
|
|
7
|
+
- Alexander Bulancov
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: concurrent-ruby
|
|
@@ -31,11 +30,12 @@ description: |
|
|
|
31
30
|
To use, just define some factories and call them wherever you need,
|
|
32
31
|
be it in tests, console, or application code.
|
|
33
32
|
If needed, almost any part of the process can be easily replaced with a custom solution.
|
|
34
|
-
email:
|
|
35
33
|
executables: []
|
|
36
34
|
extensions: []
|
|
37
|
-
extra_rdoc_files:
|
|
35
|
+
extra_rdoc_files:
|
|
36
|
+
- README.md
|
|
38
37
|
files:
|
|
38
|
+
- README.md
|
|
39
39
|
- lib/object_forge.rb
|
|
40
40
|
- lib/object_forge/crucible.rb
|
|
41
41
|
- lib/object_forge/forge.rb
|
|
@@ -44,7 +44,6 @@ files:
|
|
|
44
44
|
- lib/object_forge/molds.rb
|
|
45
45
|
- lib/object_forge/molds/hash_mold.rb
|
|
46
46
|
- lib/object_forge/molds/keywords_mold.rb
|
|
47
|
-
- lib/object_forge/molds/mold_mold.rb
|
|
48
47
|
- lib/object_forge/molds/single_argument_mold.rb
|
|
49
48
|
- lib/object_forge/molds/struct_mold.rb
|
|
50
49
|
- lib/object_forge/molds/wrapped_mold.rb
|
|
@@ -59,14 +58,15 @@ licenses:
|
|
|
59
58
|
metadata:
|
|
60
59
|
homepage_uri: https://github.com/trinistr/object_forge
|
|
61
60
|
bug_tracker_uri: https://github.com/trinistr/object_forge/issues
|
|
62
|
-
documentation_uri: https://rubydoc.info/gems/object_forge/0.
|
|
63
|
-
source_code_uri: https://github.com/trinistr/object_forge/tree/v0.
|
|
64
|
-
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
|
|
65
64
|
rubygems_mfa_required: 'true'
|
|
66
|
-
post_install_message:
|
|
67
65
|
rdoc_options:
|
|
68
66
|
- "--tag"
|
|
69
67
|
- thread_safety:Thread safety
|
|
68
|
+
- "--main"
|
|
69
|
+
- README.md
|
|
70
70
|
require_paths:
|
|
71
71
|
- lib
|
|
72
72
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
@@ -80,8 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0'
|
|
82
82
|
requirements: []
|
|
83
|
-
rubygems_version: 3.
|
|
84
|
-
signing_key:
|
|
83
|
+
rubygems_version: 3.6.9
|
|
85
84
|
specification_version: 4
|
|
86
85
|
summary: A simple factory for objects with minimal assumptions.
|
|
87
86
|
test_files: []
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ObjectForge
|
|
4
|
-
module Molds
|
|
5
|
-
# Special "mold" that returns appropriate mold for the given forged object.
|
|
6
|
-
# Probably not the best fit though.
|
|
7
|
-
#
|
|
8
|
-
# Currently provides specific recognition for:
|
|
9
|
-
# - subclasses of +Struct+ ({StructMold}),
|
|
10
|
-
# - subclasses of +Data+ ({KeywordsMold}),
|
|
11
|
-
# - +Hash+ and subclasses ({HashMold}).
|
|
12
|
-
# Other objects just get {SingleArgumentMold}.
|
|
13
|
-
#
|
|
14
|
-
# @thread_safety Thread-safe.
|
|
15
|
-
# @since 0.2.0
|
|
16
|
-
class MoldMold
|
|
17
|
-
# Get maybe appropriate mold for the given forged object.
|
|
18
|
-
#
|
|
19
|
-
# @param forged [Class, Any]
|
|
20
|
-
# @return [#call] an instance of a mold
|
|
21
|
-
def call(forged:, **_)
|
|
22
|
-
# rubocop:disable Style/YodaCondition
|
|
23
|
-
if ::Class === forged
|
|
24
|
-
if ::Struct > forged
|
|
25
|
-
StructMold.new
|
|
26
|
-
elsif defined?(::Data) && ::Data > forged
|
|
27
|
-
KeywordsMold.new
|
|
28
|
-
elsif ::Hash >= forged
|
|
29
|
-
HashMold.new
|
|
30
|
-
else
|
|
31
|
-
SingleArgumentMold.new
|
|
32
|
-
end
|
|
33
|
-
else
|
|
34
|
-
SingleArgumentMold.new
|
|
35
|
-
end
|
|
36
|
-
# rubocop:enable Style/YodaCondition
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|