factrey 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c99d12d76f3bb5c00381aa92c12fa7590256312a4bc44af811b6ebfcc379ac2a
4
- data.tar.gz: 62cfdf298cfa82d3341d26e140e57d28660376ed810142b54486650deb37ecd4
3
+ metadata.gz: 16dba45f9a86cdb78925559b08fe048a8cb1b8004a5206e2a103aa8df2889fe3
4
+ data.tar.gz: e84a1b67bce17d637837ecc3e1882e1b7dce4b96267652cc0e23c8bc97c46441
5
5
  SHA512:
6
- metadata.gz: 68317b56351965174b8ae361c2a68e9d125359f8d9f2ec3203b6892391cacb8c9a2db6e4056c2e13531c8ffbb4a47d141bed74662327b170da55de6852fcc806
7
- data.tar.gz: 3705848394bf3870e84505241ecfbd7075813c558d9b377848c60b6bb939a8cda789edd3bc5d0cf24087bd7847daf4f1fb5fe62ba9f7e2da9dd240f470b9e9d9
6
+ metadata.gz: 001c21f917a7c52d76191005a127889f6ed067c0d440d4f42b6879e252224b0ffa0e585a7458ba999ec753feb7e03b4bc29c79d8f35fe0265450e56ffd48b285
7
+ data.tar.gz: 45e4982f36da43ff79aa9dcb316b82c8551f30034ba223aee39a321b19f01f0deddbb7ac901e842fd60ded4bd9ac26a04e2b58097b0f94f713a5a035e16a6103
@@ -23,11 +23,6 @@ module Factrey
23
23
  @objects
24
24
  end
25
25
 
26
- def instantiate_result
27
- instantiate_objects # To keep consistency in the order of instantiation
28
- resolver.resolve(@blueprint.result)
29
- end
30
-
31
26
  private
32
27
 
33
28
  # @param node [Node]
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "set"
4
+ require "securerandom"
4
5
 
5
6
  module Factrey
6
7
  class Blueprint
7
8
  # A node corresponds to an object to be created. A {Blueprint} consists of a set of nodes.
8
9
  class Node
10
+ # A name prefix given to anonymous nodes for convenience.
9
11
  ANONYMOUS_NAME_PREFIX = "_anon_"
12
+ # Name used for the node that hold the results of the blueprint.
13
+ RESULT_NAME = :_result_
10
14
 
11
15
  # @return [Symbol] name given to the object to be created
12
16
  attr_reader :name
@@ -35,14 +39,31 @@ module Factrey
35
39
  @kwargs = kwargs
36
40
  end
37
41
 
38
- # @return [Ref]
39
- def to_ref = Ref.new(name)
42
+ # @param name [Symbol, nil]
43
+ # @param value [Object]
44
+ # @param ancestors [Array<Node>]
45
+ def self.computed(name, value, ancestors: [])
46
+ new(name, Blueprint::Type::COMPUTED, ancestors:, args: [value])
47
+ end
40
48
 
41
49
  # @return [Boolean]
42
- def root? = ancestors.empty?
50
+ def anonymous? = name.start_with?(ANONYMOUS_NAME_PREFIX)
43
51
 
44
52
  # @return [Boolean]
45
- def anonymous? = name.start_with?(ANONYMOUS_NAME_PREFIX)
53
+ def result? = name == RESULT_NAME
54
+
55
+ # @return [Ref] the reference to this node
56
+ def to_ref = Ref.new(name)
57
+
58
+ # @return [Ref, nil] if this node works as an alias to another node, return the reference to the node
59
+ def alias_ref
60
+ case [@type, args]
61
+ in Blueprint::Type::COMPUTED, [Ref => ref]
62
+ ref
63
+ else
64
+ nil
65
+ end
66
+ end
46
67
 
47
68
  # Used for debugging and error reporting.
48
69
  # @return [String]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module Factrey
4
6
  class Blueprint
5
7
  # A type representation on Factrey blueprints.
@@ -40,6 +42,9 @@ module Factrey
40
42
  @auto_references = auto_references.freeze
41
43
  @factory = factory
42
44
  end
45
+
46
+ # A special type that represents values computed from other objects.
47
+ COMPUTED = Type.new(:_computed) { |_, _, arg| arg }
43
48
  end
44
49
  end
45
50
  end
@@ -10,8 +10,6 @@ module Factrey
10
10
  class Blueprint
11
11
  # @return [Hash{Symbol => Node}] a set of nodes
12
12
  attr_reader :nodes
13
- # @return [Object] the result of the DSL code is defined here
14
- attr_reader :result
15
13
 
16
14
  # Creates an empty blueprint.
17
15
  def initialize
@@ -23,7 +21,7 @@ module Factrey
23
21
  result = self.class.new
24
22
 
25
23
  nodes.each_value do |node|
26
- result.add_node(
24
+ new_node = Node.new(
27
25
  node.name,
28
26
  node.type,
29
27
  # This is OK since Hash insertion order in Ruby is retained
@@ -31,38 +29,39 @@ module Factrey
31
29
  args: node.args.dup,
32
30
  kwargs: node.kwargs.dup,
33
31
  )
32
+ result.add_node(new_node)
34
33
  end
35
34
 
36
35
  result
37
36
  end
38
37
 
39
38
  # Add a node. This method is used by {DSL} and usually does not need to be called directly.
39
+ # @param node [Node]
40
40
  # @return [Node]
41
- def add_node(...)
42
- node = Node.new(...)
41
+ def add_node(node)
43
42
  raise ArgumentError, "duplicate node: #{node.name}" if nodes.member?(node.name)
44
43
 
45
44
  nodes[node.name] = node
46
45
  node
47
46
  end
48
47
 
49
- # Define the result. This method is used by {DSL} and usually does not need to be called directly.
50
- # @param result [Object]
51
- # @param overwrite [Boolean] whether to overwrite the existing result
52
- def define_result(result, overwrite: false)
53
- return if defined?(@result) && !overwrite
48
+ # Resolve a node.
49
+ # @param name [Symbol]
50
+ # @param follow_alias [Boolean] whether to follow an alias node. See {Node#alias_ref}
51
+ # @return [Node, nil]
52
+ def resolve_node(name, follow_alias: true)
53
+ node = nodes[name]
54
+ return node unless follow_alias
54
55
 
55
- @result = result
56
+ ref = node&.alias_ref
57
+ ref ? resolve_node(ref.name) : node
56
58
  end
57
59
 
58
- # Create a set of objects and compute the result based on this blueprint.
60
+ # Create a set of objects based on this blueprint.
59
61
  # @param context [Object] context object to be passed to the factories
60
- # @return [(Object, {Symbol => Object})] the result and the created objects
62
+ # @return [Hash{Symbol => Object}] the created objects
61
63
  def instantiate(context = nil)
62
- instantiator = Instantiator.new(context, self)
63
- objects = instantiator.instantiate_objects
64
- result = instantiator.instantiate_result
65
- [result, objects]
64
+ Instantiator.new(context, self).instantiate_objects
66
65
  end
67
66
  end
68
67
  end
data/lib/factrey/dsl.rb CHANGED
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "set"
4
- require "securerandom"
5
-
6
- require_relative "dsl/let"
7
- require_relative "dsl/on"
8
4
 
9
5
  module Factrey
10
6
  # {Blueprint} DSL implementation.
11
7
  class DSL
12
8
  # Methods reserved for DSL.
13
9
  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
10
+ ref ext object_node computed_node let on args
11
+ __send__ __method__ __id__ nil? is_a? to_s inspect object_id class instance_eval instance_variables
12
+ initialize block_given? enum_for raise
16
13
  ].to_set.freeze
17
14
 
18
15
  (instance_methods + private_instance_methods).each do |method|
@@ -32,100 +29,81 @@ module Factrey
32
29
  # @return [Object] the external object passed to {Factrey.blueprint}
33
30
  attr_reader :ext
34
31
 
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]
32
+ # Add an object node to the blueprint.
33
+ #
34
+ # This method is usually not called directly. Use the shorthand method defined by {.add_type} instead.
35
+ # @param name [Symbol, nil]
36
+ # @param type [Blueprint::Type]
37
+ # @yieldparam ref [Ref]
38
+ # @return [Ref]
39
+ def object_node(name, type, ...)
40
+ node = @blueprint.add_node(Blueprint::Node.new(name, type, ancestors: @ancestors))
41
+ on(node.name, ...)
42
+ end
43
+
44
+ # Add a computed node to the blueprint.
45
+ #
46
+ # This method is usually not called directly. Use {#let} instead.
47
+ # @param name [Symbol, nil]
48
+ # @param value [Object]
49
+ def computed_node(name, value)
50
+ node = @blueprint.add_node(Blueprint::Node.computed(name, value, ancestors: @ancestors))
51
+ node.to_ref
52
+ end
53
+
54
+ # Define a computed node with name.
55
+ # @param setter_name [Symbol, nil] the setter name for the computed node
56
+ # @return [Ref, Proxy] returns a {Proxy} for <code>let.node_name = ...</code> notation if no argument is given
38
57
  # @example
39
58
  # bp =
40
59
  # 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
60
+ # article(title: "Foo") # object itself has no meaningful name (See Blueprint::Node#anonymous?)
61
+ # let.article = article(title: "Bar") # an alias `article` to the article object is defined
62
+ # let.article(title: "Bar") # We can omit `.node_name =` if the name is the same as the method name
63
+ # let.article2 = article(title: "Baz") # an alias `article2` to the article object is defined
44
64
  # end
45
65
  # 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)
66
+ def let(setter_name = nil, *args, **kwargs, &block)
67
+ return Proxy.new(self, __method__) unless setter_name
76
68
 
77
- if @let_scope && @let_scope.name.nil?
78
- @let_scope = nil # consumed
69
+ if setter_name.end_with? "="
70
+ raise ArgumentError, "Wrong setter use" if args.size != 1 || !kwargs.empty? || block
79
71
 
80
- let(name, &)
72
+ computed_node(setter_name[0..-2].to_sym, args[0])
81
73
  else
82
- return self unless block_given?
83
-
84
- yield
74
+ # `let.node_name(...)` is a shorthand for `let.node_name = node_name(...)`
75
+ let(:"#{setter_name}=", __send__(setter_name, *args, **kwargs, &block))
85
76
  end
86
77
  end
87
78
 
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
- # @yieldparam ref [Ref]
93
- # @return [Ref]
94
- def node(type, ...)
95
- name = @let_scope ? (@let_scope.name || type.name) : nil
96
- @let_scope = nil # consumed
97
-
98
- node = @blueprint.add_node(name, type, ancestors: @ancestors)
99
- on(node.name, ...)
100
- end
101
-
102
79
  # Enter the node to configure arguments and child nodes.
103
- # @yieldparam ref [Ref]
104
- # @return [Ref]
80
+ # @param node_name [Symbol, nil] the node name to enter
81
+ # @return [Ref, Proxy] returns a {Proxy} for <code>on.node_name(...)</code> notation if no argument is given
105
82
  # @example
106
83
  # Factrey.blueprint do
107
84
  # let.blog do
108
- # let(:article1).article
109
- # let(:article2).article
85
+ # let.article1 = article
86
+ # let.article2 = article
110
87
  # end
111
88
  #
112
89
  # # Add article to `blog`
113
- # on.blog { let(:article3).article }
90
+ # on.blog { let.article3 = article }
114
91
  # # Add title to `article2`
115
92
  # on.article2(title: "This is an article 2")
116
93
  # end
117
- def on(name = nil, *args, **kwargs)
118
- return On.new(self) if name.nil? && !block_given?
94
+ def on(node_name = nil, ...)
95
+ return Proxy.new(self, __method__) unless node_name
119
96
 
120
- node = @blueprint.nodes[name]
121
- raise ArgumentError, "unknown node: #{name}" unless node
97
+ node = @blueprint.resolve_node(node_name)
98
+ raise ArgumentError, "unknown node: #{node_name}" unless node
122
99
 
123
100
  stashed_ancestors = @ancestors
124
101
  @ancestors = node.ancestors + [node]
125
- args(*args, **kwargs)
126
- yield node.to_ref if block_given?
127
- @ancestors = stashed_ancestors
128
- node.to_ref
102
+ begin
103
+ args(...)
104
+ ensure
105
+ @ancestors = stashed_ancestors
106
+ end
129
107
  end
130
108
 
131
109
  # Add arguments to the current node.
@@ -138,10 +116,13 @@ module Factrey
138
116
  # on.blog(:premium, title: "Who-ha")
139
117
  # end
140
118
  def args(*args, **kwargs)
141
- raise NameError, "Cannot use args at toplevel" if @ancestors.empty?
119
+ raise NameError, "cannot use args at toplevel" if @ancestors.empty?
120
+ raise NameError, "cannot use args to computed nodes" if @ancestors.last.type == Blueprint::Type::COMPUTED
142
121
 
143
122
  @ancestors.last.args.concat(args)
144
123
  @ancestors.last.kwargs.update(kwargs)
124
+ yield @ancestors.last.to_ref if block_given?
125
+ @ancestors.last.to_ref
145
126
  end
146
127
 
147
128
  class << self
@@ -151,8 +132,8 @@ module Factrey
151
132
  end
152
133
 
153
134
  # Add a new type that will be available in this DSL.
154
- # A helper method with the same name as the type name is also defined in the DSL. For example,
155
- # if you have added the <code>foo</code> type, you can declare node with <code>#foo</code>.
135
+ # This method defines a helper method with the same name as the type name. For example,
136
+ # if you have added the <code>foo</code> type, you can declare an object node with <code>#foo</code>.
156
137
  #
157
138
  # {.add_type} is called automatically when you use <code>factory_bot-blueprint</code> gem.
158
139
  # @param type [Blueprint::Type] blueprint type
@@ -179,7 +160,7 @@ module Factrey
179
160
  end
180
161
 
181
162
  types[type.name] = type
182
- define_method(type.name) { |*args, **kwargs, &block| node(type, *args, **kwargs, &block) }
163
+ define_method(type.name) { |*args, **kwargs, &block| object_node(nil, type, *args, **kwargs, &block) }
183
164
  end
184
165
  end
185
166
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factrey
4
+ # An intermediate object to provide some notation combined with <code>method_missing</code>.
5
+ # @example
6
+ # class Foo
7
+ # def foo(name = nil)
8
+ # return Factrey::Proxy.new(self, __method__) unless name
9
+ #
10
+ # name
11
+ # end
12
+ # end
13
+ #
14
+ # Foo.new.foo.bar #=> :bar
15
+ class Proxy < BasicObject
16
+ # @param receiver [Object]
17
+ # @param method [Symbol]
18
+ def initialize(receiver, method)
19
+ @receiver = receiver
20
+ @method = method
21
+ end
22
+
23
+ # @!visibility private
24
+ def respond_to_missing?(_method_name, _) = true
25
+
26
+ def method_missing(method_name, ...)
27
+ @receiver.__send__(@method, method_name, ...)
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Factrey
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/factrey.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "factrey/version"
4
4
  require_relative "factrey/ref"
5
+ require_relative "factrey/proxy"
5
6
  require_relative "factrey/blueprint"
6
7
  require_relative "factrey/dsl"
7
8
 
@@ -36,8 +37,12 @@ module Factrey
36
37
  raise TypeError, "blueprint must be a Blueprint" if blueprint && !blueprint.is_a?(Blueprint)
37
38
  raise TypeError, "dsl must be a subclass of DSL" unless dsl <= DSL
38
39
 
40
+ is_extending = !blueprint.nil?
39
41
  blueprint ||= Blueprint.new
40
- blueprint.define_result dsl.new(blueprint:, ext:).instance_exec(&) if block_given?
42
+
43
+ result = block_given? ? dsl.new(blueprint:, ext:).instance_eval(&) : nil
44
+ blueprint.add_node(Blueprint::Node.computed(Blueprint::Node::RESULT_NAME, result)) unless is_extending
45
+
41
46
  blueprint
42
47
  end
43
48
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factrey
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yubrot
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-29 00:00:00.000000000 Z
11
+ date: 2024-08-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Factrey provides a declarative DSL to represent the creation plan of objects, for FactoryBot::Blueprint.
@@ -27,8 +27,7 @@ files:
27
27
  - lib/factrey/blueprint/node.rb
28
28
  - lib/factrey/blueprint/type.rb
29
29
  - lib/factrey/dsl.rb
30
- - lib/factrey/dsl/let.rb
31
- - lib/factrey/dsl/on.rb
30
+ - lib/factrey/proxy.rb
32
31
  - lib/factrey/ref.rb
33
32
  - lib/factrey/ref/builder.rb
34
33
  - lib/factrey/ref/defer.rb
@@ -1,22 +0,0 @@
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
@@ -1,16 +0,0 @@
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