factory_bot-with 0.2.1 → 0.4.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 +9 -0
- data/README.md +23 -3
- data/lib/factory_bot/with/methods.rb +0 -17
- data/lib/factory_bot/with/proxy.rb +6 -9
- data/lib/factory_bot/with/version.rb +1 -1
- data/lib/factory_bot/with.rb +81 -38
- 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: 880e6e75c055c95bf4c8afab93288afaeb01f1a8ea7d162ecec04c3adaba1498
|
4
|
+
data.tar.gz: d59eb2dbc2c24256e1c1b4053d194de41378cdd3773c06375bf8617ae63af642
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74ee4b6d1718cfef30a3ddfaaca2486f854c8c6db5c4b1fa63c1947f2ae3f2430e8ec67afd8dac052ff049df4d61c7f091b66a0080d187e51e58c4795534b66f
|
7
|
+
data.tar.gz: f01718b24e04fe024189d77cec858f0aad8ec88fac9de520ced36bf954863775e6c0ecb079d0831016fac2d863edec204c8a23bec82f304ecd2b8c4186c9ab5e
|
data/.github/workflows/main.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.4.0]
|
4
|
+
|
5
|
+
- Fixed: Improved error message for incorrect factory usage
|
6
|
+
- Added: Added `with` scope syntax for automatic association resolution
|
7
|
+
|
8
|
+
## [0.3.0] - 2024-12-09
|
9
|
+
|
10
|
+
- Added: Added `FactoryBot::With.register_strategy` to support custom strategies
|
11
|
+
|
3
12
|
## [0.2.1] - 2024-11-29
|
4
13
|
|
5
14
|
- Fixed: Adjusted priority of factory names autocompletion
|
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
|
@@ -143,16 +145,18 @@ create.blog(with.article) # autocomplete to :blog_article
|
|
143
145
|
|
144
146
|
</details>
|
145
147
|
|
148
|
+
## Additional features
|
149
|
+
|
146
150
|
### `with` as a template
|
147
151
|
|
148
|
-
`with` can also be used stand-alone. It works as a template for factory method calls.
|
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.
|
149
153
|
|
150
154
|
Instead of writing:
|
151
155
|
|
152
156
|
```ruby
|
153
157
|
let(:story) { create(:story, *story_args, **story_kwargs) }
|
154
158
|
let(:story_args) { [] }
|
155
|
-
let(:story_kwargs) { {} }
|
159
|
+
let(:story_kwargs) { { category: "SF" } }
|
156
160
|
|
157
161
|
context "when published more than one year ago" do
|
158
162
|
let(:story_args) { [*super(), :published] }
|
@@ -167,7 +171,7 @@ You can write like this:
|
|
167
171
|
```ruby
|
168
172
|
# Factory methods accept a With instance as a first argument:
|
169
173
|
let(:story) { create(story_template) }
|
170
|
-
let(:story_template) { with.story }
|
174
|
+
let(:story_template) { with.story(category: "SF") }
|
171
175
|
|
172
176
|
context "when published more than one year ago" do
|
173
177
|
let(:story_template) { with(super(), :published, start_at: 2.year.ago) }
|
@@ -176,6 +180,22 @@ context "when published more than one year ago" do
|
|
176
180
|
end
|
177
181
|
```
|
178
182
|
|
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
|
+
|
179
199
|
## Development
|
180
200
|
|
181
201
|
```bash
|
@@ -5,23 +5,6 @@ module FactoryBot
|
|
5
5
|
# A <code>FactoryBot::Syntax::Methods</code> replacement to enable <code>factory_bot-with</code> features.
|
6
6
|
module Methods
|
7
7
|
include FactoryBot::Syntax::Methods
|
8
|
-
|
9
|
-
BUILD_STRATEGIES = %i[build build_stubbed create attributes_for with].freeze
|
10
|
-
VARIATIONS = {
|
11
|
-
unit: BUILD_STRATEGIES.to_h { [_1, _1] }.freeze,
|
12
|
-
pair: BUILD_STRATEGIES.to_h { [_1, :"#{_1}_pair"] }.freeze,
|
13
|
-
list: BUILD_STRATEGIES.to_h { [_1, :"#{_1}_list"] }.freeze,
|
14
|
-
}.freeze
|
15
|
-
|
16
|
-
VARIATIONS.each do |variation, build_strategies|
|
17
|
-
build_strategies.each do |build_strategy, method_name|
|
18
|
-
define_method(method_name) do |factory = nil, *args, **kwargs, &block|
|
19
|
-
return Proxy.new(self, __method__) unless factory
|
20
|
-
|
21
|
-
With.build(variation, factory, *args, **kwargs, &block).instantiate(build_strategy)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
8
|
end
|
26
9
|
end
|
27
10
|
end
|
@@ -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
@@ -9,7 +9,7 @@ require_relative "with/methods"
|
|
9
9
|
module FactoryBot
|
10
10
|
# An intermediate state for <code>with</code> operator.
|
11
11
|
class With
|
12
|
-
# @return [:
|
12
|
+
# @return [:singular, :pair, :list]
|
13
13
|
attr_reader :variation
|
14
14
|
# @return [Symbol]
|
15
15
|
attr_reader :factory_name
|
@@ -24,7 +24,7 @@ module FactoryBot
|
|
24
24
|
|
25
25
|
# @!visibility private
|
26
26
|
def initialize(variation, factory_name, withes: [], traits: [], attrs: {}, &block)
|
27
|
-
raise ArgumentError unless %i[
|
27
|
+
raise ArgumentError unless %i[singular pair list].include? variation
|
28
28
|
raise TypeError unless factory_name.is_a? Symbol
|
29
29
|
raise TypeError unless withes.is_a?(Array) && withes.all? { _1.is_a? self.class }
|
30
30
|
raise TypeError unless traits.is_a?(Array) && traits.all? { _1.is_a?(Symbol) || _1.is_a?(Numeric) }
|
@@ -59,18 +59,48 @@ module FactoryBot
|
|
59
59
|
withes: [*withes, *other.withes],
|
60
60
|
traits: [*traits, *other.traits],
|
61
61
|
attrs: { **attrs, **other.attrs },
|
62
|
-
&
|
62
|
+
&lambda do |first, second|
|
63
|
+
return first unless second
|
64
|
+
return second unless first
|
65
|
+
|
66
|
+
lambda do |*args|
|
67
|
+
first.call(*args)
|
68
|
+
second.call(*args)
|
69
|
+
end
|
70
|
+
end.call(block, other.block)
|
63
71
|
)
|
64
72
|
end
|
65
73
|
|
66
74
|
# @!visibility private
|
67
|
-
|
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
|
+
# @!visibility private
|
96
|
+
# @param build_strategy [Symbol]
|
68
97
|
# @param ancestors [Array<Array(AssocInfo, Object)>, nil]
|
69
98
|
# @return [Object]
|
70
|
-
def instantiate(build_strategy, ancestors =
|
99
|
+
def instantiate(build_strategy, ancestors = self.class.scoped_ancestors)
|
71
100
|
return self if build_strategy == :with
|
72
101
|
|
73
|
-
factory_bot_method =
|
102
|
+
factory_bot_method =
|
103
|
+
variation == :singular ? build_strategy : :"#{build_strategy}_#{variation}"
|
74
104
|
factory_name, attrs =
|
75
105
|
if ancestors
|
76
106
|
attrs = @attrs.dup
|
@@ -83,7 +113,7 @@ module FactoryBot
|
|
83
113
|
result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs, &block)
|
84
114
|
|
85
115
|
unless withes.empty?
|
86
|
-
parents = variation == :
|
116
|
+
parents = variation == :singular ? [result] : result
|
87
117
|
assoc_info = AssocInfo.get(factory_name)
|
88
118
|
parents.each do |parent|
|
89
119
|
ancestors_for_children = [[assoc_info, parent], *ancestors || []]
|
@@ -96,51 +126,64 @@ module FactoryBot
|
|
96
126
|
|
97
127
|
class << self
|
98
128
|
# @!visibility private
|
99
|
-
# @param variation [:
|
129
|
+
# @param variation [:singular, :pair, :list]
|
100
130
|
# @param factory [Symbol, With]
|
101
131
|
# @param args [Array<Object>]
|
102
132
|
# @param kwargs [{Symbol => Object}]
|
103
133
|
def build(variation, factory, *, **, &)
|
104
134
|
return factory.merge(build(variation, factory.factory_name, *, **, &)) if factory.is_a? self
|
105
135
|
|
106
|
-
|
107
|
-
insert_args!(with, *)
|
108
|
-
with.attrs.merge!(**)
|
109
|
-
with
|
136
|
+
new(variation, factory, &).extend!(*, { ** })
|
110
137
|
end
|
111
138
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
139
|
+
# If you want to use a custom strategy, call this along with <code>FactoryBot.register_strategy</code>.
|
140
|
+
# @param build_strategy [Symbol]
|
141
|
+
# @example
|
142
|
+
# FactoryBot.register_strategy(:json, JsonStrategy)
|
143
|
+
# FactoryBot::With.register_strategy(:json)
|
144
|
+
def register_strategy(build_strategy)
|
145
|
+
{
|
146
|
+
singular: build_strategy,
|
147
|
+
pair: :"#{build_strategy}_pair",
|
148
|
+
list: :"#{build_strategy}_list",
|
149
|
+
}.each do |variation, method_name|
|
150
|
+
Methods.define_method(method_name) do |factory = nil, *args, **kwargs, &block|
|
151
|
+
if factory
|
152
|
+
# <__method__>(<factory_name>, ...)
|
153
|
+
With.build(variation, factory, *args, **kwargs, &block).instantiate(build_strategy)
|
154
|
+
elsif args.empty? && kwargs.empty? && !block
|
155
|
+
# <__method__>.<factory_name>(...)
|
156
|
+
Proxy.new(self, __method__)
|
157
|
+
elsif __method__ == :with && args.empty? && !kwargs.empty?
|
158
|
+
# with(<factory_name>: <object>, ...) { ... }
|
159
|
+
With.with_scoped_ancestors(kwargs, &block)
|
160
|
+
else
|
161
|
+
raise ArgumentError, "Invalid use of #{__method__}"
|
162
|
+
end
|
127
163
|
end
|
128
164
|
end
|
129
165
|
end
|
130
166
|
|
131
167
|
# @!visibility private
|
132
|
-
# @param
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
143
180
|
end
|
181
|
+
|
182
|
+
# @!visibility private
|
183
|
+
# @return [Array<Array(AssocInfo, Object)>, nil]
|
184
|
+
def scoped_ancestors = Thread.current[:factory_bot_with_scoped_ancestors]
|
144
185
|
end
|
186
|
+
|
187
|
+
%i[build build_stubbed create attributes_for with].each { register_strategy _1 }
|
145
188
|
end
|
146
189
|
end
|
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.4.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-15 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
|