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 +4 -4
- data/.github/workflows/main.yml +2 -1
- data/CHANGELOG.md +11 -0
- data/README.md +72 -4
- data/lib/factory_bot/with/proxy.rb +6 -9
- data/lib/factory_bot/with/version.rb +1 -1
- data/lib/factory_bot/with.rb +58 -10
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa2396f8ce3d44a17caacc293e2b5914b781eb167c50de541c2950b6f3e8ce0f
|
4
|
+
data.tar.gz: a5b0e4b93ba6f97439dd25290c2ab5c3832175799e7e45f538e0c9db28be14bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a655f59ca867e26316fb08d3fd80e9352d4cefeb600c9d324de89358096a031e549acc5d98da43d67f608f615084bfc384a4c8d45f9e528891d4f90f9fe9de5f
|
7
|
+
data.tar.gz: 4709882eea9a1a90e6719a44133d66e1de9c189da2c602963f77b9c80b14952b99dbd2b350085d496f15cf70f20bbbce4edc1a5a37b2dcf9272d244be31d7589
|
data/.github/workflows/main.yml
CHANGED
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
|
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
|
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
|
-
#
|
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
|
-
|
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
|
data/lib/factory_bot/with.rb
CHANGED
@@ -63,9 +63,9 @@ module FactoryBot
|
|
63
63
|
return first unless second
|
64
64
|
return second unless first
|
65
65
|
|
66
|
-
|
67
|
-
first.call(
|
68
|
-
second.call(
|
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 =
|
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
|
113
|
+
result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs)
|
114
114
|
|
115
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
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.
|
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:
|
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.
|
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
|