factrey 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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +10 -0
- data/lib/factrey/blueprint/instantiator.rb +68 -0
- data/lib/factrey/blueprint/node.rb +49 -0
- data/lib/factrey/blueprint/type.rb +45 -0
- data/lib/factrey/blueprint.rb +62 -0
- data/lib/factrey/dsl/let.rb +22 -0
- data/lib/factrey/dsl/on.rb +16 -0
- data/lib/factrey/dsl.rb +182 -0
- data/lib/factrey/ref/builder.rb +17 -0
- data/lib/factrey/ref/defer.rb +27 -0
- data/lib/factrey/ref/resolver.rb +38 -0
- data/lib/factrey/ref/shorthand_methods.rb +30 -0
- data/lib/factrey/ref.rb +40 -0
- data/lib/factrey/version.rb +5 -0
- data/lib/factrey.rb +44 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 30e4d58ea43d66a45f4d22cb563acaa3a5f7811d09cd6e7bb3a6f617c57f4223
|
4
|
+
data.tar.gz: a0ba9e04f9d0a387d3cf643e27c7ae8d7ace5529c5dd7bca75f0ae7cbe3d1819
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 765c317be9294d3837c19e172ef05093b16b08d6903313f43ffaeeec9d39dc9e46e08fca2d07dddde9010595909f44bebe8cdcd735d8dd5f940760c6226fe829
|
7
|
+
data.tar.gz: 2fcc20ada4f208d4341cc5165241349690bb08131eb18e11b7623ff9ed8374326fc79012f88ec4b2429870175f33ac029a644b5ba67806f17f84809118a2dd54
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 yubrot
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# `factrey` gem
|
2
|
+
|
3
|
+
This is a part of [FactoryBot::Blueprint](https://github.com/yubrot/factory_bot-blueprint) library.
|
4
|
+
|
5
|
+
Factrey provides a declarative DSL to represent the creation plan of objects, for FactoryBot::Blueprint.
|
6
|
+
The name Factrey is derived from the words factory and tree.
|
7
|
+
|
8
|
+
## License
|
9
|
+
|
10
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Factrey
|
6
|
+
class Blueprint
|
7
|
+
# An internal class used by {Blueprint#instantiate}.
|
8
|
+
class Instantiator
|
9
|
+
# @return [Hash{Symbol => Object}]
|
10
|
+
attr_reader :objects
|
11
|
+
|
12
|
+
# @param context [Object]
|
13
|
+
# @param blueprint [Blueprint]
|
14
|
+
def initialize(context, blueprint)
|
15
|
+
@context = context
|
16
|
+
@objects = {}
|
17
|
+
@visited = Set.new
|
18
|
+
@blueprint = blueprint
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param node [Node]
|
22
|
+
# @return [Object]
|
23
|
+
def visit(node)
|
24
|
+
@objects.fetch(node.name) do
|
25
|
+
unless @visited.add?(node.name)
|
26
|
+
raise ArgumentError, "Circular references detected around #{node.type_annotated_name}"
|
27
|
+
end
|
28
|
+
|
29
|
+
@objects[node.name] = instantiate_object(node)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @param node [Node]
|
36
|
+
# @return [Object]
|
37
|
+
def instantiate_object(node)
|
38
|
+
resolver = Ref::Resolver.new(recursion_limit: 5) do |name|
|
39
|
+
visit(
|
40
|
+
@blueprint.nodes.fetch(name) do
|
41
|
+
raise ArgumentError, "Missing definition #{name} around #{node.type_annotated_name}"
|
42
|
+
end,
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
args = resolver.resolve(node.args)
|
47
|
+
kwargs = resolver.resolve(node.kwargs)
|
48
|
+
|
49
|
+
# Resolve auto references to the ancestors
|
50
|
+
auto_references = {}
|
51
|
+
node.type.auto_references.each do |type_name, attribute|
|
52
|
+
next if kwargs.member? attribute # explicitly specified
|
53
|
+
|
54
|
+
compatible_ancestor, index = node.ancestors.reverse_each.with_index.find do |ancestor, _|
|
55
|
+
ancestor.type.compatible_types.include?(type_name)
|
56
|
+
end
|
57
|
+
next unless compatible_ancestor
|
58
|
+
next if auto_references.member?(attribute) && auto_references[attribute][1] <= index
|
59
|
+
|
60
|
+
auto_references[attribute] = [compatible_ancestor, index]
|
61
|
+
end
|
62
|
+
auto_references.each { |attribute, (ancestor, _)| kwargs[attribute] = visit(ancestor) }
|
63
|
+
|
64
|
+
node.type.factory.call(node.type, @context, *args, **kwargs)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Factrey
|
6
|
+
class Blueprint
|
7
|
+
# A node corresponds to an object to be created. A {Blueprint} consists of a set of nodes.
|
8
|
+
class Node
|
9
|
+
ANONYMOUS_NAME_PREFIX = "_anon_"
|
10
|
+
|
11
|
+
# @return [Symbol] name given to the object to be created
|
12
|
+
attr_reader :name
|
13
|
+
# @return [Type] type of the object
|
14
|
+
attr_reader :type
|
15
|
+
# @return [Array<Node>] list of ancestor nodes, from root to terminal nodes
|
16
|
+
attr_reader :ancestors
|
17
|
+
# @return [Array<Object>] positional arguments to be passed to the factory
|
18
|
+
attr_reader :args
|
19
|
+
# @return [Hash{Object => Object}] keyword arguments to be passed to the factory
|
20
|
+
attr_reader :kwargs
|
21
|
+
|
22
|
+
def initialize(name, type, ancestors: [], args: [], kwargs: {})
|
23
|
+
raise TypeError, "name must be a Symbol" if name && !name.is_a?(Symbol)
|
24
|
+
raise TypeError, "type must be a Blueprint::Type" unless type.is_a? Blueprint::Type
|
25
|
+
unless ancestors.is_a?(Array) && ancestors.all? { _1.is_a?(Node) }
|
26
|
+
raise TypeError, "ancestors must be an Array of Nodes"
|
27
|
+
end
|
28
|
+
raise TypeError, "args must be an Array" unless args.is_a? Array
|
29
|
+
raise TypeError, "kwargs must be a Hash" unless kwargs.is_a? Hash
|
30
|
+
|
31
|
+
@name = name || :"#{ANONYMOUS_NAME_PREFIX}#{SecureRandom.hex(6)}"
|
32
|
+
@type = type
|
33
|
+
@ancestors = ancestors
|
34
|
+
@args = args
|
35
|
+
@kwargs = kwargs
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Boolean]
|
39
|
+
def root? = ancestors.empty?
|
40
|
+
|
41
|
+
# @return [Boolean]
|
42
|
+
def anonymous? = name.start_with?(ANONYMOUS_NAME_PREFIX)
|
43
|
+
|
44
|
+
# Used for debugging and error reporting.
|
45
|
+
# @return [String]
|
46
|
+
def type_annotated_name = "#{name}(#{type.name})"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Factrey
|
4
|
+
class Blueprint
|
5
|
+
# A type representation on Factrey blueprints.
|
6
|
+
# This definition includes how the actual object is created ({#factory}) and
|
7
|
+
# what other types the object refers to ({#auto_references}).
|
8
|
+
class Type
|
9
|
+
# @return [Symbol] the name of this type. It is also used as the default object name at instantiation phase
|
10
|
+
attr_reader :name
|
11
|
+
# @return [Set<Symbol>] List of type names to be considered compatible with this type
|
12
|
+
attr_reader :compatible_types
|
13
|
+
# @return [Hash{Symbol => Symbol}] a name-to-attribute mapping for auto-referencing
|
14
|
+
attr_reader :auto_references
|
15
|
+
# @return [Proc] procedure that actually creates an object. See {Blueprint::Instantiator} implementation
|
16
|
+
attr_reader :factory
|
17
|
+
|
18
|
+
# @param name [Symbol]
|
19
|
+
# @param compatible_types [Array<Symbol>, Symbol]
|
20
|
+
# @param auto_references [Hash{Symbol => Symbol}, Array<Symbol>, Symbol]
|
21
|
+
# @yield [type, context, *args, **kwargs]
|
22
|
+
def initialize(name, compatible_types: [], auto_references: {}, &factory)
|
23
|
+
compatible_types = [compatible_types] if compatible_types.is_a? Symbol
|
24
|
+
auto_references = [auto_references] if auto_references.is_a? Symbol
|
25
|
+
auto_references = auto_references.to_h { [_1, _1] } if auto_references.is_a? Array
|
26
|
+
|
27
|
+
raise TypeError, "name must be a Symbol" unless name.is_a? Symbol
|
28
|
+
unless compatible_types.is_a?(Array) && compatible_types.all? { _1.is_a?(Symbol) }
|
29
|
+
raise TypeError, "compatible_types must be an Array of Symbols"
|
30
|
+
end
|
31
|
+
unless auto_references.is_a?(Hash) && auto_references.all? { |k, v| k.is_a?(Symbol) && v.is_a?(Symbol) }
|
32
|
+
raise TypeError, "auto_references must be a Hash containing Symbol keys and values"
|
33
|
+
end
|
34
|
+
raise ArgumentError, "factory must be provided" unless factory
|
35
|
+
|
36
|
+
compatible_types = [name] + compatible_types unless compatible_types.include? name
|
37
|
+
|
38
|
+
@name = name
|
39
|
+
@compatible_types = Set.new(compatible_types).freeze
|
40
|
+
@auto_references = auto_references.freeze
|
41
|
+
@factory = factory
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "blueprint/type"
|
4
|
+
require_relative "blueprint/node"
|
5
|
+
require_relative "blueprint/instantiator"
|
6
|
+
|
7
|
+
module Factrey
|
8
|
+
# Represents how to create a set of objects.
|
9
|
+
# {Blueprint} can be created and extended by the Blueprint DSL. See {Factrey.blueprint}.
|
10
|
+
class Blueprint
|
11
|
+
# @return [Hash{Symbol => Node}] a set of nodes
|
12
|
+
attr_reader :nodes
|
13
|
+
|
14
|
+
# Creates an empty blueprint.
|
15
|
+
def initialize
|
16
|
+
@nodes = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Blueprint]
|
20
|
+
def dup
|
21
|
+
result = self.class.new
|
22
|
+
|
23
|
+
nodes.each_value do |node|
|
24
|
+
result.add_node(
|
25
|
+
node.name,
|
26
|
+
node.type,
|
27
|
+
# This is OK since Hash insertion order in Ruby is retained
|
28
|
+
ancestors: node.ancestors.map { result.nodes[_1.name] },
|
29
|
+
args: node.args.dup,
|
30
|
+
kwargs: node.kwargs.dup,
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get the last root node.
|
38
|
+
# @return [Node, nil]
|
39
|
+
def representative_node = nodes.each_value.reverse_each.find(&:root?)
|
40
|
+
|
41
|
+
# Add a node. This method is used by {DSL} and usually does not need to be called directly.
|
42
|
+
# @return [Node]
|
43
|
+
def add_node(...)
|
44
|
+
node = Node.new(...)
|
45
|
+
raise ArgumentError, "duplicate node: #{node.name}" if nodes.member?(node.name)
|
46
|
+
|
47
|
+
nodes[node.name] = node
|
48
|
+
node
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a set of objects based on this blueprint.
|
52
|
+
# @param context [Object] context object to be passed to the factories
|
53
|
+
# @return [Hash{Symbol => Object}]
|
54
|
+
def instantiate(context = nil)
|
55
|
+
instantiator = Instantiator.new(context, self)
|
56
|
+
|
57
|
+
nodes.each_value { instantiator.visit(_1) }
|
58
|
+
|
59
|
+
instantiator.objects
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Factrey
|
4
|
+
class DSL
|
5
|
+
# An intermediate object for <code>let(:name).node(...)</code> notation. See {DSL#let}.
|
6
|
+
class Let < BasicObject
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# @param dsl [DSL]
|
10
|
+
# @param name [Symbol, nil]
|
11
|
+
def initialize(dsl, name)
|
12
|
+
@dsl = dsl
|
13
|
+
@name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
# @!visibility private
|
17
|
+
def respond_to_missing?(_method_name, _) = true
|
18
|
+
|
19
|
+
def method_missing(method_name, ...) = @dsl.let(@name) { @dsl.__send__(method_name, ...) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Factrey
|
4
|
+
class DSL
|
5
|
+
# An intermediate object for <code>on.name(...)</code> notation. See {DSL#on}.
|
6
|
+
class On < BasicObject
|
7
|
+
# @param dsl [DSL]
|
8
|
+
def initialize(dsl) = @dsl = dsl
|
9
|
+
|
10
|
+
# @!visibility private
|
11
|
+
def respond_to_missing?(_name, _) = true
|
12
|
+
|
13
|
+
def method_missing(name, ...) = @dsl.on(name, ...)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/factrey/dsl.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
require_relative "dsl/let"
|
7
|
+
require_relative "dsl/on"
|
8
|
+
|
9
|
+
module Factrey
|
10
|
+
# {Blueprint} DSL implementation.
|
11
|
+
class DSL
|
12
|
+
# Methods reserved for DSL.
|
13
|
+
RESERVED_METHODS = %i[
|
14
|
+
ref ext let let_default_name node on type args
|
15
|
+
__send__ __id__ nil? object_id class instance_exec initialize block_given? raise
|
16
|
+
].to_set.freeze
|
17
|
+
|
18
|
+
(instance_methods + private_instance_methods).each do |method|
|
19
|
+
undef_method(method) unless RESERVED_METHODS.include?(method)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param blueprint [Blueprint]
|
23
|
+
# @param ext [Object]
|
24
|
+
def initialize(blueprint:, ext:)
|
25
|
+
@blueprint = blueprint
|
26
|
+
@ext = ext
|
27
|
+
@ancestors = []
|
28
|
+
end
|
29
|
+
|
30
|
+
include Ref::ShorthandMethods
|
31
|
+
|
32
|
+
# @return [Object] the external object passed to {Factrey.blueprint}
|
33
|
+
attr_reader :ext
|
34
|
+
|
35
|
+
# By preceding <code>let(name).</code> to the declaration, give a name to the node.
|
36
|
+
# @param name [Symbol, nil] defaults to {Blueprint::Type#name} if omitted
|
37
|
+
# @return [Let]
|
38
|
+
# @example
|
39
|
+
# bp =
|
40
|
+
# Factrey.blueprint do
|
41
|
+
# article # no meaningful name is given (See Blueprint::Node#anonymous?)
|
42
|
+
# let.article # named as article
|
43
|
+
# let(:article2).article # named as article2
|
44
|
+
# end
|
45
|
+
# bp.instantiate #=> { article: ..., article2: ..., ... }
|
46
|
+
def let(name = nil, &)
|
47
|
+
raise TypeError, "name must be a Symbol" if name && !name.is_a?(Symbol)
|
48
|
+
raise ArgumentError, "nested let" if @let_scope
|
49
|
+
|
50
|
+
let = Let.new(self, name)
|
51
|
+
return let unless block_given?
|
52
|
+
|
53
|
+
@let_scope = let
|
54
|
+
ret = yield
|
55
|
+
@let_scope = nil
|
56
|
+
ret
|
57
|
+
end
|
58
|
+
|
59
|
+
# Overrides the default name given by {#let}.
|
60
|
+
#
|
61
|
+
# This method does nothing if it is not preceded by {#let}.
|
62
|
+
# @param name [Symbol]
|
63
|
+
# @return [Let, Blueprint]
|
64
|
+
# @example
|
65
|
+
# class Factrey::DSL do
|
66
|
+
# # Define a shortcut method for user(:admin)
|
67
|
+
# def admin_user(...) = let_default_name(:admin_user).user(:admin, ...)
|
68
|
+
# end
|
69
|
+
# Factrey.blueprint do
|
70
|
+
# admin_user # no meaningful name is given (See Blueprint::Node#anonymous?)
|
71
|
+
# let.admin_user # named as admin_user
|
72
|
+
# let(:user2).admin_user # named as user2
|
73
|
+
# end
|
74
|
+
def let_default_name(name, &)
|
75
|
+
raise TypeError, "name must be a Symbol" unless name.is_a?(Symbol)
|
76
|
+
|
77
|
+
if @let_scope && @let_scope.name.nil?
|
78
|
+
@let_scope = nil # consumed
|
79
|
+
|
80
|
+
let(name, &)
|
81
|
+
else
|
82
|
+
return self unless block_given?
|
83
|
+
|
84
|
+
yield
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Add a node to the blueprint.
|
89
|
+
#
|
90
|
+
# This method is usually not called directly. Use the shorthand method defined by {.add_type} instead.
|
91
|
+
# @param type [Blueprint::Type]
|
92
|
+
def node(type, ...)
|
93
|
+
name = @let_scope ? (@let_scope.name || type.name) : nil
|
94
|
+
@let_scope = nil # consumed
|
95
|
+
|
96
|
+
node = @blueprint.add_node(name, type, ancestors: @ancestors)
|
97
|
+
on(node.name, ...)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Enter the node to configure arguments and child nodes.
|
101
|
+
# @example
|
102
|
+
# Factrey.blueprint do
|
103
|
+
# let.blog do
|
104
|
+
# let(:article1).article
|
105
|
+
# let(:article2).article
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# # Add article to `blog`
|
109
|
+
# on.blog { let(:article3).article }
|
110
|
+
# # Add title to `article2`
|
111
|
+
# on.article2(title: "This is an article 2")
|
112
|
+
# end
|
113
|
+
def on(name = nil, ...)
|
114
|
+
return On.new(self) if name.nil? && !block_given?
|
115
|
+
|
116
|
+
node = @blueprint.nodes[name]
|
117
|
+
raise ArgumentError, "unknown node: #{name}" unless node
|
118
|
+
|
119
|
+
stashed_ancestors = @ancestors
|
120
|
+
@ancestors = node.ancestors + [node]
|
121
|
+
args(...)
|
122
|
+
@ancestors = stashed_ancestors
|
123
|
+
node
|
124
|
+
end
|
125
|
+
|
126
|
+
# Add arguments to the current node.
|
127
|
+
# @example
|
128
|
+
# Factrey.blueprint do
|
129
|
+
# let.blog
|
130
|
+
#
|
131
|
+
# # The following two lines are equivalent:
|
132
|
+
# on.blog { args :premium, title: "Who-ha" }
|
133
|
+
# on.blog(:premium, title: "Who-ha")
|
134
|
+
# end
|
135
|
+
def args(*args, **kwargs)
|
136
|
+
raise NameError, "Cannot use args at toplevel" if @ancestors.empty?
|
137
|
+
|
138
|
+
@ancestors.last.args.concat(args)
|
139
|
+
@ancestors.last.kwargs.update(kwargs)
|
140
|
+
yield if block_given?
|
141
|
+
end
|
142
|
+
|
143
|
+
class << self
|
144
|
+
# @return [Hash{Symbol => Type}] the types defined in this DSL
|
145
|
+
def types
|
146
|
+
@types ||= {}
|
147
|
+
end
|
148
|
+
|
149
|
+
# Add a new type that will be available in this DSL.
|
150
|
+
# A helper method with the same name as the type name is also defined in the DSL. For example,
|
151
|
+
# if you have added the <code>foo</code> type, you can declare node with <code>#foo</code>.
|
152
|
+
#
|
153
|
+
# {.add_type} is called automatically when you use <code>factory_bot-blueprint</code> gem.
|
154
|
+
# @param type [Blueprint::Type] blueprint type
|
155
|
+
# @example
|
156
|
+
# factory = ->(type, _ctx, *args, **kwargs) { FactoryBot.create(type.name, *args, **kwargs) }
|
157
|
+
# Factrey::DSL.add_type(Factrey::Blueprint::Type.new(:blog, &factory))
|
158
|
+
# Factrey::DSL.add_type(Factrey::Blueprint::Type.new(:article, auto_references: :blog, &factory))
|
159
|
+
#
|
160
|
+
# Factrey.blueprint do
|
161
|
+
# blog do
|
162
|
+
# article(title: "Article 1")
|
163
|
+
# article(title: "Article 2")
|
164
|
+
# end
|
165
|
+
# end
|
166
|
+
def add_type(type)
|
167
|
+
if RESERVED_METHODS.member? type.name
|
168
|
+
raise ArgumentError, "Cannot use reserved method name '#{type.name}' for type name"
|
169
|
+
end
|
170
|
+
|
171
|
+
if types.member? type.name
|
172
|
+
raise ArgumentError, "duplicate type definition: #{type.name}" if types[type.name] != type
|
173
|
+
|
174
|
+
return
|
175
|
+
end
|
176
|
+
|
177
|
+
types[type.name] = type
|
178
|
+
define_method(type.name) { |*args, **kwargs, &block| node(type, *args, **kwargs, &block) }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Factrey
|
4
|
+
class Ref
|
5
|
+
# An intermediate object for creating {Ref}s. See {ShorthandMethods#ref}.
|
6
|
+
class Builder < BasicObject
|
7
|
+
# @!visibility private
|
8
|
+
def respond_to_missing?(_name, _) = true
|
9
|
+
|
10
|
+
# @example
|
11
|
+
# ref = Factrey::Ref::Builder.new
|
12
|
+
# ref.hoge # same as Factrey::Ref.new(:hoge)
|
13
|
+
# @return [Ref]
|
14
|
+
def method_missing(name) = Ref.new(name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Factrey
|
4
|
+
class Ref
|
5
|
+
# A thin wrapper around {Proc} to represent the procedure using the results of the reference resolution.
|
6
|
+
# Each argument name is considered as a reference.
|
7
|
+
# These references are resolved and the results are passed to the {Proc}.
|
8
|
+
#
|
9
|
+
# {Ref}s and {Defer}s are usually created through {ShorthandMethods#ref}.
|
10
|
+
class Defer
|
11
|
+
# @return [Proc]
|
12
|
+
attr_reader :body
|
13
|
+
|
14
|
+
# @return [Array<Ref>]
|
15
|
+
def refs = @body.parameters.map { Ref.new(_1[1]) }
|
16
|
+
|
17
|
+
# @example
|
18
|
+
# Factrey::Ref::Defer.new { |foo, bar| foo + bar }
|
19
|
+
def initialize(&body)
|
20
|
+
body.parameters.all? { _1[0] == :req || _1[0] == :opt } or
|
21
|
+
raise ArgumentError, "block must take only fixed positional arguments"
|
22
|
+
|
23
|
+
@body = body
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Factrey
|
4
|
+
class Ref
|
5
|
+
# {Resolver} resolves {Ref}s and {Defer}s.
|
6
|
+
class Resolver
|
7
|
+
# @param recursion_limit [Integer, nil] how many recursions are allowed
|
8
|
+
# @yieldparam name [Symbol] the name of the reference to be resolved
|
9
|
+
def initialize(recursion_limit: nil, &handler)
|
10
|
+
@recursion_limit = recursion_limit
|
11
|
+
@handler = handler
|
12
|
+
end
|
13
|
+
|
14
|
+
# Traverse data recursively and resolve all {Ref}s and {Defer}s.
|
15
|
+
#
|
16
|
+
# This method supports recursive traversal for {Array} and {Hash}. For other structures, consider using {Defer}.
|
17
|
+
# @param object [Object]
|
18
|
+
# @param recursion_count [Integer]
|
19
|
+
def resolve(object, recursion_count: 0)
|
20
|
+
return object if !@recursion_limit.nil? && @recursion_limit < recursion_count
|
21
|
+
|
22
|
+
recursion_count += 1
|
23
|
+
case object
|
24
|
+
when Array
|
25
|
+
object.map { resolve(_1, recursion_count:) }
|
26
|
+
when Hash
|
27
|
+
object.to_h { |key, value| [resolve(key, recursion_count:), resolve(value, recursion_count:)] }
|
28
|
+
when Ref
|
29
|
+
@handler.call(object.name)
|
30
|
+
when Defer
|
31
|
+
object.body.call(*object.refs.map { resolve(_1) })
|
32
|
+
else
|
33
|
+
object
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Factrey
|
4
|
+
class Ref
|
5
|
+
# Provides shorthand methods for creating {Ref}s and {Defer}s.
|
6
|
+
module ShorthandMethods
|
7
|
+
# @return [Ref, Builder, Defer]
|
8
|
+
# @example
|
9
|
+
# include Factrey::Ref::ShorthandMethods
|
10
|
+
#
|
11
|
+
# # `ref(symbol)` returns a `Ref` instance
|
12
|
+
# ref(:foo)
|
13
|
+
# # `ref` returns a `Ref::Builder` instance; thus, we can write
|
14
|
+
# ref.foo
|
15
|
+
# # `ref { ... }` returns a `Ref::Defer` instance
|
16
|
+
# ref { |foo, bar| foo + bar }
|
17
|
+
def ref(name = nil, &)
|
18
|
+
if name
|
19
|
+
raise ArgumentError, "both name and block given" if block_given?
|
20
|
+
|
21
|
+
Ref.new(name)
|
22
|
+
elsif block_given?
|
23
|
+
Defer.new(&)
|
24
|
+
else
|
25
|
+
Builder.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/factrey/ref.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ref/builder"
|
4
|
+
require_relative "ref/defer"
|
5
|
+
require_relative "ref/resolver"
|
6
|
+
require_relative "ref/shorthand_methods"
|
7
|
+
|
8
|
+
module Factrey
|
9
|
+
# Represents a reference that can be embedded in the data and resolved later.
|
10
|
+
#
|
11
|
+
# {Ref}s and {Defer}s are usually created through {ShorthandMethods#ref}.
|
12
|
+
# @example
|
13
|
+
# # Some data containing several references:
|
14
|
+
# include Factrey::Ref::ShorthandMethods
|
15
|
+
# some_data1 = [12, ref.foo, 34, ref.bar, 56]
|
16
|
+
# some_data2 = { foo: ref.foo, foobar: ref { |foo, bar| foo + bar } }
|
17
|
+
#
|
18
|
+
# # Resolve references by a `mapping` hash table:
|
19
|
+
# mapping = { foo: 'hello', bar: 'world' }
|
20
|
+
# resolver = Factrey::Ref::Resolver.new { mapping.fetch(_1) }
|
21
|
+
# resolver.resolve(some_data1) #=> [12, 'hello', 34, 'world', 56]
|
22
|
+
# resolver.resolve(some_data2) #=> { foo: 'hello', foobar: 'helloworld' }
|
23
|
+
class Ref
|
24
|
+
# @return [Symbol]
|
25
|
+
attr_reader :name
|
26
|
+
|
27
|
+
# @param name [Symbol]
|
28
|
+
def initialize(name)
|
29
|
+
raise TypeError, "name must be a Symbol" unless name.is_a?(Symbol)
|
30
|
+
|
31
|
+
@name = name
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(other) = other.is_a?(Ref) && other.name == name
|
35
|
+
|
36
|
+
def eql?(other) = self == other
|
37
|
+
|
38
|
+
def hash = name.hash
|
39
|
+
end
|
40
|
+
end
|
data/lib/factrey.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "factrey/version"
|
4
|
+
require_relative "factrey/ref"
|
5
|
+
require_relative "factrey/blueprint"
|
6
|
+
require_relative "factrey/dsl"
|
7
|
+
|
8
|
+
# Factrey provides a declarative DSL to represent the creation plan of objects.
|
9
|
+
module Factrey
|
10
|
+
class << self
|
11
|
+
# Entry point to build or extend a {Blueprint}.
|
12
|
+
# @param blueprint [Blueprint, nil] to extend an existing blueprint
|
13
|
+
# @param ext [Object] an external object that can be accessed using {DSL#ext} in the DSL
|
14
|
+
# @param dsl [Class<DSL>] which DSL is used
|
15
|
+
# @yield Write Blueprint DSL code here. See {DSL} methods for DSL details
|
16
|
+
# @return [Blueprint] the built or extended blueprint
|
17
|
+
# @example
|
18
|
+
# bp =
|
19
|
+
# Factrey.blueprint do
|
20
|
+
# let.blog do
|
21
|
+
# article(title: "Article 1", body: "...")
|
22
|
+
# article(title: "Article 2", body: "...")
|
23
|
+
# article(title: "Article 3", body: "...") do
|
24
|
+
# comment(name: "John", body: "...")
|
25
|
+
# comment(name: "Doe", body: "...")
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# instance = bp.instantiate
|
31
|
+
# # This creates...
|
32
|
+
# # - a blog (can be accessed by `instance[:blog]`)
|
33
|
+
# # - with three articles
|
34
|
+
# # - and two comments to the last article
|
35
|
+
def blueprint(blueprint = nil, ext: nil, dsl: DSL, &)
|
36
|
+
raise TypeError, "blueprint must be a Blueprint" if blueprint && !blueprint.is_a?(Blueprint)
|
37
|
+
raise TypeError, "dsl must be a subclass of DSL" unless dsl <= DSL
|
38
|
+
|
39
|
+
blueprint ||= Blueprint.new
|
40
|
+
dsl.new(blueprint:, ext:).instance_exec(&) if block_given?
|
41
|
+
blueprint
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: factrey
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- yubrot
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-07-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
Factrey provides a declarative DSL to represent the creation plan of objects, for FactoryBot::Blueprint.
|
15
|
+
The name Factrey is derived from the words factory and tree.
|
16
|
+
email:
|
17
|
+
- yubrot@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- LICENSE.txt
|
23
|
+
- README.md
|
24
|
+
- lib/factrey.rb
|
25
|
+
- lib/factrey/blueprint.rb
|
26
|
+
- lib/factrey/blueprint/instantiator.rb
|
27
|
+
- lib/factrey/blueprint/node.rb
|
28
|
+
- lib/factrey/blueprint/type.rb
|
29
|
+
- lib/factrey/dsl.rb
|
30
|
+
- lib/factrey/dsl/let.rb
|
31
|
+
- lib/factrey/dsl/on.rb
|
32
|
+
- lib/factrey/ref.rb
|
33
|
+
- lib/factrey/ref/builder.rb
|
34
|
+
- lib/factrey/ref/defer.rb
|
35
|
+
- lib/factrey/ref/resolver.rb
|
36
|
+
- lib/factrey/ref/shorthand_methods.rb
|
37
|
+
- lib/factrey/version.rb
|
38
|
+
homepage: https://github.com/yubrot/factory_bot-blueprint
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata:
|
42
|
+
homepage_uri: https://github.com/yubrot/factory_bot-blueprint
|
43
|
+
source_code_uri: https://github.com/yubrot/factory_bot-blueprint
|
44
|
+
changelog_uri: https://github.com/yubrot/factory_bot-blueprint/blob/main/CHANGELOG.md
|
45
|
+
rubygems_mfa_required: 'true'
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.1.0
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.5.11
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Provides a declarative DSL to represent the creation plan of objects
|
65
|
+
test_files: []
|