factory_bot-with 0.4.0 → 0.5.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: fa2396f8ce3d44a17caacc293e2b5914b781eb167c50de541c2950b6f3e8ce0f
4
+ data.tar.gz: a5b0e4b93ba6f97439dd25290c2ab5c3832175799e7e45f538e0c9db28be14bf
5
5
  SHA512:
6
- metadata.gz: 74ee4b6d1718cfef30a3ddfaaca2486f854c8c6db5c4b1fa63c1947f2ae3f2430e8ec67afd8dac052ff049df4d61c7f091b66a0080d187e51e58c4795534b66f
7
- data.tar.gz: f01718b24e04fe024189d77cec858f0aad8ec88fac9de520ced36bf954863775e6c0ecb079d0831016fac2d863edec204c8a23bec82f304ecd2b8c4186c9ab5e
6
+ metadata.gz: a655f59ca867e26316fb08d3fd80e9352d4cefeb600c9d324de89358096a031e549acc5d98da43d67f608f615084bfc384a4c8d45f9e528891d4f90f9fe9de5f
7
+ data.tar.gz: 4709882eea9a1a90e6719a44133d66e1de9c189da2c602963f77b9c80b14952b99dbd2b350085d496f15cf70f20bbbce4edc1a5a37b2dcf9272d244be31d7589
data/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.4.0]
3
+ ## [0.5.0] - 2025-03-20
4
+
5
+ - Added: When using `with` scope syntax, blocks can now take given objects as arguments
6
+ - Added: `with_list` also works as a scope syntax, but calls a block for each product of objects
7
+ - **Changed**: Passing blocks to factory methods behaves same as `with` scope syntax
8
+
9
+ ## [0.4.0] - 2025-03-15
4
10
 
5
11
  - Fixed: Improved error message for incorrect factory usage
6
12
  - Added: Added `with` scope syntax for automatic association resolution
data/README.md CHANGED
@@ -62,7 +62,7 @@ FactoryBot::With overrides the behavior when factory methods are called without
62
62
 
63
63
  ```ruby
64
64
  create(:foo, ...) # behaves in the same way as FactoryBot.create
65
- create # returns a Proxy (an intermadiate) object
65
+ create # returns a Proxy (an intermediate) object
66
66
  create.foo(...) # is equivalent to create(:foo, ...)
67
67
  ```
68
68
 
@@ -147,6 +147,70 @@ create.blog(with.article) # autocomplete to :blog_article
147
147
 
148
148
  ## Additional features
149
149
 
150
+ ### `with` scope syntax for automatic association resolution
151
+
152
+ 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 scope where those objects become candidates for automatic association resolution.
153
+
154
+ ```ruby
155
+ let(:blog) { create.blog }
156
+
157
+ before do
158
+ with(blog:) do
159
+ # Just like when using `create.blog(with.article)`,
160
+ # `blog:` is completed automatically at each `create.article`
161
+ create.article(with.comment)
162
+ create.article(with_list.comment(3))
163
+ end
164
+ end
165
+ ```
166
+
167
+ The same behavior occurs when a block is passed to factory methods.
168
+
169
+ ```ruby
170
+ create.blog do
171
+ create.article(with.comment)
172
+ create.article(with_list.comment(3))
173
+ end
174
+ ```
175
+
176
+ <details>
177
+ <summary>Comparison</summary>
178
+
179
+ `_pair` and `_list` methods have [an incompatible behavior](./lib/factory_bot/with.rb#L121) with FactoryBot. If you want to avoid this, just use `Object#tap`.
180
+
181
+ ```ruby
182
+ # This creates a blog with 2 articles, each with 1 comment
183
+ # plain factory_bot:
184
+ create(:blog) do |blog|
185
+ create_list(:article, 2, blog:) do |articles| # yielded once with an array of articles in plain factory_bot
186
+ articles.each { |article| create(:comment, article:) }
187
+ end
188
+ end
189
+
190
+ # `with` operator:
191
+ create.blog(
192
+ with_list.article(2, with.comment)
193
+ )
194
+
195
+ # factory methods with blocks:
196
+ create.blog do
197
+ create_list.article(2) { create.comment } # yielded *for each article* in factory_bot-with
198
+ end
199
+
200
+ # with as a scope syntax for existing blog:
201
+ blog = create.blog
202
+ with(blog:) do
203
+ create_list.article(2) { create.comment }
204
+ end
205
+
206
+ # with_list can also be used as a scope syntax:
207
+ blog = create.blog
208
+ articles = create_list.article(2, blog:)
209
+ with_list(article: articles) { create.comment } # yielded *for each article*
210
+ ```
211
+
212
+ </details>
213
+
150
214
  ### `with` as a template
151
215
 
152
216
  `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.
@@ -180,22 +244,6 @@ context "when published more than one year ago" do
180
244
  end
181
245
  ```
182
246
 
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
247
  ## Development
200
248
 
201
249
  ```bash
@@ -2,6 +2,6 @@
2
2
 
3
3
  module FactoryBot
4
4
  class With
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -63,9 +63,9 @@ module FactoryBot
63
63
  return first unless second
64
64
  return second unless first
65
65
 
66
- lambda do |*args|
67
- first.call(*args)
68
- second.call(*args)
66
+ proc do |arg|
67
+ first.call(arg)
68
+ second.call(arg)
69
69
  end
70
70
  end.call(block, other.block)
71
71
  )
@@ -110,14 +110,17 @@ module FactoryBot
110
110
  else
111
111
  [@factory_name, @attrs]
112
112
  end
113
- result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs, &block)
113
+ result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs)
114
114
 
115
- unless withes.empty?
116
- parents = variation == :singular ? [result] : result
115
+ if block || !withes.empty?
117
116
  assoc_info = AssocInfo.get(factory_name)
117
+ parents = variation == :singular ? [result] : result
118
118
  parents.each do |parent|
119
119
  ancestors_for_children = [[assoc_info, parent], *ancestors || []]
120
120
  withes.each { _1.instantiate(build_strategy, ancestors_for_children) }
121
+ # We call the block for each parent object. This is an incompatible behavior with FactoryBot!
122
+ # If you want to avoid this, use `Object#tap` manually.
123
+ self.class.with_scoped_ancestors(ancestors_for_children) { block.call(result) } if block
121
124
  end
122
125
  end
123
126
 
@@ -154,9 +157,14 @@ module FactoryBot
154
157
  elsif args.empty? && kwargs.empty? && !block
155
158
  # <__method__>.<factory_name>(...)
156
159
  Proxy.new(self, __method__)
157
- elsif __method__ == :with && args.empty? && !kwargs.empty?
160
+ elsif __method__ == :with && args.empty? && !kwargs.empty? && block
158
161
  # with(<factory_name>: <object>, ...) { ... }
159
- With.with_scoped_ancestors(kwargs, &block)
162
+ block = With.call_with_scope_adapter(&block)
163
+ With.call_with_scope(kwargs, &block)
164
+ elsif __method__ == :with_list && args.empty? && !kwargs.empty? && block
165
+ # with_list(<factory_name>: [<object>, ...], ...) { ... }
166
+ block = With.call_with_scope_adapter(&block)
167
+ kwargs.values.inject(:product).map { With.call_with_scope(kwargs.keys.zip(_1).to_h, &block) }
160
168
  else
161
169
  raise ArgumentError, "Invalid use of #{__method__}"
162
170
  end
@@ -165,23 +173,35 @@ module FactoryBot
165
173
  end
166
174
 
167
175
  # @!visibility private
168
- # @param objects [{Symbol => Object}]
169
- def with_scoped_ancestors(objects)
170
- return unless block_given?
176
+ # @return [Array<Array(AssocInfo, Object)>, nil]
177
+ def scoped_ancestors = Thread.current[:factory_bot_with_scoped_ancestors]
171
178
 
179
+ # @param ancestors [Array<Array(AssocInfo, Object)>]
180
+ def with_scoped_ancestors(ancestors, &)
172
181
  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
- ]
182
+ Thread.current[:factory_bot_with_scoped_ancestors] = [*ancestors, *tmp_scoped_ancestors || []]
177
183
  result = yield
178
184
  Thread.current[:factory_bot_with_scoped_ancestors] = tmp_scoped_ancestors
179
185
  result
180
186
  end
181
187
 
182
188
  # @!visibility private
183
- # @return [Array<Array(AssocInfo, Object)>, nil]
184
- def scoped_ancestors = Thread.current[:factory_bot_with_scoped_ancestors]
189
+ # @param objects [{Symbol => Object}]
190
+ def call_with_scope(objects, &block)
191
+ with_scoped_ancestors(objects.map { [AssocInfo.get(_1), _2] }) { block.call(objects) }
192
+ end
193
+
194
+ # @!visibility private
195
+ def call_with_scope_adapter(&block)
196
+ params = block.parameters
197
+ if params.any? { %i[req opt rest].include?(_1[0]) }
198
+ ->(objects) { block.call(*objects.values) }
199
+ elsif params.any? { %i[keyreq key keyrest].include?(_1[0]) }
200
+ ->(objects) { block.call(**objects) }
201
+ else
202
+ ->(_) { block.call }
203
+ end
204
+ end
185
205
  end
186
206
 
187
207
  %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.5.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-03-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: factory_bot