factory_bot-with 0.3.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: 330ba97b29d7109bb0cb89cf0493cced5300e17c3a1465066405e46c66cbcd76
4
- data.tar.gz: 504cbcee6a16323d2a9494f10bcb68754d84130b343c316eab7a9f4c01432a0c
3
+ metadata.gz: fa2396f8ce3d44a17caacc293e2b5914b781eb167c50de541c2950b6f3e8ce0f
4
+ data.tar.gz: a5b0e4b93ba6f97439dd25290c2ab5c3832175799e7e45f538e0c9db28be14bf
5
5
  SHA512:
6
- metadata.gz: 11ed1a26a0ebd2590398eef94ebbf8aaeb23419cdd2b0d5e3979f3a64bbe4179e1f96d731804ccbd042535806a4999de62f5a6d5b72cf7ff5c2fbf379cd30372
7
- data.tar.gz: 5da17d95d18e8c746ddd5ce0a1213a8cf24dec8766753655ea912bc91a6b7c38dde9c7d85d73bca4165b7d17f126af31c2a4fd71be3cdbb5807c40a0434ef259
6
+ metadata.gz: a655f59ca867e26316fb08d3fd80e9352d4cefeb600c9d324de89358096a031e549acc5d98da43d67f608f615084bfc384a4c8d45f9e528891d4f90f9fe9de5f
7
+ data.tar.gz: 4709882eea9a1a90e6719a44133d66e1de9c189da2c602963f77b9c80b14952b99dbd2b350085d496f15cf70f20bbbce4edc1a5a37b2dcf9272d244be31d7589
@@ -14,7 +14,8 @@ jobs:
14
14
  strategy:
15
15
  matrix:
16
16
  ruby:
17
- - '3.3.6'
17
+ - '3.4.2'
18
+ - '3.3.7'
18
19
  - '3.2.6'
19
20
 
20
21
  steps:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
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
10
+
11
+ - Fixed: Improved error message for incorrect factory usage
12
+ - Added: Added `with` scope syntax for automatic association resolution
13
+
3
14
  ## [0.3.0] - 2024-12-09
4
15
 
5
16
  - Added: Added `FactoryBot::With.register_strategy` to support custom strategies
data/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  FactoryBot::With is a FactoryBot extension that wraps `FactoryBot::Syntax::Methods` to make it a little easier to use.
7
7
 
8
+ [FactoryBot における関連の扱いと、factory_bot-with gem を作った話 (Japanese)](https://zenn.dev/yubrot/articles/032447068e308e)
9
+
8
10
  For example, with these factories:
9
11
 
10
12
  ```ruby
@@ -60,7 +62,7 @@ FactoryBot::With overrides the behavior when factory methods are called without
60
62
 
61
63
  ```ruby
62
64
  create(:foo, ...) # behaves in the same way as FactoryBot.create
63
- create # returns a Proxy (an intermadiate) object
65
+ create # returns a Proxy (an intermediate) object
64
66
  create.foo(...) # is equivalent to create(:foo, ...)
65
67
  ```
66
68
 
@@ -143,16 +145,82 @@ create.blog(with.article) # autocomplete to :blog_article
143
145
 
144
146
  </details>
145
147
 
148
+ ## Additional features
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
+
146
214
  ### `with` as a template
147
215
 
148
- `with` can also be used stand-alone. It works as a template for factory method calls.
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.
149
217
 
150
218
  Instead of writing:
151
219
 
152
220
  ```ruby
153
221
  let(:story) { create(:story, *story_args, **story_kwargs) }
154
222
  let(:story_args) { [] }
155
- let(:story_kwargs) { {} }
223
+ let(:story_kwargs) { { category: "SF" } }
156
224
 
157
225
  context "when published more than one year ago" do
158
226
  let(:story_args) { [*super(), :published] }
@@ -167,7 +235,7 @@ You can write like this:
167
235
  ```ruby
168
236
  # Factory methods accept a With instance as a first argument:
169
237
  let(:story) { create(story_template) }
170
- let(:story_template) { with.story }
238
+ let(:story_template) { with.story(category: "SF") }
171
239
 
172
240
  context "when published more than one year ago" do
173
241
  let(:story_template) { with(super(), :published, start_at: 2.year.ago) }
@@ -4,31 +4,28 @@ module FactoryBot
4
4
  class With
5
5
  # An intermediate object to provide some notation combined with <code>method_missing</code>.
6
6
  # @example
7
- # class Foo
8
- # def foo(name = nil)
7
+ # class Example
8
+ # def foo(name = nil, ...)
9
9
  # return FactoryBot::With::Proxy.new(self, __method__) unless name
10
10
  #
11
11
  # name
12
12
  # end
13
13
  # end
14
14
  #
15
- # Foo.new.foo.bar #=> :bar
15
+ # ex = Example.new
16
+ # ex.foo.bar #=> :bar
16
17
  class Proxy < BasicObject
17
18
  # @param receiver [Object]
18
19
  # @param method [Symbol]
19
- # @param preargs [Array]
20
- def initialize(receiver, method, *preargs)
20
+ def initialize(receiver, method)
21
21
  @receiver = receiver
22
22
  @method = method
23
- @preargs = preargs
24
23
  end
25
24
 
26
25
  # @!visibility private
27
26
  def respond_to_missing?(_method_name, _) = true
28
27
 
29
- def method_missing(method_name, ...)
30
- @receiver.__send__(@method, *@preargs, method_name, ...)
31
- end
28
+ def method_missing(method_name, ...) = @receiver.__send__(@method, method_name, ...)
32
29
  end
33
30
  end
34
31
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module FactoryBot
4
4
  class With
5
- VERSION = "0.3.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
  )
@@ -96,7 +96,7 @@ module FactoryBot
96
96
  # @param build_strategy [Symbol]
97
97
  # @param ancestors [Array<Array(AssocInfo, Object)>, nil]
98
98
  # @return [Object]
99
- def instantiate(build_strategy, ancestors = nil)
99
+ def instantiate(build_strategy, ancestors = self.class.scoped_ancestors)
100
100
  return self if build_strategy == :with
101
101
 
102
102
  factory_bot_method =
@@ -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
 
@@ -148,12 +151,57 @@ module FactoryBot
148
151
  list: :"#{build_strategy}_list",
149
152
  }.each do |variation, method_name|
150
153
  Methods.define_method(method_name) do |factory = nil, *args, **kwargs, &block|
151
- return Proxy.new(self, __method__) unless factory
152
-
153
- With.build(variation, factory, *args, **kwargs, &block).instantiate(build_strategy)
154
+ if factory
155
+ # <__method__>(<factory_name>, ...)
156
+ With.build(variation, factory, *args, **kwargs, &block).instantiate(build_strategy)
157
+ elsif args.empty? && kwargs.empty? && !block
158
+ # <__method__>.<factory_name>(...)
159
+ Proxy.new(self, __method__)
160
+ elsif __method__ == :with && args.empty? && !kwargs.empty? && block
161
+ # with(<factory_name>: <object>, ...) { ... }
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) }
168
+ else
169
+ raise ArgumentError, "Invalid use of #{__method__}"
170
+ end
154
171
  end
155
172
  end
156
173
  end
174
+
175
+ # @!visibility private
176
+ # @return [Array<Array(AssocInfo, Object)>, nil]
177
+ def scoped_ancestors = Thread.current[:factory_bot_with_scoped_ancestors]
178
+
179
+ # @param ancestors [Array<Array(AssocInfo, Object)>]
180
+ def with_scoped_ancestors(ancestors, &)
181
+ tmp_scoped_ancestors = scoped_ancestors
182
+ Thread.current[:factory_bot_with_scoped_ancestors] = [*ancestors, *tmp_scoped_ancestors || []]
183
+ result = yield
184
+ Thread.current[:factory_bot_with_scoped_ancestors] = tmp_scoped_ancestors
185
+ result
186
+ end
187
+
188
+ # @!visibility private
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
157
205
  end
158
206
 
159
207
  %i[build build_stubbed create attributes_for with].each { register_strategy _1 }
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factory_bot-with
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yubrot
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-12-09 00:00:00.000000000 Z
10
+ date: 2025-03-20 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: factory_bot
@@ -52,7 +51,6 @@ metadata:
52
51
  source_code_uri: https://github.com/yubrot/factory_bot-with
53
52
  changelog_uri: https://github.com/yubrot/factory_bot-with/blob/main/CHANGELOG.md
54
53
  rubygems_mfa_required: 'true'
55
- post_install_message:
56
54
  rdoc_options: []
57
55
  require_paths:
58
56
  - lib
@@ -67,8 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
65
  - !ruby/object:Gem::Version
68
66
  version: '0'
69
67
  requirements: []
70
- rubygems_version: 3.5.22
71
- signing_key:
68
+ rubygems_version: 3.6.2
72
69
  specification_version: 4
73
70
  summary: FactoryBot extension that wraps FactoryBot::Syntax::Methods to make it a
74
71
  little easier to use