factory_bot-with 0.4.0 → 0.6.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: 880e6e75c055c95bf4c8afab93288afaeb01f1a8ea7d162ecec04c3adaba1498
4
- data.tar.gz: d59eb2dbc2c24256e1c1b4053d194de41378cdd3773c06375bf8617ae63af642
3
+ metadata.gz: ab9e29f43a809164761618a28339cd98f4c26060183045f79dcd52cc107ee3aa
4
+ data.tar.gz: 2c4814ce52ed94f4120077410519424a19aa8b590a2af571429aadb059f59687
5
5
  SHA512:
6
- metadata.gz: 74ee4b6d1718cfef30a3ddfaaca2486f854c8c6db5c4b1fa63c1947f2ae3f2430e8ec67afd8dac052ff049df4d61c7f091b66a0080d187e51e58c4795534b66f
7
- data.tar.gz: f01718b24e04fe024189d77cec858f0aad8ec88fac9de520ced36bf954863775e6c0ecb079d0831016fac2d863edec204c8a23bec82f304ecd2b8c4186c9ab5e
6
+ metadata.gz: 25a743163e0a1f248d0106534086ee928ecd4b46257985588287dfdf681a03309ea5f4c6db40bf9c5591aad3c2ba78edb80b071752fa992d7935b458b25bbf40
7
+ data.tar.gz: d01e0dcecfa6b765a7510f581d6197f324cfd464bb0f6c939477f067fb1dd76675c46c898c7d7759b3923477162ef161994cc60f86ddad5f9ae6f2a6be6956a7
data/.irbrc ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "factory_bot/with"
4
+ require_relative "spec/factories"
5
+
6
+ class << self
7
+ prepend FactoryBot::With::Methods
8
+ end
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.4.0]
3
+ ## [0.6.0] - 2025-04-11
4
+
5
+ - Added: Factory methods are now also provided as class methods of `FactoryBot::With`
6
+ - Fixed: Removed some internal methods from `FactoryBot::With`
7
+
8
+ ## [0.5.0] - 2025-03-20
9
+
10
+ - Added: When using `with` scope syntax, blocks can now take given objects as arguments
11
+ - Added: `with_list` also works as a scope syntax, but calls a block for each product of objects
12
+ - **Changed**: Passing blocks to factory methods behaves same as `with` scope syntax
13
+
14
+ ## [0.4.0] - 2025-03-15
4
15
 
5
16
  - Fixed: Improved error message for incorrect factory usage
6
17
  - Added: Added `with` scope syntax for automatic association resolution
data/README.md CHANGED
@@ -3,11 +3,11 @@
3
3
  [![](https://badge.fury.io/rb/factory_bot-with.svg)](https://badge.fury.io/rb/factory_bot-with)
4
4
  [![](https://github.com/yubrot/factory_bot-with/actions/workflows/main.yml/badge.svg)](https://github.com/yubrot/factory_bot-with/actions/workflows/main.yml)
5
5
 
6
- FactoryBot::With is a FactoryBot extension that wraps `FactoryBot::Syntax::Methods` to make it a little easier to use.
6
+ FactoryBot::With is a FactoryBot extension that enhances usability by wrapping factory methods.
7
7
 
8
8
  [FactoryBot における関連の扱いと、factory_bot-with gem を作った話 (Japanese)](https://zenn.dev/yubrot/articles/032447068e308e)
9
9
 
10
- For example, with these factories:
10
+ For example, given these factories:
11
11
 
12
12
  ```ruby
13
13
  FactoryBot.define do
@@ -26,18 +26,18 @@ create(:blog) do |blog|
26
26
  end
27
27
  ```
28
28
 
29
- FactoryBot::With allows you to write:
29
+ FactoryBot::With allows you to write like this:
30
30
 
31
31
  ```ruby
32
- create.blog(
33
- with.article(with.comment),
34
- with.article(with_list.comment(3)),
35
- )
32
+ create.blog do
33
+ create.article { create.comment }
34
+ create.article { create_list.comment(3) }
35
+ end
36
36
  ```
37
37
 
38
38
  ## Installation
39
39
 
40
- On your Gemfile:
40
+ Add the following line to your Gemfile:
41
41
 
42
42
  ```ruby
43
43
  gem "factory_bot-with"
@@ -54,28 +54,29 @@ RSpec.configure do |config|
54
54
  end
55
55
  ```
56
56
 
57
+ Alternatively, these factory methods are also provided as class methods of `FactoryBot::With`.
58
+
57
59
  ## What differs from `FactoryBot::Syntax::Methods`?
58
60
 
59
- ### Method style syntax
61
+ ### Method-style syntax
60
62
 
61
- FactoryBot::With overrides the behavior when factory methods are called without arguments.
63
+ FactoryBot::With overrides the behavior of factory methods called without arguments.
62
64
 
63
65
  ```ruby
64
- create(:foo, ...) # behaves in the same way as FactoryBot.create
65
- create # returns a Proxy (an intermadiate) object
66
+ create(:foo, ...) # normal usage
67
+ create # returns a Proxy (an intermediate) object
66
68
  create.foo(...) # is equivalent to create(:foo, ...)
67
- ```
68
69
 
69
- This applies to other factory methods such as `build_stubbed`, `create_list`, `with` (described later), etc. as well.
70
-
71
- ```ruby
70
+ # This also applies to other factory methods:
72
71
  build_stubbed.foo(...)
73
72
  create_list.foo(10, ...)
74
73
  ```
75
74
 
76
75
  ### Smarter interpretation of positional arguments
77
76
 
78
- FactoryBot::With adjusts the behavior of factory methods to accept `Hash`, `Array`, and falsy values (`false` or `nil`) as positional arguments.
77
+ FactoryBot::With allows factory methods to accept `Hash`, `Array`, and falsy values (`false` or `nil`) as positional arguments[^1].
78
+
79
+ [^1]: The idea for this behavior came from JavaScript libraries such as [clsx](https://github.com/lukeed/clsx).
79
80
 
80
81
  ```ruby
81
82
  create.foo({ title: "Recipe" }, is_new && %i[latest hot])
@@ -85,54 +86,57 @@ create.foo({ title: "Recipe" }, is_new && %i[latest hot])
85
86
 
86
87
  ### `with`, `with_pair`, and `with_list` operator
87
88
 
88
- FactoryBot::With provides a new operator `with` (and its family).
89
+ FactoryBot::With introduces new operators: `with` (and its family).
90
+
91
+ - `with(:factory_name, ...)`
92
+ - `with_pair(:factory_name, ...)`
93
+ - `with_list(:factory_name, number_of_items, ...)`
94
+
95
+ These operators produce a `With` instance. This instance can be passed as an argument to factory methods such as `build` or `create`:
89
96
 
90
97
  ```ruby
91
- with(:factory_name, ...)
92
- with_pair(:factory_name, ...)
93
- with_list(:factory_name, number_of_items, ...)
98
+ create.blog(with.article(with.comment))
94
99
  ```
95
100
 
96
- The result of this operator (`With` instance) can be passed as an argument to factory methods such as `build` or `create`, which can then create additional objects following the result of the factory.
101
+ When the factory method is called, it first collects and removes `With` arguments, then delegates the actual object creation to the standard FactoryBot factory method, and finally creates additional objects based on the factory definition. Above example is equivalent to:
97
102
 
98
103
  ```ruby
99
- create.blog(with.article)
100
- # is equivalent to ...
101
- blog = create.blog
102
- create.article(blog:)
104
+ _tmp1 = FactoryBot.create(:blog)
105
+ _tmp2 = FactoryBot.create(:article, blog: _tmp1)
106
+ _tmp3 = FactoryBot.create(:comment, article: _tmp2)
107
+ # Here, `blog: _tmp1` and `article: _tmp2` are automatically completed by AAR (described later)
103
108
  ```
104
109
 
105
- The overridden factory methods collect these `with` arguments before delegating object creation to the actual factory methods.
106
-
107
110
  <details>
108
- <summary>Automatic association resolution</summary>
111
+ <summary>Automatic Association Resolution (AAR)</summary>
109
112
 
110
- `with` automatically resolves references to ancestor objects based on the definition of the FactoryBot associations.
113
+ `with` automatically resolves references to ancestor objects based on the definition in your FactoryBot factories.
111
114
 
112
- This automatic resolution takes into account any [traits](https://thoughtbot.github.io/factory_bot/traits/summary.html) in the factories, [aliases](https://thoughtbot.github.io/factory_bot/sequences/aliases.html) in the factories, and [factory specifications](https://thoughtbot.github.io/factory_bot/associations/specifying-the-factory.html) in the associations.
115
+ This automatic resolution takes into account any [traits](https://thoughtbot.github.io/factory_bot/traits/summary.html), [aliases](https://thoughtbot.github.io/factory_bot/sequences/aliases.html), and [factory specifications](https://thoughtbot.github.io/factory_bot/associations/specifying-the-factory.html) in the definition.
113
116
 
114
117
  ```ruby
115
118
  FactoryBot.define do
116
119
  factory(:video)
117
120
  factory(:photo)
118
121
  factory(:tag) do
122
+ # `tag` potentially has an association on `taggable` field. `taggable` is either `video` or `photo`.
119
123
  trait(:for_video) { taggable factory: :video }
120
124
  trait(:for_photo) { taggable factory: :photo }
121
125
  end
122
126
  end
123
127
 
124
- create.video(with.tag(text: "latest")) # resolved as taggable: video
125
- create.photo(with.tag(text: "latest")) # ...
128
+ create.video(with.tag(text: "latest")) # resolved as `taggable: <created video object>`
129
+ create.photo(with.tag(text: "latest")) # resolved as `taggable: <created photo object>`
126
130
  ```
127
131
 
128
- Due to technical limitations, [inline associations](https://thoughtbot.github.io/factory_bot/associations/inline-definition.html) cannot be resolved.
132
+ Due to technical limitations, [inline associations](https://thoughtbot.github.io/factory_bot/associations/inline-definition.html) are not taken into account.
129
133
 
130
134
  </details>
131
135
 
132
136
  <details>
133
- <summary>Autocomplete fully-qualified factory name</summary>
137
+ <summary>Factory Name Completion (FNC)</summary>
134
138
 
135
- For a factory name that is prefixed by the parent object's factory name, the prefix can be omitted.
139
+ For a factory name that is prefixed by the ancestor object's factory name, the prefix can be omitted.
136
140
 
137
141
  ```ruby
138
142
  FactoryBot.define do
@@ -140,16 +144,90 @@ FactoryBot.define do
140
144
  factory(:blog_article) { blog }
141
145
  end
142
146
 
143
- create.blog(with.article) # autocomplete to :blog_article
147
+ create.blog(with.article) # completes to :blog_article
148
+ ```
149
+
150
+ </details>
151
+
152
+ ### Implicit context scope
153
+
154
+ FactoryBot::With factory methods can accept a block argument, just like standard FactoryBot. However, in FactoryBot::With, nested factory method calls within a block recognize ancestor objects. This means that nested factory method calls perform AAR and FNC in the same way as the `with` operator.
155
+
156
+ ```ruby
157
+ # Instead of writing:
158
+ create.blog(with.article(with.comment))
159
+ # You can write:
160
+ create.blog { create.article { create.comment } }
161
+ # ^ This works in the same way as:
162
+ create(:blog) do |blog|
163
+ create(:article, blog:) do |article|
164
+ create(:comment, article:)
165
+ end
166
+ end
167
+ ```
168
+
169
+ <details>
170
+ <summary>Incompatible behavior when calling <code>_list</code> or <code>_pair</code> factory methods with a block</summary>
171
+
172
+ To align the behavior with the `with_list` operator, there is [an incompatible behavior](./lib/factory_bot/with.rb#L121) compared to standard FactoryBot:
173
+
174
+ ```ruby
175
+ # This code creates a blog with 2 articles, each with a comment in standard FactoryBot:
176
+ # This does not work in FactoryBot::With!
177
+ create(:blog) do |blog|
178
+ create_list(:article, 2, blog:) do |articles| # yielded *once* with an array of articles
179
+ articles.each { |article| create(:comment, article:) }
180
+ end
181
+ end
182
+
183
+ # In FactoryBot::With, blocks are yielded for each object. So we must write like this:
184
+ create.blog do |blog|
185
+ create_list.article(2, blog:) do |article| # yielded *for each article*
186
+ create.comment(article:)
187
+ end
188
+ end
189
+
190
+ # Again, you can simplify this by (1)omitting the block or (2)using the `with` operator:
191
+ create.blog { create_list.article(2) { create.comment } } # (1)
192
+ create.blog(with_list.article(2, with.comment)) # (2)
144
193
  ```
145
194
 
195
+ If you want to avoid this incompatibility, you can use `Object#tap`.
196
+
146
197
  </details>
147
198
 
148
199
  ## Additional features
149
200
 
150
- ### `with` as a template
201
+ ### Implicit context scope with existing objects
151
202
 
152
- `with` can also be used stand-alone. Stand-alone `with` can be used in place of the factory name. It works as a template for factory method calls.
203
+ By calling `with` without positional arguments, but with keyword arguments that define the relationship between factory names and objects, along with a block, it creates a context scope where those objects become candidates for AAR and FNC.
204
+
205
+ ```ruby
206
+ let(:blog) { create.blog }
207
+
208
+ before do
209
+ with(blog:) do
210
+ # Just like `create.blog { ... }`,
211
+ # the `blog` object is available for AAR and FNC in the following `create.article` calls:
212
+ create.article(with.comment)
213
+ create.article(with_list.comment(3))
214
+ end
215
+ end
216
+ ```
217
+
218
+ `with_list` works similarly to `with`, except that it accepts arrays as its values:
219
+
220
+ ```ruby
221
+ blog = create.blog
222
+ articles = create_list.article(2, blog:)
223
+ with_list(article: articles) { create.comment } # yielded *for each article*
224
+ ```
225
+
226
+ </details>
227
+
228
+ ### `with` as a factory method call template
229
+
230
+ A `With` instance can also be used as a template for factory method calls.
153
231
 
154
232
  Instead of writing:
155
233
 
@@ -180,22 +258,6 @@ context "when published more than one year ago" do
180
258
  end
181
259
  ```
182
260
 
183
- ### `with` scope for automatic association resolution
184
-
185
- By calling `with` without positional arguments, but with keyword arguments that define the relationship between factory names and objects, along with a block, `factory_bot-with` creates a scope where those objects become candidates for automatic association resolution.
186
-
187
- ```ruby
188
- let(:blog) { create.blog }
189
-
190
- before do
191
- with(blog:) do
192
- # Just like when using create.blog, blog is automatically resolved when create.article is called
193
- create.article(with.comment)
194
- create.article(with_list.comment(3))
195
- end
196
- end
197
- ```
198
-
199
261
  ## Development
200
262
 
201
263
  ```bash
@@ -45,7 +45,7 @@ module FactoryBot
45
45
  # @param ancestors [Array<Array(AssocInfo, Object)>]
46
46
  # @param partial_factory_name [Symbol]
47
47
  # @return [Symbol]
48
- def autocomplete_fully_qualified_factory_name(ancestors, partial_factory_name)
48
+ def perform_factory_name_completion(ancestors, partial_factory_name)
49
49
  ancestors.each do |(ancestor_assoc_info, _)|
50
50
  ancestor_assoc_info.factory_names.each do |ancestor_factory_name|
51
51
  factory_name = :"#{ancestor_factory_name}_#{partial_factory_name}"
@@ -53,8 +53,8 @@ module FactoryBot
53
53
  end
54
54
  end
55
55
 
56
- # Attempt to resolve with the autocompleted names, then attempt to resolve with the original name.
57
- # If we want to avoid autocompletion, we should be able to simply use a factory such as build or create.
56
+ # Attempt to resolve with the completed names, then attempt to resolve with the original name.
57
+ # If we want to avoid completion, we should be able to simply use a factory such as build or create.
58
58
  return partial_factory_name if exists?(partial_factory_name)
59
59
 
60
60
  raise ArgumentError, "FactoryBot factory #{partial_factory_name} is not defined"
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FactoryBot
4
+ class With
5
+ # An internal class to implement implicit context scope.
6
+ class Scoped
7
+ class << self
8
+ # @!visibility private
9
+ # @return [Array<Array(AssocInfo, Object)>, nil]
10
+ def ancestors = Thread.current[:factory_bot_with_scoped_ancestors]
11
+
12
+ # @param ancestors [Array<Array(AssocInfo, Object)>]
13
+ def with_ancestors(ancestors, &)
14
+ tmp_ancestors = self.ancestors
15
+ Thread.current[:factory_bot_with_scoped_ancestors] = [*ancestors, *tmp_ancestors || []]
16
+ yield
17
+ ensure
18
+ Thread.current[:factory_bot_with_scoped_ancestors] = tmp_ancestors
19
+ end
20
+
21
+ # @!visibility private
22
+ # @param objects [{Symbol => Object}]
23
+ def with_objects(objects, &) = with_ancestors(objects.map { [AssocInfo.get(_1), _2] }, &)
24
+
25
+ # @!visibility private
26
+ def block(&block)
27
+ params = block.parameters
28
+ if params.any? { %i[req opt rest].include?(_1[0]) }
29
+ ->(objects) { with_objects(objects) { block.call(*objects.values) } }
30
+ elsif params.any? { %i[keyreq key keyrest].include?(_1[0]) }
31
+ ->(objects) { with_objects(objects) { block.call(**objects) } }
32
+ else
33
+ ->(objects) { with_objects(objects, &block) }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module FactoryBot
4
4
  class With
5
- VERSION = "0.4.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -4,11 +4,14 @@ require "factory_bot"
4
4
  require_relative "with/version"
5
5
  require_relative "with/proxy"
6
6
  require_relative "with/assoc_info"
7
+ require_relative "with/scoped"
7
8
  require_relative "with/methods"
8
9
 
9
10
  module FactoryBot
10
11
  # An intermediate state for <code>with</code> operator.
11
12
  class With
13
+ extend Methods
14
+
12
15
  # @return [:singular, :pair, :list]
13
16
  attr_reader :variation
14
17
  # @return [Symbol]
@@ -23,7 +26,7 @@ module FactoryBot
23
26
  attr_reader :block
24
27
 
25
28
  # @!visibility private
26
- def initialize(variation, factory_name, withes: [], traits: [], attrs: {}, &block)
29
+ def initialize(variation, factory_name, *, withes: [], traits: [], attrs: {}, &block)
27
30
  raise ArgumentError unless %i[singular pair list].include? variation
28
31
  raise TypeError unless factory_name.is_a? Symbol
29
32
  raise TypeError unless withes.is_a?(Array) && withes.all? { _1.is_a? self.class }
@@ -36,6 +39,8 @@ module FactoryBot
36
39
  @traits = traits
37
40
  @attrs = attrs
38
41
  @block = block
42
+
43
+ collect!(*)
39
44
  end
40
45
 
41
46
  # @!visibility private
@@ -46,7 +51,7 @@ module FactoryBot
46
51
  # @param other [With]
47
52
  # @return [With]
48
53
  def merge(other)
49
- raise TypeError, "oter must be an instance of #{self.class}" unless other.is_a? self.class
54
+ raise TypeError, "other must be an instance of #{self.class}" unless other.is_a? self.class
50
55
  raise ArgumentError, "other must have the same variation" if other.variation != variation
51
56
  raise ArgumentError, "other must have the same factory_name" if other.factory_name != factory_name
52
57
 
@@ -63,40 +68,19 @@ module FactoryBot
63
68
  return first unless second
64
69
  return second unless first
65
70
 
66
- lambda do |*args|
67
- first.call(*args)
68
- second.call(*args)
71
+ proc do |arg|
72
+ first.call(arg)
73
+ second.call(arg)
69
74
  end
70
75
  end.call(block, other.block)
71
76
  )
72
77
  end
73
78
 
74
- # @!visibility private
75
- def extend!(*args)
76
- args.each do |arg|
77
- case arg
78
- when self.class
79
- withes << arg
80
- when Symbol, Numeric
81
- traits << arg
82
- when Array
83
- extend!(*arg)
84
- when Hash
85
- attrs.merge!(arg)
86
- when false, nil
87
- # Ignored. This behavior is useful for conditional arguments like `is_premium && :premium`
88
- else
89
- raise ArgumentError, "Unsupported type for factory argument: #{arg}"
90
- end
91
- end
92
- self
93
- end
94
-
95
79
  # @!visibility private
96
80
  # @param build_strategy [Symbol]
97
81
  # @param ancestors [Array<Array(AssocInfo, Object)>, nil]
98
82
  # @return [Object]
99
- def instantiate(build_strategy, ancestors = self.class.scoped_ancestors)
83
+ def instantiate(build_strategy, ancestors = Scoped.ancestors)
100
84
  return self if build_strategy == :with
101
85
 
102
86
  factory_bot_method =
@@ -104,38 +88,51 @@ module FactoryBot
104
88
  factory_name, attrs =
105
89
  if ancestors
106
90
  attrs = @attrs.dup
107
- factory_name = AssocInfo.autocomplete_fully_qualified_factory_name(ancestors, @factory_name)
91
+ factory_name = AssocInfo.perform_factory_name_completion(ancestors, @factory_name)
108
92
  AssocInfo.get(factory_name).perform_automatic_association_resolution(ancestors, attrs)
109
93
  [factory_name, attrs]
110
94
  else
111
95
  [@factory_name, @attrs]
112
96
  end
113
- result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs, &block)
97
+ result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs)
114
98
 
115
- unless withes.empty?
116
- parents = variation == :singular ? [result] : result
99
+ if block || !withes.empty?
117
100
  assoc_info = AssocInfo.get(factory_name)
101
+ parents = variation == :singular ? [result] : result
118
102
  parents.each do |parent|
119
103
  ancestors_for_children = [[assoc_info, parent], *ancestors || []]
120
104
  withes.each { _1.instantiate(build_strategy, ancestors_for_children) }
105
+ # We call the block for each parent object. This is an incompatible behavior with FactoryBot!
106
+ # If you want to avoid this, use `Object#tap` manually.
107
+ Scoped.with_ancestors(ancestors_for_children) { block.call(result) } if block
121
108
  end
122
109
  end
123
110
 
124
111
  result
125
112
  end
126
113
 
127
- class << self
128
- # @!visibility private
129
- # @param variation [:singular, :pair, :list]
130
- # @param factory [Symbol, With]
131
- # @param args [Array<Object>]
132
- # @param kwargs [{Symbol => Object}]
133
- def build(variation, factory, *, **, &)
134
- return factory.merge(build(variation, factory.factory_name, *, **, &)) if factory.is_a? self
135
-
136
- new(variation, factory, &).extend!(*, { ** })
114
+ private
115
+
116
+ def collect!(*args)
117
+ args.each do |arg|
118
+ case arg
119
+ when self.class
120
+ withes << arg
121
+ when Symbol, Numeric
122
+ traits << arg
123
+ when Array
124
+ collect!(*arg)
125
+ when Hash
126
+ attrs.merge!(arg)
127
+ when false, nil
128
+ # Ignored. This behavior is useful for conditional arguments like `is_premium && :premium`
129
+ else
130
+ raise ArgumentError, "Unsupported type for factory argument: #{arg}"
131
+ end
137
132
  end
133
+ end
138
134
 
135
+ class << self
139
136
  # If you want to use a custom strategy, call this along with <code>FactoryBot.register_strategy</code>.
140
137
  # @param build_strategy [Symbol]
141
138
  # @example
@@ -148,40 +145,30 @@ module FactoryBot
148
145
  list: :"#{build_strategy}_list",
149
146
  }.each do |variation, method_name|
150
147
  Methods.define_method(method_name) do |factory = nil, *args, **kwargs, &block|
151
- if factory
148
+ if factory.is_a? With
149
+ # <__method__>(<with_instance>, ...)
150
+ factory
151
+ .merge(With.new(variation, factory.factory_name, *args, attrs: kwargs, &block))
152
+ .instantiate(build_strategy)
153
+ elsif factory
152
154
  # <__method__>(<factory_name>, ...)
153
- With.build(variation, factory, *args, **kwargs, &block).instantiate(build_strategy)
155
+ With.new(variation, factory, *args, attrs: kwargs, &block).instantiate(build_strategy)
154
156
  elsif args.empty? && kwargs.empty? && !block
155
157
  # <__method__>.<factory_name>(...)
156
158
  Proxy.new(self, __method__)
157
- elsif __method__ == :with && args.empty? && !kwargs.empty?
159
+ elsif __method__ == :with && args.empty? && !kwargs.empty? && block
158
160
  # with(<factory_name>: <object>, ...) { ... }
159
- With.with_scoped_ancestors(kwargs, &block)
161
+ Scoped.block(&block).call(kwargs)
162
+ elsif __method__ == :with_list && args.empty? && !kwargs.empty? && block
163
+ # with_list(<factory_name>: [<object>, ...], ...) { ... }
164
+ block = Scoped.block(&block)
165
+ kwargs.values.inject(:product).map { block.call(kwargs.keys.zip(_1).to_h) }
160
166
  else
161
167
  raise ArgumentError, "Invalid use of #{__method__}"
162
168
  end
163
169
  end
164
170
  end
165
171
  end
166
-
167
- # @!visibility private
168
- # @param objects [{Symbol => Object}]
169
- def with_scoped_ancestors(objects)
170
- return unless block_given?
171
-
172
- tmp_scoped_ancestors = scoped_ancestors
173
- Thread.current[:factory_bot_with_scoped_ancestors] = [
174
- *objects.map { [AssocInfo.get(_1), _2] },
175
- *tmp_scoped_ancestors || [],
176
- ]
177
- result = yield
178
- Thread.current[:factory_bot_with_scoped_ancestors] = tmp_scoped_ancestors
179
- result
180
- end
181
-
182
- # @!visibility private
183
- # @return [Array<Array(AssocInfo, Object)>, nil]
184
- def scoped_ancestors = Thread.current[:factory_bot_with_scoped_ancestors]
185
172
  end
186
173
 
187
174
  %i[build build_stubbed create attributes_for with].each { register_strategy _1 }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factory_bot-with
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yubrot
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-15 00:00:00.000000000 Z
10
+ date: 2025-04-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: factory_bot
@@ -23,8 +23,7 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '6.0'
26
- description: FactoryBot extension that wraps FactoryBot::Syntax::Methods to make it
27
- a little easier to use
26
+ description: a FactoryBot extension that enhances usability by wrapping factory methods
28
27
  email:
29
28
  - yubrot@gmail.com
30
29
  executables: []
@@ -33,6 +32,7 @@ extra_rdoc_files: []
33
32
  files:
34
33
  - ".github/workflows/main.yml"
35
34
  - ".gitignore"
35
+ - ".irbrc"
36
36
  - ".solargraph.yml"
37
37
  - CHANGELOG.md
38
38
  - CODE_OF_CONDUCT.md
@@ -42,6 +42,7 @@ files:
42
42
  - lib/factory_bot/with/assoc_info.rb
43
43
  - lib/factory_bot/with/methods.rb
44
44
  - lib/factory_bot/with/proxy.rb
45
+ - lib/factory_bot/with/scoped.rb
45
46
  - lib/factory_bot/with/version.rb
46
47
  homepage: https://github.com/yubrot/factory_bot-with
47
48
  licenses:
@@ -67,6 +68,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
68
  requirements: []
68
69
  rubygems_version: 3.6.2
69
70
  specification_version: 4
70
- summary: FactoryBot extension that wraps FactoryBot::Syntax::Methods to make it a
71
- little easier to use
71
+ summary: a FactoryBot extension that enhances usability by wrapping factory methods
72
72
  test_files: []