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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05dce89141257f37f8b8af6a7f0cdfb1d7ee785a37ad119d0c13ff28c58d9245
4
- data.tar.gz: e171f2f3d504d0edadfdb5f2ba2f87d9d9edd1a0f175a0be366b12dc528182a0
3
+ metadata.gz: bd0b538c878ddc72f9041187f1612a1d779b55d7d7a4a72bb8be9c2c22d36073
4
+ data.tar.gz: 0cd2ec6ea0594f13f1780eb503220d5313489657d6aa31e771d075d1ceef4c9b
5
5
  SHA512:
6
- metadata.gz: c8a330629ce325a8bf78930a3025d5c1d75305e80c970cb5217e9d4ae6f93659d3fc9d3fdd89884920b5843c8c1bfb787bdfc6be8ce220db29124a2d73a749da
7
- data.tar.gz: a0515b9f4111c0e3bcd1b9bbb4f5cb3011bb12bba62750a2ca0f49b702f55a459be7877fb540cf16c6a40503a5c2294e962352408345e67bbb7179bc79f2539b
6
+ metadata.gz: 63431e04ef6c32990268ba7545564c964a72b72b23b9469315479d696c42a4106b24dd490a80481182f04b41e91cc78614f1de177f3516bb66d965c0f71b7df9
7
+ data.tar.gz: 1ed2a7e317671bb416bad5960d2bb227332ccf911d936835802470f785d98975223f3bf2070ebd7ca212b84932e773d1844bf64a718d857f8fc514c3cf2516e5
data/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # ObjectForge
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/object_forge.svg?icon=si%3Arubygems)](https://rubygems.org/gems/object_forge)
4
+ [![CI](https://github.com/trinistr/object_forge/actions/workflows/CI.yaml/badge.svg)](https://github.com/trinistr/object_forge/actions/workflows/CI.yaml)
5
+
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, ::Object.instance_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)
@@ -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
- Parameters = Struct.new(:attributes, :traits, keyword_init: true)
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
- # +traits+ and +overrides+ are thread-safe.
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 = build_instance(resolved_attributes)
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 [] forge
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
- ::Object.instance_method(:freeze).bind_call(self)
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 to define attributes with reserved names.
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 {#[]} instead.
269
+ # When in doubt, use {#attribute} or {#setting} instead.
235
270
  #
236
271
  # Reserved names are:
237
- # - all names ending in +?+, +!+ or +=+
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?("?", "!", "=") && !name.match?(/\A(?=\p{ASCII})\P{Word}/) && name != :rand
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, *traits, **overrides, &)
59
- @forges.fetch(name)[*traits, **overrides, &]
70
+ def forge(name, ...)
71
+ @forges.fetch(name).call(...)
60
72
  end
61
73
 
62
74
  alias build forge
63
- alias [] forge
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
- # module ObjectForge
4
- # module Molds
5
- # # Mold which calls +forged.new(**attributes)+.
6
- # #
7
- # # @since 0.1.1
8
- # class KeywordsMold
9
- # # Instantiate +forged+ with a hash of attributes.
10
- # #
11
- # # @param forged [Class]
12
- # # @param attributes [Hash{Symbol => Any}]
13
- # # @return [Any]
14
- # def call(forged:, attributes:, **)
15
- # forged.new(**attributes)
16
- # end
17
- # end
18
- # end
19
- # end
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, ::Object.instance_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 { |m| private define_method(m, ::Object.instance_method(m)) }
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).
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ObjectForge
4
4
  # Current version
5
- VERSION = "0.1.1"
5
+ VERSION = "0.3.0"
6
6
  end
data/lib/object_forge.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir["#{__dir__}/object_forge/**/*.rb"].each { require _1 }
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[:frobber, :static, inator: "Value"]
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
- # @since 0.1.0
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 [] forge
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 [] forge
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 rand: [T] (?(Float | Integer | Range[T])) -> (Float | Integer | T)
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 include_private) -> 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.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - Alexandr Bulancov
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.1.1
54
- source_code_uri: https://github.com/trinistr/object_forge/tree/v0.1.1
55
- changelog_uri: https://github.com/trinistr/object_forge/blob/v0.1.1/CHANGELOG.md
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.7.1
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: []