object_forge 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13df90e0eb14f3fb52c1b9be5706a3b2da6c844369005f0500fe31f67ced6091
4
+ data.tar.gz: 6ce301c70ae87b0e46662482cd46af63d8e282aca65f92e220dc02e4a6b3e2b6
5
+ SHA512:
6
+ metadata.gz: b75f0d0ddacd4f102a02bbb74bd0189e8b2759eb4e9a4f3f62de84a028356800908d3313ec5361299078e49a2101abcedda91358601cfc03cf76ac2bb3fb05b7
7
+ data.tar.gz: 76231dd798153526c679874b3528d120a7f3562c8dfc540001be54465b46f53a50afa46f04d17f8bc2ddfcb152708d9003e417fd4d8c6d220890c62597930104
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ require_relative "un_basic_object"
6
+
7
+ module ObjectForge
8
+ # Melting pot for the forged object's attributes.
9
+ #
10
+ # @note This class is not intended to be used directly,
11
+ # but it's not a private API.
12
+ #
13
+ # @thread_safety Attribute resolution is idempotent,
14
+ # but modifies instance variables, making it unsafe to share the Crucible
15
+ #
16
+ # @since 0.1.0
17
+ class Crucible < UnBasicObject
18
+ %i[rand].each { |m| private define_method(m, ::Object.instance_method(m)) }
19
+
20
+ # @param attributes [Hash{Symbol => Proc, Any}] initial attributes
21
+ def initialize(attributes)
22
+ super()
23
+ @attributes = attributes
24
+ @resolved_attributes = ::Set.new
25
+ end
26
+
27
+ # Resolve all attributes by calling their +Proc+s,
28
+ # using +self+ as the evaluation context.
29
+ #
30
+ # Attributes can freely refer to each other inside +Proc+s
31
+ # through bareword names or +#[]+.
32
+ # However, make sure to avoid cyclic dependencies:
33
+ # they aren't specially detected or handled, and will cause +SystemStackError+.
34
+ #
35
+ # @note This method destructively modifies initial attributes.
36
+ #
37
+ # @return [Hash{Symbol => Any}] resolved attributes
38
+ def resolve!
39
+ @attributes.each_key { |name| method_missing(name) }
40
+ @attributes
41
+ end
42
+
43
+ private
44
+
45
+ # Get the value of the attribute +name+.
46
+ #
47
+ # To prevent problems with calling methods which may be defined,
48
+ # +#[]+ can be used instead.
49
+ #
50
+ # @example
51
+ # attrs = {
52
+ # name: -> { "Name" },
53
+ # description: -> { name.downcase },
54
+ # duration: -> { rand(1000) }
55
+ # }
56
+ # Crucible.new(attrs).resolve!
57
+ # # => { name: "Name", description: "name", duration: 123 }
58
+ # @example using conflicting and reserved names
59
+ # attrs = {
60
+ # "[]": -> { "Brackets" },
61
+ # "[]=": -> { "#{self[:[]]} are brackets" },
62
+ # "!": -> { "#{self[:[]=]}!" }
63
+ # }
64
+ # Crucible.new(attrs).resolve!
65
+ # # => { "[]": "Brackets", "[]=": "Brackets are brackets", "!": "Brackets are brackets!" }
66
+ #
67
+ # @param name [Symbol]
68
+ # @return [Any]
69
+ def method_missing(name)
70
+ if @attributes.key?(name)
71
+ if @resolved_attributes.include?(name) || !(::Proc === @attributes[name])
72
+ @attributes[name]
73
+ else
74
+ @resolved_attributes << name
75
+ @attributes[name] = instance_exec(&@attributes[name])
76
+ end
77
+ else
78
+ super
79
+ end
80
+ end
81
+
82
+ alias [] method_missing
83
+
84
+ def respond_to_missing?(name, _include_all)
85
+ @attributes.key?(name)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "crucible"
4
+ require_relative "forge_dsl"
5
+
6
+ module ObjectForge
7
+ # Object instantitation forge.
8
+ #
9
+ # @since 0.1.0
10
+ class Forge
11
+ # Interface for forge parameters.
12
+ # It is not used internally, but can be useful for defining forges
13
+ # through means other than {ForgeDSL}.
14
+ #
15
+ # @!attribute [r] attributes
16
+ # Non-trait values of the attributes.
17
+ # @return [Hash{Symbol => Any}]
18
+ #
19
+ # @!attribute [r] traits
20
+ # Attributes belonging to traits.
21
+ # @return [Hash{Symbol => Hash{Symbol => Any}}]
22
+ Parameters = Struct.new(:attributes, :traits, keyword_init: true)
23
+
24
+ # Define (and create) a forge using DSL.
25
+ #
26
+ # @see ForgeDSL
27
+ # @thread_safety Thread-safe if DSL definition is thread-safe.
28
+ #
29
+ # @param forged [Class] class to forge
30
+ # @param name [Symbol, nil] forge name
31
+ # @yieldparam f [ForgeDSL]
32
+ # @yieldreturn [void]
33
+ # @return [Forge] forge
34
+ def self.define(forged, name: nil, &)
35
+ new(forged, ForgeDSL.new(&), name:)
36
+ end
37
+
38
+ # @return [Symbol, nil] forge name
39
+ attr_reader :name
40
+
41
+ # @return [Class] class to forge
42
+ attr_reader :forged
43
+
44
+ # @return [Parameters, ForgeDSL] forge parameters
45
+ attr_reader :parameters
46
+
47
+ # @param forged [Class] class to forge
48
+ # @param parameters [Parameters, ForgeDSL] forge parameters
49
+ # @param name [Symbol, nil] forge name
50
+ def initialize(forged, parameters, name: nil)
51
+ @name = name
52
+ @forged = forged
53
+ @parameters = parameters
54
+ end
55
+
56
+ # Forge a new instance.
57
+ #
58
+ # @overload forge(*traits, **overrides)
59
+ # @overload forge(traits, overrides)
60
+ #
61
+ # Positional arguments are taken as trait names, keyword arguments as attribute overrides,
62
+ # unless there are exactly two positional arguments: an array and a hash.
63
+ #
64
+ # All traits and overrides are applied in argument order,
65
+ # with overrides always applied after traits.
66
+ #
67
+ # @thread_safety Forging is thread-safe if {#parameters},
68
+ # +traits+ and +overrides+ are thread-safe.
69
+ #
70
+ # @param traits [Array<Symbol>] traits to apply
71
+ # @param overrides [Hash{Symbol => Any}] attribute overrides
72
+ # @return [Any] built instance
73
+ def forge(*traits, **overrides)
74
+ # @type var traits: Array[(Array[Symbol] | Hash[Symbol, untyped])]
75
+ traits, overrides = check_traits_and_overrides(traits, overrides)
76
+ attributes = @parameters.attributes.merge(*@parameters.traits.values_at(*traits), overrides)
77
+ attributes = Crucible.new(attributes).resolve!
78
+
79
+ forged.new(attributes)
80
+ end
81
+
82
+ alias build forge
83
+ alias [] forge
84
+
85
+ private
86
+
87
+ def check_traits_and_overrides(traits, overrides)
88
+ unless traits.size == 2 && overrides.empty?
89
+ # @type var traits: Array[Symbol]
90
+ # @type var overrides: Hash[Symbol, untyped]
91
+ return [traits, overrides]
92
+ end
93
+
94
+ case traits
95
+ in [Array => real_traits, Hash => real_overrides]
96
+ [real_traits, real_overrides]
97
+ else
98
+ [traits, overrides]
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sequence"
4
+ require_relative "un_basic_object"
5
+
6
+ module ObjectForge
7
+ # DSL for defining a forge.
8
+ #
9
+ # @note This class is not intended to be used directly,
10
+ # but it's not a private API.
11
+ #
12
+ # @thread_safety DSL is not thread-safe.
13
+ # Take care not to introduce side effects,
14
+ # especially in attribute definitions.
15
+ # The instance itself is frozen after initialization,
16
+ # so it should be safe to share.
17
+ #
18
+ # @since 0.1.0
19
+ class ForgeDSL < UnBasicObject
20
+ # @return [Hash{Symbol => Proc}] attribute definitions
21
+ attr_reader :attributes
22
+
23
+ # @return [Hash{Symbol => Sequence}] used sequences
24
+ attr_reader :sequences
25
+
26
+ # @return [Hash{Symbol => Hash{Symbol => Proc}}] trait definitions
27
+ attr_reader :traits
28
+
29
+ # Define forge's parameters through DSL.
30
+ #
31
+ # If the block has a parameter, an object will be yielded,
32
+ # and +self+ context will be preserved.
33
+ # Otherwise, DSL will change +self+ context inside the block,
34
+ # without ability to call methods available outside.
35
+ #
36
+ # @example with block parameter
37
+ # ForgeDSL.new do |f|
38
+ # f.attribute(:name) { "Name" }
39
+ # f[:description] { name.upcase }
40
+ # f.duration { rand(1000) }
41
+ # end
42
+ #
43
+ # @example without block parameter
44
+ # ForgeDSL.new do
45
+ # attribute(:name) { "Name" }
46
+ # self[:description] { name.upcase }
47
+ # duration { rand(1000) }
48
+ # end
49
+ #
50
+ # @yieldparam f [ForgeDSL] self
51
+ # @yieldreturn [void]
52
+ def initialize(&dsl)
53
+ super
54
+ @attributes = {}
55
+ @sequences = {}
56
+ @traits = {}
57
+
58
+ dsl.arity.zero? ? instance_exec(&dsl) : yield(self)
59
+
60
+ freeze
61
+ end
62
+
63
+ # Freezes the instance, including +attributes+, +sequences+ and +traits+.
64
+ # Prevents further responses through +#method_missing+.
65
+ #
66
+ # @note Called automatically in {#initialize}.
67
+ #
68
+ # @return [self]
69
+ def freeze
70
+ ::Object.instance_method(:freeze).bind_call(self)
71
+ @attributes.freeze
72
+ @sequences.freeze
73
+ @traits.freeze
74
+ self
75
+ end
76
+
77
+ # Define an attribute, possibly transient.
78
+ #
79
+ # DSL does not know or care what attributes the forged class has,
80
+ # so the only difference between "real" and "transient" attributes
81
+ # is how the class itself treats them.
82
+ #
83
+ # It is also possible to define attributes using +method_missing+ shortcut,
84
+ # except for conflicting or reserved names.
85
+ #
86
+ # You can refer to any other attribute inside the attribute definition block.
87
+ # +self[:name]+ can be used to refer to an attribute with a conflicting or reserved name.
88
+ #
89
+ # @example
90
+ # f.attribute(:name) { "Name" }
91
+ # f[:description] { name.downcase }
92
+ # f.duration { rand(1000) }
93
+ # @example using conflicting and reserved names
94
+ # f.attribute(:[]) { "Brackets" }
95
+ # f.attribute(:[]=) { "#{self[:[]]} are brackets" }
96
+ # f.attribute(:!) { "#{self[:[]=]}!" }
97
+ #
98
+ # @param name [Symbol] attribute name
99
+ # @yieldreturn [Any] attribute value
100
+ # @return [Symbol] attribute name
101
+ #
102
+ # @raise [ArgumentError] if +name+ is not a Symbol
103
+ # @raise [DSLError] if no block is given
104
+ def attribute(name, &definition)
105
+ unless ::Symbol === name
106
+ raise ::ArgumentError,
107
+ "attribute name must be a Symbol, #{name.class} given (in #{name.inspect})"
108
+ end
109
+ unless block_given?
110
+ raise DSLError, "attribute definition requires a block (in #{name.inspect})"
111
+ end
112
+
113
+ if @current_trait
114
+ @traits[@current_trait][name] = definition
115
+ else
116
+ @attributes[name] = definition
117
+ end
118
+
119
+ name
120
+ end
121
+
122
+ alias [] attribute
123
+
124
+ # Define an attribute, using a sequence.
125
+ #
126
+ # +name+ is used for both attribute and sequence, for the whole forge.
127
+ # If the name was used for a sequence previously,
128
+ # the sequence will not be redefined on subsequent calls.
129
+ #
130
+ # @example
131
+ # f.sequence(:date, Date.today)
132
+ # f.sequence(:id) { _1.to_s }
133
+ # f.sequence(:dated_id, 10) { |n| "#{Date.today}/#{n}-#{id}" }
134
+ # @example using external sequence
135
+ # seq = Sequence.new(1)
136
+ # f.sequence(:global_id, seq)
137
+ # @example sequence reuse
138
+ # f.sequence(:id, "a") # => "a", "b", ...
139
+ # f.trait :new_id do
140
+ # f.sequence(:id) { |n| n * 2 } # => "aa", "bb", ...
141
+ # end
142
+ #
143
+ # @param name [Symbol] attribute name
144
+ # @param initial [Sequence, #succ] existing sequence, or initial value for a new sequence
145
+ # @yieldparam value [#succ] current value of the sequence to calculate attribute value
146
+ # @yieldreturn [Any] attribute value
147
+ # @return [Symbol] attribute name
148
+ #
149
+ # @raise [ArgumentError] if +name+ is not a Symbol
150
+ # @raise [DSLError] if +initial+ does not respond to #succ and is not a {Sequence}
151
+ def sequence(name, initial = 1, **nil, &)
152
+ unless ::Symbol === name
153
+ raise ::ArgumentError,
154
+ "sequence name must be a Symbol, #{name.class} given (in #{name.inspect})"
155
+ end
156
+
157
+ seq = @sequences[name] ||= Sequence.new(initial)
158
+
159
+ if block_given?
160
+ attribute(name) { instance_exec(seq.next, &) }
161
+ else
162
+ attribute(name) { seq.next }
163
+ end
164
+
165
+ name
166
+ end
167
+
168
+ # Define a trait — a group of attributes with non-default values.
169
+ #
170
+ # DSL yields itself to the block, in case you need to refer to it.
171
+ # This can be used to define traits using a block coming from outside of DSL.
172
+ #
173
+ # @example
174
+ # f.trait :special do
175
+ # f.name { "***xXxSPECIALxXx***" }
176
+ # f.sequence(:special_id) { "~~~ SpEcIaL #{_1} ~~~" }
177
+ # end
178
+ # @example externally defined trait
179
+ # # Variable defined outside of DSL:
180
+ # success_trait = ->(ft) do
181
+ # ft.status { :success }
182
+ # ft.error_code { 0 }
183
+ # end
184
+ # # Inside the DSL:
185
+ # f.trait(:success, &success_trait)
186
+ #
187
+ # @note Traits can not be defined inside of traits.
188
+ #
189
+ # @param name [Symbol] trait name
190
+ # @yield block for trait definition
191
+ # @yieldparam f [ForgeDSL] self
192
+ # @yieldreturn [void]
193
+ # @return [Symbol] trait name
194
+ #
195
+ # @raise [ArgumentError] if +name+ is not a Symbol
196
+ # @raise [DSLError] if no block is given
197
+ # @raise [DSLError] if called inside of another trait definition
198
+ def trait(name, **nil)
199
+ unless ::Symbol === name
200
+ raise ::ArgumentError,
201
+ "trait name must be a Symbol, #{name.class} given (in #{name.inspect})"
202
+ end
203
+ if @current_trait
204
+ raise DSLError, "can not define trait inside of another trait (in #{name.inspect})"
205
+ end
206
+ raise DSLError, "trait definition requires a block (in #{name.inspect})" unless block_given?
207
+
208
+ @current_trait = name
209
+ @traits[name] = {}
210
+ yield self
211
+ @traits[name].freeze
212
+ @current_trait = nil
213
+
214
+ name
215
+ end
216
+
217
+ # Return a string containing a human-readable representation of the definition.
218
+ #
219
+ # @return [String]
220
+ def inspect
221
+ "#<#{self.class.name}:#{__id__} " \
222
+ "attributes=#{@attributes.keys.inspect} " \
223
+ "sequences=#{@sequences.keys.inspect} " \
224
+ "traits={#{@traits.map { |k, v| "#{k.inspect}=#{v.keys.inspect}" }.join(", ")}}>"
225
+ end
226
+
227
+ private
228
+
229
+ # Define an attribute using a shorthand.
230
+ #
231
+ # Can not be used to define attributes with reserved names.
232
+ # Trying to use a conflicting name will lead to usual issues
233
+ # with calling random methods.
234
+ # When in doubt, use {#attribute} or {#[]} instead.
235
+ #
236
+ # Reserved names are:
237
+ # - all names ending in +?+, +!+ or +=+
238
+ # - all names starting with a non-word ASCII character
239
+ # (operators, +`+, +[]+, +[]=+)
240
+ # - +rand+
241
+ #
242
+ # @param name [Symbol] attribute name
243
+ # @yieldreturn [Any] attribute value
244
+ # @return [Symbol] attribute name
245
+ #
246
+ # @raise [DSLError] if a reserved +name+ is used
247
+ def method_missing(name, **nil, &)
248
+ return super if frozen?
249
+ return attribute(name, &) if respond_to_missing?(name, false)
250
+
251
+ raise DSLError, "#{name.inspect} is a reserved name (in #{name.inspect})"
252
+ end
253
+
254
+ def respond_to_missing?(name, _include_all)
255
+ return false if frozen?
256
+
257
+ !name.end_with?("?", "!", "=") && !name.match?(/\A(?=\p{ASCII})\P{Word}/) && name != :rand
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ module ObjectForge
6
+ # A registry for forges, making them accessible by name.
7
+ #
8
+ # @since 0.1.0
9
+ class Forgeyard
10
+ # @return [Concurrent::Map{Symbol => Forge}] registered forges
11
+ attr_reader :forges
12
+
13
+ def initialize
14
+ @forges = Concurrent::Map.new
15
+ end
16
+
17
+ # Define and register a forge in one go.
18
+ #
19
+ # @see #register
20
+ # @see Forge.define
21
+ #
22
+ # @param name [Symbol] name to register forge under
23
+ # @param forged [Class] class to forge
24
+ # @yieldparam f [ForgeDSL]
25
+ # @yieldreturn [void]
26
+ # @return [Forge] forge
27
+ def define(name, forged, &)
28
+ register(name, Forge.define(forged, name: name, &))
29
+ end
30
+
31
+ # Add a forge under a specified name.
32
+ #
33
+ # If +name+ was already taken, new +forge+ will be ignored
34
+ # and existing forge will be returned.
35
+ #
36
+ # @thread_safety Registration is thread-safe, i.e. first one always wins.
37
+ #
38
+ # @param name [Symbol] name to register forge under
39
+ # @param forge [Forge] forge to register
40
+ # @return [Forge] actually registered forge
41
+ def register(name, forge)
42
+ # `put_if_absent` returns `nil` if there was no previous value, hence the `||`.
43
+ @forges.put_if_absent(name, forge) || forge
44
+ end
45
+
46
+ # Build an instance using a forge.
47
+ #
48
+ # @see Forge#forge
49
+ #
50
+ # @param name [Symbol] name of the forge
51
+ # @param traits [Array<Symbol>] traits to apply
52
+ # @param overrides [Hash{Symbol => Any}] attribute overrides
53
+ # @return [Any] built instance
54
+ #
55
+ # @raise [KeyError] if forge with the specified name is not registered
56
+ def forge(name, *traits, **overrides)
57
+ @forges.fetch(name)[traits, overrides]
58
+ end
59
+
60
+ alias build forge
61
+ alias [] forge
62
+ end
63
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/mvar"
4
+
5
+ module ObjectForge
6
+ # A thread-safe representation of a sequence of values.
7
+ #
8
+ # @since 0.1.0
9
+ class Sequence
10
+ # Return a new sequence, or +initial+ if it's already a sequence.
11
+ #
12
+ # @param initial [#succ, Sequence]
13
+ # @return [Sequence]
14
+ def self.new(initial, ...)
15
+ return initial if initial.is_a?(Sequence)
16
+
17
+ super
18
+ end
19
+
20
+ # @return [#succ] initial value for the sequence
21
+ attr_reader :initial
22
+
23
+ # @note Initial value must not be modified after the sequence is created,
24
+ # or the results will be unpredicatable. Consider always passing a frozen value.
25
+ #
26
+ # @param initial [#succ] initial value for the sequence
27
+ #
28
+ # @raise [ArgumentError] if +initial+ does not respond to #succ
29
+ def initialize(initial)
30
+ unless initial.respond_to?(:succ)
31
+ raise ArgumentError, "initial value must respond to #succ, #{initial.class} given"
32
+ end
33
+
34
+ @initial = initial
35
+ @container = Concurrent::MVar.new(initial)
36
+ end
37
+
38
+ # Get the next value in the sequence, starting with the initial value.
39
+ #
40
+ # @thread_safety Sequence traversal is synchronized,
41
+ # so no duplicate values will be returned.
42
+ #
43
+ # @return [#succ] next value
44
+ def next
45
+ @container.modify(&:succ)
46
+ end
47
+
48
+ # Reset the sequence to its {#initial} value.
49
+ #
50
+ # @thread_safety Reset is synchronized with {#next}.
51
+ #
52
+ # @return [#succ] whatever value would be returned by {#next} before reset
53
+ def reset
54
+ @container.modify { @initial }
55
+ end
56
+
57
+ alias rewind reset
58
+ end
59
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectForge
4
+ # BasicObject with a few common methods copied from Object.
5
+ #
6
+ # @api private
7
+ #
8
+ # @since 0.1.0
9
+ class UnBasicObject < ::BasicObject
10
+ # @!group Instance methods copied from Object
11
+ # @!method class
12
+ # @see Kernel#class
13
+ # @return [Class]
14
+ # @!method eql?(other)
15
+ # @see Object#eql?
16
+ # @return [Boolean]
17
+ # @!method freeze
18
+ # @see Kernel#freeze
19
+ # @return [self]
20
+ # @!method frozen?
21
+ # @see Kernel#frozen?
22
+ # @return [Boolean]
23
+ # @!method hash
24
+ # @see Object#hash
25
+ # @return [Integer]
26
+ # @!method inspect
27
+ # @see Object#inspect
28
+ # @return [String]
29
+ # @!method is_a?(class)
30
+ # @see Kernel#is_a?
31
+ # @return [Boolean]
32
+ # @!method respond_to?(symbol [, include_private])
33
+ # @see Object#respond_to?
34
+ # @return [Boolean]
35
+ # @!method to_s
36
+ # @see Object#to_s
37
+ # @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))
40
+ end
41
+ alias kind_of? is_a?
42
+ # @!endgroup
43
+
44
+ %i[block_given? raise].each { |m| private define_method(m, ::Object.instance_method(m)) }
45
+
46
+ # @!macro pp_support
47
+ # Support for +pp+ (and IRB).
48
+ #
49
+ # @note This method dynamically calls UnboundMethod#bind_call, making it fairly slow.
50
+ #
51
+ # @api public
52
+ def pretty_print(...)
53
+ # We have to do it this way, instead of defining methods,
54
+ # because Object#pretty_print does not exist without requiring "pp".
55
+ ::Object.instance_method(:pretty_print).bind_call(self, ...)
56
+ end
57
+
58
+ # @!macro pp_support
59
+ def pretty_print_cycle(...)
60
+ # See comment for #pretty_print.
61
+ # :nocov:
62
+ ::Object.instance_method(:pretty_print_cycle).bind_call(self, ...)
63
+ # :nocov:
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectForge
4
+ # Current version
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir["#{__dir__}/object_forge/**/*.rb"].each { require _1 }
4
+
5
+ # A simple all-purpose factory library with minimal assumptions.
6
+ #
7
+ # These are the main classes you should be aware of:
8
+ # - {Forgeyard} is a registry of named related Forges.
9
+ # A Forgeyard allows to {Forgeyard#define} a Forge,
10
+ # and {Forgeyard#forge} a new object using a defined Forge.
11
+ # - {Forge} is a factory for objects.
12
+ # Usually created through {Forgeyard#define}/{Forge.define} in a manner similar to FactoryBot,
13
+ # Forges can be used standalone, or as a part of a Forgeyard.
14
+ # - {Sequence} is a representation of a sequence of values.
15
+ # They are usually used implicitly through {ForgeDSL#sequence},
16
+ # but can be created explicitly to be shared (or used outside of ObjectForge).
17
+ #
18
+ # Additionally, successful use may depend on understanding these:
19
+ # - {ForgeDSL} is a block-based DSL inspired by FactoryBot and ROM::Factory.
20
+ # It allows defining arbitrary attributes (possibly using sequences),
21
+ # with support for traits (collections of attributes with non-default values).
22
+ # - {Crucible} is used to resolve attributes.
23
+ #
24
+ # @example Quick example
25
+ # Frobinator = Struct.new(:frob, :inator, keyword_init: true)
26
+ # # Forge's name and forged class are completely independent.
27
+ # ObjectForge.define(:frobber, Frobinator) do |f|
28
+ # f.frob { "Frob" + inator.call }
29
+ # f.inator { -> { "inator" } }
30
+ # f.trait :static do |tf|
31
+ # tf.frob { "Static" }
32
+ # end
33
+ # end
34
+ # # These methods are aliases:
35
+ # ObjectForge.forge(:frobber)
36
+ # # => #<struct Frobinator frob="Frobinator", inator=#<Proc:...>>
37
+ # ObjectForge.build(:frobber, frob: -> { "Frob" + inator }, inator: "orn")
38
+ # # => #<struct Frobinator frob="Froborn", inator="orn">
39
+ # ObjectForge[:frobber, :static, inator: "Value"]
40
+ # # => #<struct Frobinator frob="Static", inator="Value">
41
+ module ObjectForge
42
+ # Base error class for ObjectForge.
43
+ # @since 0.1.0
44
+ class Error < StandardError; end
45
+ # Error raised when a mistake is made in using DSL.
46
+ # @since 0.1.0
47
+ class DSLError < Error; end
48
+
49
+ # Default {Forgeyard} that is used by {.define} and {.forge}.
50
+ #
51
+ # @!macro default_forgeyard
52
+ # @note
53
+ # Default forgeyard is intended to be useful for non-shareable code,
54
+ # like simple application tests and specs.
55
+ # It should not be used in application code, and never in gems.
56
+ # @since 0.1.0
57
+ DEFAULT_YARD = Forgeyard.new
58
+
59
+ # @overload sequence(initial)
60
+ # Create a sequence, to be used wherever it needs to be.
61
+ #
62
+ # @see Sequence.new
63
+ # @since 0.1.0
64
+ #
65
+ # @param initial [#succ, Sequence]
66
+ # @return [Sequence]
67
+ def self.sequence(...)
68
+ Sequence.new(...)
69
+ end
70
+
71
+ # @overload define(name, forged, &)
72
+ # Define and create a forge in {DEFAULT_YARD}.
73
+ #
74
+ # @!macro default_forgeyard
75
+ # @see Forgeyard#define
76
+ # @since 0.1.0
77
+ #
78
+ # @param name [Symbol] forge name
79
+ # @param forged [Class] class to forge
80
+ # @yieldparam f [ForgeDSL]
81
+ # @yieldreturn [void]
82
+ # @return [Forge] forge
83
+ def self.define(...)
84
+ DEFAULT_YARD.define(...)
85
+ end
86
+
87
+ # @overload forge(name, *traits, **overrides)
88
+ # Build an instance using a forge from {DEFAULT_YARD}.
89
+ #
90
+ # @!macro default_forgeyard
91
+ # @see Forgeyard#forge
92
+ # @since 0.1.0
93
+ #
94
+ # @param name [Symbol] name of the forge
95
+ # @param traits [Array<Symbol>] traits to apply
96
+ # @param overrides [Hash{Symbol => Any}] attribute overrides
97
+ # @return [Any] built instance
98
+ def self.forge(...)
99
+ DEFAULT_YARD.forge(...)
100
+ end
101
+
102
+ class << self
103
+ # @since 0.1.0
104
+ alias build forge
105
+ # @since 0.1.0
106
+ alias [] forge
107
+ end
108
+ end
@@ -0,0 +1,186 @@
1
+ module ObjectForge
2
+ class Error < StandardError
3
+ end
4
+ class DSLError < Error
5
+ end
6
+
7
+ interface _Sequenceable
8
+ def succ: -> self
9
+ def respond_to?: (Symbol name, ?bool include_private) -> bool
10
+ def class: -> Class
11
+ end
12
+ interface _Forgable
13
+ def new: (Hash[Symbol, untyped]) -> self
14
+ end
15
+ interface _ForgeParameters
16
+ def attributes: () -> Hash[Symbol, untyped]
17
+ def traits: () -> Hash[Symbol, Hash[Symbol, untyped]]
18
+ end
19
+
20
+ VERSION: String
21
+ DEFAULT_YARD: ObjectForge::Forgeyard
22
+
23
+ def self.sequence
24
+ : (?(ObjectForge::_Sequenceable | ObjectForge::Sequence) initial) -> ObjectForge::Sequence
25
+
26
+ def self.define
27
+ : (Symbol name, ObjectForge::_Forgable forged) { (ObjectForge::ForgeDSL) -> void } -> ObjectForge::Forge
28
+ | (Symbol name, ObjectForge::_Forgable forged) { [self: ObjectForge::ForgeDSL] -> void } -> ObjectForge::Forge
29
+
30
+ def self.forge
31
+ : (Symbol name, *Symbol traits, **untyped overrides) -> ObjectForge::_Forgable
32
+ end
33
+
34
+ class ObjectForge::Sequence
35
+ def self.new
36
+ : (?(ObjectForge::_Sequenceable | ObjectForge::Sequence) initial) -> ObjectForge::Sequence
37
+
38
+ attr_reader initial: ObjectForge::_Sequenceable
39
+
40
+ def initialize: (ObjectForge::_Sequenceable initial) -> void
41
+
42
+ def next: -> ObjectForge::_Sequenceable
43
+
44
+ def reset: -> ObjectForge::_Sequenceable
45
+ alias rewind reset
46
+ end
47
+
48
+ class ObjectForge::Forgeyard
49
+ attr_reader forges: Concurrent::Map[Symbol, ObjectForge::Forge]
50
+
51
+ def initialize
52
+ : () -> void
53
+
54
+ def define
55
+ : (Symbol name, ObjectForge::_Forgable forged) { (ObjectForge::ForgeDSL) -> void } -> ObjectForge::Forge
56
+ | (Symbol name, ObjectForge::_Forgable forged) { [self: ObjectForge::ForgeDSL] -> void } -> ObjectForge::Forge
57
+
58
+ def register
59
+ : (Symbol name, ObjectForge::Forge forge) -> ObjectForge::Forge
60
+
61
+ def forge
62
+ : (Symbol name, *Symbol traits, **untyped overrides) -> ObjectForge::_Forgable
63
+ alias build forge
64
+ alias [] forge
65
+ end
66
+
67
+ class ObjectForge::Forge
68
+ class Parameters
69
+ include ObjectForge::_ForgeParameters
70
+
71
+ def intitialize
72
+ : (attributes: Hash[Symbol, untyped], traits: Hash[Symbol, Hash[Symbol, untyped]]) -> void
73
+ end
74
+
75
+ attr_reader forged: ObjectForge::_Forgable
76
+ attr_reader name: Symbol
77
+
78
+ def self.define
79
+ : (ObjectForge::_Forgable forged, ?name: Symbol?) { (ObjectForge::ForgeDSL) -> void } -> ObjectForge::Forge
80
+ | (ObjectForge::_Forgable forged, ?name: Symbol?) { [self: ObjectForge::ForgeDSL] -> void } -> ObjectForge::Forge
81
+
82
+ def initialize
83
+ : (ObjectForge::_Forgable forged, ObjectForge::_ForgeParameters parameters, ?name: Symbol?) -> void
84
+
85
+ def forge
86
+ : (*Symbol traits, **untyped overrides) -> ObjectForge::_Forgable
87
+ | (Array[Symbol] traits, Hash[Symbol, untyped] overrides) -> ObjectForge::_Forgable
88
+ alias build forge
89
+ alias [] forge
90
+
91
+ private
92
+
93
+ def check_traits_and_overrides
94
+ : (Array[Symbol] traits, Hash[Symbol, untyped] overrides) -> [Array[Symbol], Hash[Symbol, untyped]]
95
+ | (Array[(Array[Symbol] | Hash[Symbol, untyped])], Hash[Symbol, untyped]) -> [Array[Symbol], Hash[Symbol, untyped]]
96
+ end
97
+
98
+ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
99
+ include ObjectForge::_ForgeParameters
100
+ attr_reader sequences: Hash[Symbol, ObjectForge::Sequence]
101
+
102
+ @attributes: Hash[Symbol, Proc]
103
+ @sequences: Hash[Symbol, ObjectForge::Sequence]
104
+ @traits: Hash[Symbol, Hash[Symbol, Proc]]
105
+
106
+ def initialize
107
+ : () { (ObjectForge::ForgeDSL) -> void } -> void
108
+ | () { [self: ObjectForge::ForgeDSL] -> void } -> void
109
+
110
+ def freeze: -> self
111
+
112
+ def attribute
113
+ : (Symbol name) { -> untyped } -> Symbol
114
+ | (Symbol name) { (ObjectForge::_Sequenceable) -> untyped } -> Symbol
115
+ alias [] attribute
116
+
117
+ def sequence
118
+ : (Symbol name, ?(ObjectForge::_Sequenceable | ObjectForge::Sequence) initial) { (ObjectForge::_Sequenceable) -> untyped } -> Symbol
119
+
120
+ def trait
121
+ : (Symbol name) { (self) -> void } -> Symbol
122
+
123
+ def inspect: -> String
124
+
125
+ private
126
+
127
+ def method_missing
128
+ : (Symbol name) { -> untyped } -> Symbol
129
+ # After freezing:
130
+ | (Symbol name) { -> untyped } -> void
131
+
132
+ def respond_to_missing?
133
+ : (Symbol name, bool include_all) -> bool
134
+
135
+ def rand: [T] (?(Float | Integer | Range[T])) -> (Float | Integer | T)
136
+ end
137
+
138
+ class ObjectForge::Crucible < ObjectForge::UnBasicObject
139
+ @attributes: Hash[Symbol, untyped]
140
+ @resolved_attributes: Set[Symbol]
141
+
142
+ def initialize
143
+ : (Hash[Symbol, untyped] attributes) -> void
144
+
145
+ def resolve!
146
+ : -> Hash[Symbol, untyped]
147
+
148
+ private
149
+
150
+ def method_missing
151
+ : (Symbol name) -> untyped
152
+ alias [] method_missing
153
+
154
+ def respond_to_missing?
155
+ : (Symbol name, bool include_all) -> bool
156
+ end
157
+
158
+ class ObjectForge::UnBasicObject < BasicObject
159
+ def class: -> Class
160
+
161
+ def eql?: (untyped other) -> bool
162
+
163
+ def freeze: -> self
164
+
165
+ def frozen?: -> bool
166
+
167
+ def hash: -> Integer
168
+
169
+ def inspect: -> String
170
+
171
+ def is_a?: (Module klass) -> bool
172
+
173
+ def respond_to?: (Symbol name, ?bool include_private) -> bool
174
+
175
+ def to_s: -> String
176
+
177
+ def pretty_print: (untyped) -> void
178
+
179
+ def pretty_print_cycle: (untyped) -> void
180
+
181
+ private
182
+
183
+ def block_given?: -> bool
184
+
185
+ def raise: (_Exception exception, ?String message) -> void
186
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: object_forge
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexandr Bulancov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-07-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ description: |
28
+ ObjectForge provides a familiar way to build objects in any context
29
+ with minimal assumptions about usage environment.
30
+ It has no connection to any framework and, indeed, has nothing to do with a database.
31
+ To use, just define some factories and call them wherever you need,
32
+ be it in tests, console, or application code.
33
+ email:
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - lib/object_forge.rb
39
+ - lib/object_forge/crucible.rb
40
+ - lib/object_forge/forge.rb
41
+ - lib/object_forge/forge_dsl.rb
42
+ - lib/object_forge/forgeyard.rb
43
+ - lib/object_forge/sequence.rb
44
+ - lib/object_forge/un_basic_object.rb
45
+ - lib/object_forge/version.rb
46
+ - sig/object_forge.rbs
47
+ homepage: https://github.com/trinistr/object_forge
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ homepage_uri: https://github.com/trinistr/object_forge
52
+ bug_tracker_uri: https://github.com/trinistr/object_forge/issues
53
+ documentation_uri: https://rubydoc.info/gems/object_forge/0.1.0
54
+ source_code_uri: https://github.com/trinistr/object_forge/tree/v0.1.0
55
+ changelog_uri: https://github.com/trinistr/object_forge/blob/v0.1.0/CHANGELOG.md
56
+ rubygems_mfa_required: 'true'
57
+ post_install_message:
58
+ rdoc_options:
59
+ - "--tag"
60
+ - thread_safety:Thread safety
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.1.3
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.3.27
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: A simple factory for objects with minimal assumptions.
78
+ test_files: []