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 +4 -4
- data/.irbrc +8 -0
- data/CHANGELOG.md +12 -1
- data/README.md +117 -55
- data/lib/factory_bot/with/assoc_info.rb +3 -3
- data/lib/factory_bot/with/scoped.rb +39 -0
- data/lib/factory_bot/with/version.rb +1 -1
- data/lib/factory_bot/with.rb +51 -64
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab9e29f43a809164761618a28339cd98f4c26060183045f79dcd52cc107ee3aa
|
4
|
+
data.tar.gz: 2c4814ce52ed94f4120077410519424a19aa8b590a2af571429aadb059f59687
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25a743163e0a1f248d0106534086ee928ecd4b46257985588287dfdf681a03309ea5f4c6db40bf9c5591aad3c2ba78edb80b071752fa992d7935b458b25bbf40
|
7
|
+
data.tar.gz: d01e0dcecfa6b765a7510f581d6197f324cfd464bb0f6c939477f067fb1dd76675c46c898c7d7759b3923477162ef161994cc60f86ddad5f9ae6f2a6be6956a7
|
data/.irbrc
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [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)
|
4
4
|
[](https://github.com/yubrot/factory_bot-with/actions/workflows/main.yml)
|
5
5
|
|
6
|
-
FactoryBot::With is a FactoryBot extension that
|
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,
|
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
|
-
|
34
|
-
|
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
|
-
|
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
|
61
|
+
### Method-style syntax
|
60
62
|
|
61
|
-
FactoryBot::With overrides the behavior
|
63
|
+
FactoryBot::With overrides the behavior of factory methods called without arguments.
|
62
64
|
|
63
65
|
```ruby
|
64
|
-
create(:foo, ...) #
|
65
|
-
create # returns a Proxy (an
|
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
|
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
|
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
|
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(
|
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
|
-
|
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
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
111
|
+
<summary>Automatic Association Resolution (AAR)</summary>
|
109
112
|
|
110
|
-
`with` automatically resolves references to ancestor objects based on the definition
|
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)
|
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)
|
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>
|
137
|
+
<summary>Factory Name Completion (FNC)</summary>
|
134
138
|
|
135
|
-
For a factory name that is prefixed by the
|
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) #
|
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
|
-
###
|
201
|
+
### Implicit context scope with existing objects
|
151
202
|
|
152
|
-
`with`
|
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
|
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
|
57
|
-
# If we want to avoid
|
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
|
data/lib/factory_bot/with.rb
CHANGED
@@ -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, "
|
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
|
-
|
67
|
-
first.call(
|
68
|
-
second.call(
|
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 =
|
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.
|
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
|
97
|
+
result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs)
|
114
98
|
|
115
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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.
|
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
|
-
|
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
|
+
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-
|
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
|
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
|
71
|
-
little easier to use
|
71
|
+
summary: a FactoryBot extension that enhances usability by wrapping factory methods
|
72
72
|
test_files: []
|