object_forge 0.1.0 → 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 +28 -26
- data/lib/object_forge/forge_dsl.rb +30 -1
- data/lib/object_forge/forgeyard.rb +7 -3
- data/lib/object_forge/molds/hash_mold.rb +54 -0
- data/lib/object_forge/molds/keywords_mold.rb +23 -0
- 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/sequence.rb +1 -1
- data/lib/object_forge/un_basic_object.rb +0 -1
- data/lib/object_forge/version.rb +1 -1
- data/lib/object_forge.rb +7 -3
- data/sig/object_forge/molds.rbs +72 -0
- data/sig/object_forge.rbs +34 -25
- metadata +15 -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,25 +47,26 @@ 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.
|
|
57
67
|
#
|
|
58
|
-
# @overload forge(*traits, **overrides)
|
|
59
|
-
# @overload forge(traits, overrides)
|
|
68
|
+
# @overload forge(*traits, **overrides, &)
|
|
69
|
+
# @overload forge(traits, overrides, &)
|
|
60
70
|
#
|
|
61
71
|
# Positional arguments are taken as trait names, keyword arguments as attribute overrides,
|
|
62
72
|
# unless there are exactly two positional arguments: an array and a hash.
|
|
@@ -64,19 +74,21 @@ module ObjectForge
|
|
|
64
74
|
# All traits and overrides are applied in argument order,
|
|
65
75
|
# with overrides always applied after traits.
|
|
66
76
|
#
|
|
77
|
+
# If a block is given, forged instance is yielded to it after being built.
|
|
78
|
+
#
|
|
67
79
|
# @thread_safety Forging is thread-safe if {#parameters},
|
|
68
|
-
#
|
|
80
|
+
# +traits+ and +overrides+ are thread-safe.
|
|
69
81
|
#
|
|
70
82
|
# @param traits [Array<Symbol>] traits to apply
|
|
71
83
|
# @param overrides [Hash{Symbol => Any}] attribute overrides
|
|
84
|
+
# @yieldparam object [Any] forged instance
|
|
85
|
+
# @yieldreturn [void]
|
|
72
86
|
# @return [Any] built instance
|
|
73
87
|
def forge(*traits, **overrides)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
forged.new(attributes)
|
|
88
|
+
resolved_attributes = resolve_attributes(traits, overrides)
|
|
89
|
+
instance = @mold.call(forged: @forged, attributes: resolved_attributes)
|
|
90
|
+
yield instance if block_given?
|
|
91
|
+
instance
|
|
80
92
|
end
|
|
81
93
|
|
|
82
94
|
alias build forge
|
|
@@ -84,19 +96,9 @@ module ObjectForge
|
|
|
84
96
|
|
|
85
97
|
private
|
|
86
98
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
99
|
+
def resolve_attributes(traits, overrides)
|
|
100
|
+
attributes = @parameters.attributes.merge(*@parameters.traits.values_at(*traits), overrides)
|
|
101
|
+
Crucible.new(attributes).resolve!
|
|
100
102
|
end
|
|
101
103
|
end
|
|
102
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
|
|
@@ -50,11 +52,13 @@ module ObjectForge
|
|
|
50
52
|
# @param name [Symbol] name of the forge
|
|
51
53
|
# @param traits [Array<Symbol>] traits to apply
|
|
52
54
|
# @param overrides [Hash{Symbol => Any}] attribute overrides
|
|
55
|
+
# @yieldparam object [Any] forged instance
|
|
56
|
+
# @yieldreturn [void]
|
|
53
57
|
# @return [Any] built instance
|
|
54
58
|
#
|
|
55
59
|
# @raise [KeyError] if forge with the specified name is not registered
|
|
56
|
-
def forge(name,
|
|
57
|
-
@forges.fetch(name)[
|
|
60
|
+
def forge(name, ...)
|
|
61
|
+
@forges.fetch(name).[](...)
|
|
58
62
|
end
|
|
59
63
|
|
|
60
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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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
|
|
@@ -84,7 +86,7 @@ module ObjectForge
|
|
|
84
86
|
DEFAULT_YARD.define(...)
|
|
85
87
|
end
|
|
86
88
|
|
|
87
|
-
# @overload forge(name, *traits, **overrides)
|
|
89
|
+
# @overload forge(name, *traits, **overrides, &)
|
|
88
90
|
# Build an instance using a forge from {DEFAULT_YARD}.
|
|
89
91
|
#
|
|
90
92
|
# @!macro default_forgeyard
|
|
@@ -94,6 +96,8 @@ module ObjectForge
|
|
|
94
96
|
# @param name [Symbol] name of the forge
|
|
95
97
|
# @param traits [Array<Symbol>] traits to apply
|
|
96
98
|
# @param overrides [Hash{Symbol => Any}] attribute overrides
|
|
99
|
+
# @yieldparam object [Any] forged instance
|
|
100
|
+
# @yieldreturn [void]
|
|
97
101
|
# @return [Any] built instance
|
|
98
102
|
def self.forge(...)
|
|
99
103
|
DEFAULT_YARD.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,47 +1,53 @@
|
|
|
1
1
|
module ObjectForge
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class DSLError < Error
|
|
5
|
-
end
|
|
2
|
+
type mold = ObjectForge::_RespondTo & ObjectForge::_Mold
|
|
3
|
+
type sequenceable = ObjectForge::_RespondTo & ObjectForge::_Sequenceable
|
|
6
4
|
|
|
7
|
-
interface
|
|
8
|
-
def succ: -> self
|
|
5
|
+
interface _RespondTo
|
|
9
6
|
def respond_to?: (Symbol name, ?bool include_private) -> bool
|
|
10
7
|
def class: -> Class
|
|
11
8
|
end
|
|
9
|
+
interface _Sequenceable
|
|
10
|
+
def succ: -> self
|
|
11
|
+
end
|
|
12
12
|
interface _Forgable
|
|
13
13
|
def new: (Hash[Symbol, untyped]) -> self
|
|
14
14
|
end
|
|
15
15
|
interface _ForgeParameters
|
|
16
16
|
def attributes: () -> Hash[Symbol, untyped]
|
|
17
17
|
def traits: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
18
|
+
def mold: () -> ObjectForge::mold?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Error < StandardError
|
|
22
|
+
end
|
|
23
|
+
class DSLError < Error
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
VERSION: String
|
|
21
27
|
DEFAULT_YARD: ObjectForge::Forgeyard
|
|
22
28
|
|
|
23
29
|
def self.sequence
|
|
24
|
-
: (?(ObjectForge::
|
|
30
|
+
: (?(ObjectForge::sequenceable | ObjectForge::Sequence) initial) -> ObjectForge::Sequence
|
|
25
31
|
|
|
26
32
|
def self.define
|
|
27
33
|
: (Symbol name, ObjectForge::_Forgable forged) { (ObjectForge::ForgeDSL) -> void } -> ObjectForge::Forge
|
|
28
34
|
| (Symbol name, ObjectForge::_Forgable forged) { [self: ObjectForge::ForgeDSL] -> void } -> ObjectForge::Forge
|
|
29
35
|
|
|
30
36
|
def self.forge
|
|
31
|
-
: (Symbol name, *Symbol traits, **untyped overrides) -> ObjectForge::_Forgable
|
|
37
|
+
: (Symbol name, *Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
class ObjectForge::Sequence
|
|
35
41
|
def self.new
|
|
36
|
-
: (?(ObjectForge::
|
|
42
|
+
: (?(ObjectForge::sequenceable | ObjectForge::Sequence) initial) -> ObjectForge::Sequence
|
|
37
43
|
|
|
38
|
-
attr_reader initial: ObjectForge::
|
|
44
|
+
attr_reader initial: ObjectForge::sequenceable
|
|
39
45
|
|
|
40
|
-
def initialize: (ObjectForge::
|
|
46
|
+
def initialize: (ObjectForge::sequenceable initial) -> void
|
|
41
47
|
|
|
42
|
-
def next: -> ObjectForge::
|
|
48
|
+
def next: -> ObjectForge::sequenceable
|
|
43
49
|
|
|
44
|
-
def reset: -> ObjectForge::
|
|
50
|
+
def reset: -> ObjectForge::sequenceable
|
|
45
51
|
alias rewind reset
|
|
46
52
|
end
|
|
47
53
|
|
|
@@ -59,7 +65,7 @@ class ObjectForge::Forgeyard
|
|
|
59
65
|
: (Symbol name, ObjectForge::Forge forge) -> ObjectForge::Forge
|
|
60
66
|
|
|
61
67
|
def forge
|
|
62
|
-
: (Symbol name, *Symbol traits, **untyped overrides) -> ObjectForge::_Forgable
|
|
68
|
+
: (Symbol name, *Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
63
69
|
alias build forge
|
|
64
70
|
alias [] forge
|
|
65
71
|
end
|
|
@@ -68,10 +74,12 @@ class ObjectForge::Forge
|
|
|
68
74
|
class Parameters
|
|
69
75
|
include ObjectForge::_ForgeParameters
|
|
70
76
|
|
|
71
|
-
def
|
|
77
|
+
def initialize
|
|
72
78
|
: (attributes: Hash[Symbol, untyped], traits: Hash[Symbol, Hash[Symbol, untyped]]) -> void
|
|
73
79
|
end
|
|
74
80
|
|
|
81
|
+
MOLD_MOLD: ObjectForge::Molds::MoldMold
|
|
82
|
+
|
|
75
83
|
attr_reader forged: ObjectForge::_Forgable
|
|
76
84
|
attr_reader name: Symbol
|
|
77
85
|
|
|
@@ -83,20 +91,19 @@ class ObjectForge::Forge
|
|
|
83
91
|
: (ObjectForge::_Forgable forged, ObjectForge::_ForgeParameters parameters, ?name: Symbol?) -> void
|
|
84
92
|
|
|
85
93
|
def forge
|
|
86
|
-
: (*Symbol traits, **untyped overrides) -> ObjectForge::_Forgable
|
|
87
|
-
| (Array[Symbol] traits, Hash[Symbol, untyped] overrides) -> ObjectForge::_Forgable
|
|
94
|
+
: (*Symbol traits, **untyped overrides) ?{ (untyped) -> void } -> ObjectForge::_Forgable
|
|
88
95
|
alias build forge
|
|
89
96
|
alias [] forge
|
|
90
97
|
|
|
91
98
|
private
|
|
92
99
|
|
|
93
|
-
def
|
|
94
|
-
: (Array[Symbol] traits, Hash[Symbol, untyped] overrides) ->
|
|
95
|
-
| (Array[(Array[Symbol] | Hash[Symbol, untyped])], Hash[Symbol, untyped]) -> [Array[Symbol], Hash[Symbol, untyped]]
|
|
100
|
+
def resolve_attributes
|
|
101
|
+
: (Array[Symbol] traits, Hash[Symbol, untyped] overrides) -> Hash[Symbol, untyped]
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
99
105
|
include ObjectForge::_ForgeParameters
|
|
106
|
+
|
|
100
107
|
attr_reader sequences: Hash[Symbol, ObjectForge::Sequence]
|
|
101
108
|
|
|
102
109
|
@attributes: Hash[Symbol, Proc]
|
|
@@ -109,13 +116,15 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
109
116
|
|
|
110
117
|
def freeze: -> self
|
|
111
118
|
|
|
119
|
+
def mold=
|
|
120
|
+
: (ObjectForge::mold) -> void
|
|
121
|
+
|
|
112
122
|
def attribute
|
|
113
|
-
: (Symbol name) { -> untyped } -> Symbol
|
|
114
|
-
| (Symbol name) { (ObjectForge::_Sequenceable) -> untyped } -> Symbol
|
|
123
|
+
: (Symbol name) { [self: ObjectForge::Crucible] -> untyped } -> Symbol
|
|
115
124
|
alias [] attribute
|
|
116
125
|
|
|
117
126
|
def sequence
|
|
118
|
-
: (Symbol name, ?(ObjectForge::
|
|
127
|
+
: (Symbol name, ?(ObjectForge::sequenceable | ObjectForge::Sequence) initial) { (ObjectForge::sequenceable) [self: ObjectForge::Crucible] -> untyped } -> Symbol
|
|
119
128
|
|
|
120
129
|
def trait
|
|
121
130
|
: (Symbol name) { (self) -> void } -> Symbol
|
|
@@ -125,7 +134,7 @@ class ObjectForge::ForgeDSL < ObjectForge::UnBasicObject
|
|
|
125
134
|
private
|
|
126
135
|
|
|
127
136
|
def method_missing
|
|
128
|
-
: (Symbol name) { -> untyped } -> Symbol
|
|
137
|
+
: (Symbol name) { [self: ObjectForge::Crucible] -> untyped } -> Symbol
|
|
129
138
|
# After freezing:
|
|
130
139
|
| (Symbol name) { -> untyped } -> void
|
|
131
140
|
|
|
@@ -182,5 +191,5 @@ class ObjectForge::UnBasicObject < BasicObject
|
|
|
182
191
|
|
|
183
192
|
def block_given?: -> bool
|
|
184
193
|
|
|
185
|
-
def raise: (_Exception exception, ?String message) -> void
|
|
194
|
+
def raise: (_Exception exception, ?String message, ?Array[String] backtrace, ?cause: _Exception) -> void
|
|
186
195
|
end
|
metadata
CHANGED
|
@@ -1,14 +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
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-08-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -30,6 +30,7 @@ description: |
|
|
|
30
30
|
It has no connection to any framework and, indeed, has nothing to do with a database.
|
|
31
31
|
To use, just define some factories and call them wherever you need,
|
|
32
32
|
be it in tests, console, or application code.
|
|
33
|
+
If needed, almost any part of the process can be easily replaced with a custom solution.
|
|
33
34
|
email:
|
|
34
35
|
executables: []
|
|
35
36
|
extensions: []
|
|
@@ -40,19 +41,27 @@ files:
|
|
|
40
41
|
- lib/object_forge/forge.rb
|
|
41
42
|
- lib/object_forge/forge_dsl.rb
|
|
42
43
|
- lib/object_forge/forgeyard.rb
|
|
44
|
+
- lib/object_forge/molds.rb
|
|
45
|
+
- lib/object_forge/molds/hash_mold.rb
|
|
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'
|
|
57
66
|
post_install_message:
|
|
58
67
|
rdoc_options:
|
|
@@ -71,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
71
80
|
- !ruby/object:Gem::Version
|
|
72
81
|
version: '0'
|
|
73
82
|
requirements: []
|
|
74
|
-
rubygems_version: 3.
|
|
83
|
+
rubygems_version: 3.5.22
|
|
75
84
|
signing_key:
|
|
76
85
|
specification_version: 4
|
|
77
86
|
summary: A simple factory for objects with minimal assumptions.
|