object_forge 0.1.1 → 0.2.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/lib/object_forge/crucible.rb +0 -1
- data/lib/object_forge/forge.rb +16 -10
- data/lib/object_forge/forge_dsl.rb +30 -1
- data/lib/object_forge/forgeyard.rb +5 -3
- data/lib/object_forge/molds/hash_mold.rb +54 -0
- data/lib/object_forge/molds/keywords_mold.rb +21 -17
- data/lib/object_forge/molds/mold_mold.rb +40 -0
- data/lib/object_forge/molds/single_argument_mold.rb +20 -0
- data/lib/object_forge/molds/struct_mold.rb +69 -0
- data/lib/object_forge/molds/wrapped_mold.rb +30 -0
- data/lib/object_forge/molds.rb +63 -0
- data/lib/object_forge/un_basic_object.rb +0 -1
- data/lib/object_forge/version.rb +1 -1
- data/lib/object_forge.rb +4 -2
- data/sig/object_forge/molds.rbs +72 -0
- data/sig/object_forge.rbs +8 -4
- metadata +17 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83440b5319d1e909379d16b67830ad1f67f575358473dd4ba5914cb549c417b9
|
|
4
|
+
data.tar.gz: 279a5756a157f96252081cb60ce0541bdffdf1e21a25fe72db19b622c7d717e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e16b3091daf563ffffbb5da39b50a6cc2298a288a1e71528ab9de905e0c959718555ff8d3b6b2a3478f094fa3ebff8727f0a95e97c6adef05d3c7abb1ae6676f
|
|
7
|
+
data.tar.gz: 04455c660052171b8a4456323b700f91805d0c05190329427baee1468b42ccfee3c315b79534f997b45a1f2cd716f85076e164f8007cad821f43047a5ea8fcbf
|
|
@@ -12,7 +12,6 @@ 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
17
|
%i[rand].each { |m| private define_method(m, ::Object.instance_method(m)) }
|
data/lib/object_forge/forge.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "crucible"
|
|
4
4
|
require_relative "forge_dsl"
|
|
5
|
+
require_relative "molds"
|
|
5
6
|
|
|
6
7
|
module ObjectForge
|
|
7
8
|
# Object instantitation forge.
|
|
@@ -19,14 +20,22 @@ module ObjectForge
|
|
|
19
20
|
# @!attribute [r] traits
|
|
20
21
|
# Attributes belonging to traits.
|
|
21
22
|
# @return [Hash{Symbol => Hash{Symbol => Any}}]
|
|
22
|
-
|
|
23
|
+
#
|
|
24
|
+
# @!attribute [r] mold
|
|
25
|
+
# An object that knows how to build the instance.
|
|
26
|
+
# Must have a +call+ method that takes a class and a hash of attributes.
|
|
27
|
+
# @since 0.2.0
|
|
28
|
+
# @return [#call, nil]
|
|
29
|
+
Parameters = Struct.new(:attributes, :traits, :mold, keyword_init: true)
|
|
30
|
+
|
|
31
|
+
MOLD_MOLD = Molds::MoldMold.new.freeze
|
|
23
32
|
|
|
24
33
|
# Define (and create) a forge using DSL.
|
|
25
34
|
#
|
|
26
35
|
# @see ForgeDSL
|
|
27
36
|
# @thread_safety Thread-safe if DSL definition is thread-safe.
|
|
28
37
|
#
|
|
29
|
-
# @param forged [Class] class to forge
|
|
38
|
+
# @param forged [Class, Any] class or object to forge
|
|
30
39
|
# @param name [Symbol, nil] forge name
|
|
31
40
|
# @yieldparam f [ForgeDSL]
|
|
32
41
|
# @yieldreturn [void]
|
|
@@ -38,19 +47,20 @@ module ObjectForge
|
|
|
38
47
|
# @return [Symbol, nil] forge name
|
|
39
48
|
attr_reader :name
|
|
40
49
|
|
|
41
|
-
# @return [Class] class to forge
|
|
50
|
+
# @return [Class, Any] class or object to forge
|
|
42
51
|
attr_reader :forged
|
|
43
52
|
|
|
44
53
|
# @return [Parameters, ForgeDSL] forge parameters
|
|
45
54
|
attr_reader :parameters
|
|
46
55
|
|
|
47
|
-
# @param forged [Class] class to forge
|
|
56
|
+
# @param forged [Class, Any] class or object to forge
|
|
48
57
|
# @param parameters [Parameters, ForgeDSL] forge parameters
|
|
49
58
|
# @param name [Symbol, nil] forge name
|
|
50
59
|
def initialize(forged, parameters, name: nil)
|
|
51
60
|
@name = name
|
|
52
61
|
@forged = forged
|
|
53
62
|
@parameters = parameters
|
|
63
|
+
@mold = parameters.mold || MOLD_MOLD.call(forged: forged)
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
# Forge a new instance.
|
|
@@ -67,7 +77,7 @@ module ObjectForge
|
|
|
67
77
|
# If a block is given, forged instance is yielded to it after being built.
|
|
68
78
|
#
|
|
69
79
|
# @thread_safety Forging is thread-safe if {#parameters},
|
|
70
|
-
#
|
|
80
|
+
# +traits+ and +overrides+ are thread-safe.
|
|
71
81
|
#
|
|
72
82
|
# @param traits [Array<Symbol>] traits to apply
|
|
73
83
|
# @param overrides [Hash{Symbol => Any}] attribute overrides
|
|
@@ -76,7 +86,7 @@ module ObjectForge
|
|
|
76
86
|
# @return [Any] built instance
|
|
77
87
|
def forge(*traits, **overrides)
|
|
78
88
|
resolved_attributes = resolve_attributes(traits, overrides)
|
|
79
|
-
instance =
|
|
89
|
+
instance = @mold.call(forged: @forged, attributes: resolved_attributes)
|
|
80
90
|
yield instance if block_given?
|
|
81
91
|
instance
|
|
82
92
|
end
|
|
@@ -90,9 +100,5 @@ module ObjectForge
|
|
|
90
100
|
attributes = @parameters.attributes.merge(*@parameters.traits.values_at(*traits), overrides)
|
|
91
101
|
Crucible.new(attributes).resolve!
|
|
92
102
|
end
|
|
93
|
-
|
|
94
|
-
def build_instance(attributes)
|
|
95
|
-
forged.new(attributes)
|
|
96
|
-
end
|
|
97
103
|
end
|
|
98
104
|
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 [#call, nil] forge mold
|
|
31
|
+
attr_reader :mold
|
|
32
|
+
|
|
29
33
|
# Define forge's parameters through DSL.
|
|
30
34
|
#
|
|
31
35
|
# If the block has a parameter, an object will be yielded,
|
|
@@ -71,9 +75,34 @@ module ObjectForge
|
|
|
71
75
|
@attributes.freeze
|
|
72
76
|
@sequences.freeze
|
|
73
77
|
@traits.freeze
|
|
78
|
+
@mold.freeze
|
|
74
79
|
self
|
|
75
80
|
end
|
|
76
81
|
|
|
82
|
+
# Set the forge mold.
|
|
83
|
+
#
|
|
84
|
+
# Mold is an object that knows how to take a hash of attributes
|
|
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.
|
|
89
|
+
#
|
|
90
|
+
# @since 0.2.0
|
|
91
|
+
#
|
|
92
|
+
# @param mold [Class, #call, nil]
|
|
93
|
+
# @return [Class, #call, nil]
|
|
94
|
+
#
|
|
95
|
+
# @raise [DSLError] if +mold+ does not respond to or implement +#call+
|
|
96
|
+
def mold=(mold)
|
|
97
|
+
if nil == mold || mold.respond_to?(:call) # rubocop:disable Style/YodaCondition
|
|
98
|
+
@mold = mold
|
|
99
|
+
elsif ::Class === mold && mold.public_method_defined?(:call)
|
|
100
|
+
@mold = Molds::WrappedMold.new(mold)
|
|
101
|
+
else
|
|
102
|
+
raise DSLError, "mold must respond to or implement #call"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
77
106
|
# Define an attribute, possibly transient.
|
|
78
107
|
#
|
|
79
108
|
# DSL does not know or care what attributes the forged class has,
|
|
@@ -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
|
|
@@ -55,8 +57,8 @@ module ObjectForge
|
|
|
55
57
|
# @return [Any] built instance
|
|
56
58
|
#
|
|
57
59
|
# @raise [KeyError] if forge with the specified name is not registered
|
|
58
|
-
def forge(name,
|
|
59
|
-
@forges.fetch(name)[
|
|
60
|
+
def forge(name, ...)
|
|
61
|
+
@forges.fetch(name).[](...)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
alias build forge
|
|
@@ -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,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
module ObjectForge
|
|
4
|
+
module Molds
|
|
5
|
+
# Basic mold which calls +forged.new(**attributes)+.
|
|
6
|
+
#
|
|
7
|
+
# Can be used instead of {SingleArgumentMold},
|
|
8
|
+
# but performance is about 1.5 times worse.
|
|
9
|
+
#
|
|
10
|
+
# @thread_safety Thread-safe.
|
|
11
|
+
# @since 0.2.0
|
|
12
|
+
class KeywordsMold
|
|
13
|
+
# Instantiate +forged+ with a hash of attributes.
|
|
14
|
+
#
|
|
15
|
+
# @param forged [Class, #new]
|
|
16
|
+
# @param attributes [Hash{Symbol => Any}]
|
|
17
|
+
# @return [Any]
|
|
18
|
+
def call(forged:, attributes:, **_)
|
|
19
|
+
forged.new(**attributes)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
|
@@ -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,69 @@
|
|
|
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
|
+
# @return [Boolean]
|
|
16
|
+
RUBY_FEATURE_AUTO_KEYWORDS = (::Struct.new(:a, :b).new(a: 1, b: 2).a == 1)
|
|
17
|
+
|
|
18
|
+
# Whether to work around argument hashes with extra keys.
|
|
19
|
+
#
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
attr_reader :lax
|
|
22
|
+
alias lax? lax
|
|
23
|
+
|
|
24
|
+
# @param lax [Boolean]
|
|
25
|
+
# whether to work around argument hashes with extra keys
|
|
26
|
+
# (when keyword_init is false, workaround always happens for technical reasons)
|
|
27
|
+
# - if +true+, arguments can contain extra keys, but building is slower;
|
|
28
|
+
# - if +false+, building may raise an error if extra keys are present;
|
|
29
|
+
def initialize(lax: true)
|
|
30
|
+
@lax = lax
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Instantiate +forged+ struct with a hash of attributes.
|
|
34
|
+
#
|
|
35
|
+
# @param forged [Class] a subclass of Struct
|
|
36
|
+
# @param attributes [Hash{Symbol => Any}]
|
|
37
|
+
# @return [Struct]
|
|
38
|
+
def call(forged:, attributes:, **_)
|
|
39
|
+
if forged.keyword_init?
|
|
40
|
+
lax ? forged.new(attributes.slice(*forged.members)) : forged.new(attributes)
|
|
41
|
+
elsif forged.keyword_init? == false
|
|
42
|
+
forged.new(*attributes.values_at(*forged.members))
|
|
43
|
+
else
|
|
44
|
+
build_struct_with_unspecified_keyword_init(forged, attributes)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
if RUBY_FEATURE_AUTO_KEYWORDS
|
|
51
|
+
# Build struct by using keywords to specify member values.
|
|
52
|
+
def build_struct_with_unspecified_keyword_init(forged, attributes)
|
|
53
|
+
if lax
|
|
54
|
+
forged.new(**attributes.slice(*forged.members))
|
|
55
|
+
else
|
|
56
|
+
forged.new(**attributes)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
# :nocov:
|
|
61
|
+
# Build struct by using positional arguments to specify member values.
|
|
62
|
+
def build_struct_with_unspecified_keyword_init(forged, attributes)
|
|
63
|
+
forged.new(*attributes.values_at(*forged.members))
|
|
64
|
+
end
|
|
65
|
+
# :nocov:
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
# @thread_safety Thread-safe if {wrapped_mold} does not use global state.
|
|
11
|
+
# @since 0.2.0
|
|
12
|
+
class WrappedMold
|
|
13
|
+
# @return [Class] wrapped mold class
|
|
14
|
+
attr_reader :wrapped_mold
|
|
15
|
+
|
|
16
|
+
# @param wrapped_mold [Class] class with +#call+ method
|
|
17
|
+
def initialize(wrapped_mold)
|
|
18
|
+
@wrapped_mold = wrapped_mold
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @overload call(...)
|
|
22
|
+
# Instantiate {wrapped_mold} and call it.
|
|
23
|
+
#
|
|
24
|
+
# @return [Any] result of +wrapped_mold.new.call(...)+
|
|
25
|
+
def call(...)
|
|
26
|
+
wrapped_mold.new.call(...)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
# Molds are +#call+able objects responsible for actually building objects produced by factories
|
|
7
|
+
# (or doing other, interesting things with them (truly, only the code review is the limit!)).
|
|
8
|
+
# They are supposed to be immutable, shareable, and persistent:
|
|
9
|
+
# initialize once, use for the whole runtime.
|
|
10
|
+
#
|
|
11
|
+
# A simple mold can easily be just a +Proc+.
|
|
12
|
+
# All molds must have the following +#call+ signature: +call(forged:, attributes:, **)+.
|
|
13
|
+
# The extra keywords are for future extensions.
|
|
14
|
+
#
|
|
15
|
+
# @example A very basic FactoryBot replacement
|
|
16
|
+
# creator = ->(forged:, attributes:, **) do
|
|
17
|
+
# instance = forged.new
|
|
18
|
+
# attributes.each_pair { instance.public_send(:"#{_1}=", _2) }
|
|
19
|
+
# instance.save!
|
|
20
|
+
# end
|
|
21
|
+
# creator.call(forged: User, attributes: { name: "John", age: 30 })
|
|
22
|
+
# # => <User name="John" age=30>
|
|
23
|
+
# @example Using a mold to serialize collection of objects (contrivedly)
|
|
24
|
+
# dumpy = ->(forged:, attributes:, **) do
|
|
25
|
+
# Enumerator.new(attributes.size) do |y|
|
|
26
|
+
# attributes.each_pair { y << forged.dump(_1 => _2) }
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
# dumpy.call(forged: JSON, attributes: {a:1, b:2}).to_a
|
|
30
|
+
# # => ["{\"a\":1}", "{\"b\":2}"]
|
|
31
|
+
# dumpy.call(forged: YAML, attributes: {a:1, b:2}).to_a
|
|
32
|
+
# # => ["---\n:a: 1\n", "---\n:b: 2\n"]
|
|
33
|
+
# @example Abstract factory pattern (kind of)
|
|
34
|
+
# class FurnitureFactory
|
|
35
|
+
# def call(forged:, attributes:, **)
|
|
36
|
+
# concrete_factory = concrete_factory(forged)
|
|
37
|
+
# attributes[:pieces].map do |piece|
|
|
38
|
+
# concrete_factory.public_send(piece, attributes.dig(:color, piece))
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
# private def concrete_factory(style)
|
|
42
|
+
# case style
|
|
43
|
+
# when :hitech
|
|
44
|
+
# HiTechFactory.new
|
|
45
|
+
# when :retro
|
|
46
|
+
# RetroFactory.new
|
|
47
|
+
# end
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
# FurnitureFactory.new.call(forged: :hitech, attributes: {
|
|
51
|
+
# pieces: [:chair, :table], color: { chair: :black, table: :white }
|
|
52
|
+
# })
|
|
53
|
+
# # => [<#HiTech::Chair color=:black>, <#HiTech::Table color=:white>]
|
|
54
|
+
# @example Abusing molds
|
|
55
|
+
# printer = ->(forged:, attributes:, **) { PP.pp(attributes, forged) }
|
|
56
|
+
# printer.call(forged: $stderr, attributes: {a:1, b:2})
|
|
57
|
+
# # outputs "{:a=>1, :b=>2}" to $stderr
|
|
58
|
+
#
|
|
59
|
+
# @since 0.2.0
|
|
60
|
+
module Molds
|
|
61
|
+
Dir["#{__dir__}/molds/*.rb"].each { require_relative _1 }
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/object_forge/version.rb
CHANGED
data/lib/object_forge.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "object_forge/forgeyard"
|
|
4
|
+
require_relative "object_forge/sequence"
|
|
5
|
+
require_relative "object_forge/version"
|
|
4
6
|
|
|
5
7
|
# A simple all-purpose factory library with minimal assumptions.
|
|
6
8
|
#
|
|
@@ -76,7 +78,7 @@ module ObjectForge
|
|
|
76
78
|
# @since 0.1.0
|
|
77
79
|
#
|
|
78
80
|
# @param name [Symbol] forge name
|
|
79
|
-
# @param forged [Class] class to forge
|
|
81
|
+
# @param forged [Class, Any] class or object to forge
|
|
80
82
|
# @yieldparam f [ForgeDSL]
|
|
81
83
|
# @yieldreturn [void]
|
|
82
84
|
# @return [Forge] forge
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module ObjectForge
|
|
2
|
+
interface _Mold
|
|
3
|
+
def call
|
|
4
|
+
: (forged: untyped, attributes: Hash[Symbol, untyped], **untyped) -> untyped
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module Molds
|
|
8
|
+
class MoldMold
|
|
9
|
+
def call
|
|
10
|
+
: (forged: untyped, **untyped) -> ObjectForge::_Mold
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class SingleArgumentMold
|
|
14
|
+
include ObjectForge::_Mold
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class KeywordsMold
|
|
18
|
+
include ObjectForge::_Mold
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class WrappedMold
|
|
22
|
+
interface _MoldClass[T]
|
|
23
|
+
def new: () -> T
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
include ObjectForge::_Mold
|
|
27
|
+
|
|
28
|
+
attr_reader wrapped_mold: _MoldClass[ObjectForge::mold]
|
|
29
|
+
|
|
30
|
+
def initialize: (_MoldClass[ObjectForge::mold] wrapped_mold) -> void
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class StructMold
|
|
34
|
+
interface _StructSubclass[T]
|
|
35
|
+
def new
|
|
36
|
+
: (*untyped) -> T
|
|
37
|
+
| (**untyped) -> T
|
|
38
|
+
| (Hash[Symbol, untyped]) -> T
|
|
39
|
+
def members: -> Array[Symbol]
|
|
40
|
+
def keyword_init?: -> bool?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
RUBY_FEATURE_AUTO_KEYWORDS: bool
|
|
44
|
+
attr_reader lax: bool
|
|
45
|
+
|
|
46
|
+
def initialize: (?lax: bool) -> void
|
|
47
|
+
|
|
48
|
+
def call
|
|
49
|
+
: [T < Struct] (forged: _StructSubclass[T], attributes: Hash[Symbol, untyped], **untyped) -> T
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def build_struct_with_unspecified_keyword_init
|
|
54
|
+
: [T < Struct] (_StructSubclass[T] forged, Hash[Symbol, untyped] attributes) -> T
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class HashMold
|
|
58
|
+
interface _HashSubclass[T]
|
|
59
|
+
def []: (Hash[untyped, untyped]) -> T
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attr_reader default: untyped?
|
|
63
|
+
attr_reader default_proc: Proc?
|
|
64
|
+
|
|
65
|
+
def initialize
|
|
66
|
+
: (?untyped? default_value) ?{ (Hash[untyped, untyped] hash, untyped key) -> untyped} -> void
|
|
67
|
+
|
|
68
|
+
def call
|
|
69
|
+
: [T < Hash] (forged: _HashSubclass[T], attributes: Hash[Symbol, untyped], **untyped) -> T
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/sig/object_forge.rbs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
module ObjectForge
|
|
2
|
+
type mold = ObjectForge::_RespondTo & 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,6 +15,7 @@ interface _Sequenceable
|
|
|
14
15
|
interface _ForgeParameters
|
|
15
16
|
def attributes: () -> Hash[Symbol, untyped]
|
|
16
17
|
def traits: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
18
|
+
def mold: () -> ObjectForge::mold?
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
class Error < StandardError
|
|
@@ -76,6 +78,8 @@ class ObjectForge::Forge
|
|
|
76
78
|
: (attributes: Hash[Symbol, untyped], traits: Hash[Symbol, Hash[Symbol, untyped]]) -> void
|
|
77
79
|
end
|
|
78
80
|
|
|
81
|
+
MOLD_MOLD: ObjectForge::Molds::MoldMold
|
|
82
|
+
|
|
79
83
|
attr_reader forged: ObjectForge::_Forgable
|
|
80
84
|
attr_reader name: Symbol
|
|
81
85
|
|
|
@@ -95,9 +99,6 @@ class ObjectForge::Forge
|
|
|
95
99
|
|
|
96
100
|
def resolve_attributes
|
|
97
101
|
: (Array[Symbol] traits, Hash[Symbol, untyped] overrides) -> Hash[Symbol, untyped]
|
|
98
|
-
|
|
99
|
-
def build_instance
|
|
100
|
-
: (Hash[Symbol, untyped] attributes) -> ObjectForge::_Forgable
|
|
101
102
|
end
|
|
102
103
|
|
|
103
104
|
class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
@@ -115,6 +116,9 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
115
116
|
|
|
116
117
|
def freeze: -> self
|
|
117
118
|
|
|
119
|
+
def mold=
|
|
120
|
+
: (ObjectForge::mold) -> void
|
|
121
|
+
|
|
118
122
|
def attribute
|
|
119
123
|
: (Symbol name) { [self: ObjectForge::Crucible] -> untyped } -> Symbol
|
|
120
124
|
alias [] attribute
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: object_forge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexandr Bulancov
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2025-08-20 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: concurrent-ruby
|
|
@@ -30,6 +31,7 @@ description: |
|
|
|
30
31
|
To use, just define some factories and call them wherever you need,
|
|
31
32
|
be it in tests, console, or application code.
|
|
32
33
|
If needed, almost any part of the process can be easily replaced with a custom solution.
|
|
34
|
+
email:
|
|
33
35
|
executables: []
|
|
34
36
|
extensions: []
|
|
35
37
|
extra_rdoc_files: []
|
|
@@ -39,21 +41,29 @@ files:
|
|
|
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/mold_mold.rb
|
|
48
|
+
- lib/object_forge/molds/single_argument_mold.rb
|
|
49
|
+
- lib/object_forge/molds/struct_mold.rb
|
|
50
|
+
- lib/object_forge/molds/wrapped_mold.rb
|
|
43
51
|
- lib/object_forge/sequence.rb
|
|
44
52
|
- lib/object_forge/un_basic_object.rb
|
|
45
53
|
- lib/object_forge/version.rb
|
|
46
54
|
- sig/object_forge.rbs
|
|
55
|
+
- sig/object_forge/molds.rbs
|
|
47
56
|
homepage: https://github.com/trinistr/object_forge
|
|
48
57
|
licenses:
|
|
49
58
|
- MIT
|
|
50
59
|
metadata:
|
|
51
60
|
homepage_uri: https://github.com/trinistr/object_forge
|
|
52
61
|
bug_tracker_uri: https://github.com/trinistr/object_forge/issues
|
|
53
|
-
documentation_uri: https://rubydoc.info/gems/object_forge/0.
|
|
54
|
-
source_code_uri: https://github.com/trinistr/object_forge/tree/v0.
|
|
55
|
-
changelog_uri: https://github.com/trinistr/object_forge/blob/v0.
|
|
62
|
+
documentation_uri: https://rubydoc.info/gems/object_forge/0.2.0
|
|
63
|
+
source_code_uri: https://github.com/trinistr/object_forge/tree/v0.2.0
|
|
64
|
+
changelog_uri: https://github.com/trinistr/object_forge/blob/v0.2.0/CHANGELOG.md
|
|
56
65
|
rubygems_mfa_required: 'true'
|
|
66
|
+
post_install_message:
|
|
57
67
|
rdoc_options:
|
|
58
68
|
- "--tag"
|
|
59
69
|
- thread_safety:Thread safety
|
|
@@ -70,7 +80,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
80
|
- !ruby/object:Gem::Version
|
|
71
81
|
version: '0'
|
|
72
82
|
requirements: []
|
|
73
|
-
rubygems_version: 3.
|
|
83
|
+
rubygems_version: 3.5.22
|
|
84
|
+
signing_key:
|
|
74
85
|
specification_version: 4
|
|
75
86
|
summary: A simple factory for objects with minimal assumptions.
|
|
76
87
|
test_files: []
|