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 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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factrey
4
+ VERSION = "0.1.0"
5
+ 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: []