factory_bot-with 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa2396f8ce3d44a17caacc293e2b5914b781eb167c50de541c2950b6f3e8ce0f
4
- data.tar.gz: a5b0e4b93ba6f97439dd25290c2ab5c3832175799e7e45f538e0c9db28be14bf
3
+ metadata.gz: ab9e29f43a809164761618a28339cd98f4c26060183045f79dcd52cc107ee3aa
4
+ data.tar.gz: 2c4814ce52ed94f4120077410519424a19aa8b590a2af571429aadb059f59687
5
5
  SHA512:
6
- metadata.gz: a655f59ca867e26316fb08d3fd80e9352d4cefeb600c9d324de89358096a031e549acc5d98da43d67f608f615084bfc384a4c8d45f9e528891d4f90f9fe9de5f
7
- data.tar.gz: 4709882eea9a1a90e6719a44133d66e1de9c189da2c602963f77b9c80b14952b99dbd2b350085d496f15cf70f20bbbce4edc1a5a37b2dcf9272d244be31d7589
6
+ metadata.gz: 25a743163e0a1f248d0106534086ee928ecd4b46257985588287dfdf681a03309ea5f4c6db40bf9c5591aad3c2ba78edb80b071752fa992d7935b458b25bbf40
7
+ data.tar.gz: d01e0dcecfa6b765a7510f581d6197f324cfd464bb0f6c939477f067fb1dd76675c46c898c7d7759b3923477162ef161994cc60f86ddad5f9ae6f2a6be6956a7
data/.irbrc ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "factory_bot/with"
4
+ require_relative "spec/factories"
5
+
6
+ class << self
7
+ prepend FactoryBot::With::Methods
8
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
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
+
3
8
  ## [0.5.0] - 2025-03-20
4
9
 
5
10
  - Added: When using `with` scope syntax, blocks can now take given objects as arguments
data/README.md CHANGED
@@ -3,11 +3,11 @@
3
3
  [![](https://badge.fury.io/rb/factory_bot-with.svg)](https://badge.fury.io/rb/factory_bot-with)
4
4
  [![](https://github.com/yubrot/factory_bot-with/actions/workflows/main.yml/badge.svg)](https://github.com/yubrot/factory_bot-with/actions/workflows/main.yml)
5
5
 
6
- FactoryBot::With is a FactoryBot extension that wraps `FactoryBot::Syntax::Methods` to make it a little easier to use.
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, with these factories:
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
- with.article(with.comment),
34
- with.article(with_list.comment(3)),
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
- On your Gemfile:
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 style syntax
61
+ ### Method-style syntax
60
62
 
61
- FactoryBot::With overrides the behavior when factory methods are called without arguments.
63
+ FactoryBot::With overrides the behavior of factory methods called without arguments.
62
64
 
63
65
  ```ruby
64
- create(:foo, ...) # behaves in the same way as FactoryBot.create
66
+ create(:foo, ...) # normal usage
65
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 such as `build_stubbed`, `create_list`, `with` (described later), etc. as well.
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 adjusts the behavior of factory methods to accept `Hash`, `Array`, and falsy values (`false` or `nil`) as positional arguments.
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 provides a new operator `with` (and its family).
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(:factory_name, ...)
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
- The result of this operator (`With` instance) can be passed as an argument to factory methods such as `build` or `create`, which can then create additional objects following the result of the factory.
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.blog(with.article)
100
- # is equivalent to ...
101
- blog = create.blog
102
- create.article(blog:)
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 association resolution</summary>
111
+ <summary>Automatic Association Resolution (AAR)</summary>
109
112
 
110
- `with` automatically resolves references to ancestor objects based on the definition of the FactoryBot associations.
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) in the factories, [aliases](https://thoughtbot.github.io/factory_bot/sequences/aliases.html) in the factories, and [factory specifications](https://thoughtbot.github.io/factory_bot/associations/specifying-the-factory.html) in the associations.
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) cannot be resolved.
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>Autocomplete fully-qualified factory name</summary>
137
+ <summary>Factory Name Completion (FNC)</summary>
134
138
 
135
- For a factory name that is prefixed by the parent object's factory name, the prefix can be omitted.
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,70 +144,80 @@ FactoryBot.define do
140
144
  factory(:blog_article) { blog }
141
145
  end
142
146
 
143
- create.blog(with.article) # autocomplete to :blog_article
147
+ create.blog(with.article) # completes to :blog_article
144
148
  ```
145
149
 
146
150
  </details>
147
151
 
148
- ## Additional features
149
-
150
- ### `with` scope syntax for automatic association resolution
152
+ ### Implicit context scope
151
153
 
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.
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.
153
155
 
154
156
  ```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))
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:)
163
165
  end
164
166
  end
165
167
  ```
166
168
 
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
169
  <details>
177
- <summary>Comparison</summary>
170
+ <summary>Incompatible behavior when calling <code>_list</code> or <code>_pair</code> factory methods with a block</summary>
178
171
 
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`.
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:
180
173
 
181
174
  ```ruby
182
- # This creates a blog with 2 articles, each with 1 comment
183
- # plain factory_bot:
175
+ # This code creates a blog with 2 articles, each with a comment in standard FactoryBot:
176
+ # This does not work in FactoryBot::With!
184
177
  create(:blog) do |blog|
185
- create_list(:article, 2, blog:) do |articles| # yielded once with an array of articles in plain factory_bot
178
+ create_list(:article, 2, blog:) do |articles| # yielded *once* with an array of articles
186
179
  articles.each { |article| create(:comment, article:) }
187
180
  end
188
181
  end
189
182
 
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
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
198
188
  end
199
189
 
200
- # with as a scope syntax for existing blog:
201
- blog = create.blog
202
- with(blog:) do
203
- create_list.article(2) { create.comment }
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)
193
+ ```
194
+
195
+ If you want to avoid this incompatibility, you can use `Object#tap`.
196
+
197
+ </details>
198
+
199
+ ## Additional features
200
+
201
+ ### Implicit context scope with existing objects
202
+
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
204
215
  end
216
+ ```
217
+
218
+ `with_list` works similarly to `with`, except that it accepts arrays as its values:
205
219
 
206
- # with_list can also be used as a scope syntax:
220
+ ```ruby
207
221
  blog = create.blog
208
222
  articles = create_list.article(2, blog:)
209
223
  with_list(article: articles) { create.comment } # yielded *for each article*
@@ -211,9 +225,9 @@ with_list(article: articles) { create.comment } # yielded *for each article*
211
225
 
212
226
  </details>
213
227
 
214
- ### `with` as a template
228
+ ### `with` as a factory method call template
215
229
 
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.
230
+ A `With` instance can also be used as a template for factory method calls.
217
231
 
218
232
  Instead of writing:
219
233
 
@@ -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 autocomplete_fully_qualified_factory_name(ancestors, partial_factory_name)
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 autocompleted names, then attempt to resolve with the original name.
57
- # If we want to avoid autocompletion, we should be able to simply use a factory such as build or create.
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module FactoryBot
4
4
  class With
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -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, "oter must be an instance of #{self.class}" unless other.is_a? self.class
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
 
@@ -71,32 +76,11 @@ module FactoryBot
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 = self.class.scoped_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,7 +88,7 @@ module FactoryBot
104
88
  factory_name, attrs =
105
89
  if ancestors
106
90
  attrs = @attrs.dup
107
- factory_name = AssocInfo.autocomplete_fully_qualified_factory_name(ancestors, @factory_name)
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
@@ -120,25 +104,35 @@ module FactoryBot
120
104
  withes.each { _1.instantiate(build_strategy, ancestors_for_children) }
121
105
  # We call the block for each parent object. This is an incompatible behavior with FactoryBot!
122
106
  # If you want to avoid this, use `Object#tap` manually.
123
- self.class.with_scoped_ancestors(ancestors_for_children) { block.call(result) } if block
107
+ Scoped.with_ancestors(ancestors_for_children) { block.call(result) } if block
124
108
  end
125
109
  end
126
110
 
127
111
  result
128
112
  end
129
113
 
130
- class << self
131
- # @!visibility private
132
- # @param variation [:singular, :pair, :list]
133
- # @param factory [Symbol, With]
134
- # @param args [Array<Object>]
135
- # @param kwargs [{Symbol => Object}]
136
- def build(variation, factory, *, **, &)
137
- return factory.merge(build(variation, factory.factory_name, *, **, &)) if factory.is_a? self
138
-
139
- new(variation, factory, &).extend!(*, { ** })
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
140
132
  end
133
+ end
141
134
 
135
+ class << self
142
136
  # If you want to use a custom strategy, call this along with <code>FactoryBot.register_strategy</code>.
143
137
  # @param build_strategy [Symbol]
144
138
  # @example
@@ -151,57 +145,30 @@ module FactoryBot
151
145
  list: :"#{build_strategy}_list",
152
146
  }.each do |variation, method_name|
153
147
  Methods.define_method(method_name) do |factory = nil, *args, **kwargs, &block|
154
- 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
155
154
  # <__method__>(<factory_name>, ...)
156
- With.build(variation, factory, *args, **kwargs, &block).instantiate(build_strategy)
155
+ With.new(variation, factory, *args, attrs: kwargs, &block).instantiate(build_strategy)
157
156
  elsif args.empty? && kwargs.empty? && !block
158
157
  # <__method__>.<factory_name>(...)
159
158
  Proxy.new(self, __method__)
160
159
  elsif __method__ == :with && args.empty? && !kwargs.empty? && block
161
160
  # with(<factory_name>: <object>, ...) { ... }
162
- block = With.call_with_scope_adapter(&block)
163
- With.call_with_scope(kwargs, &block)
161
+ Scoped.block(&block).call(kwargs)
164
162
  elsif __method__ == :with_list && args.empty? && !kwargs.empty? && block
165
163
  # 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) }
164
+ block = Scoped.block(&block)
165
+ kwargs.values.inject(:product).map { block.call(kwargs.keys.zip(_1).to_h) }
168
166
  else
169
167
  raise ArgumentError, "Invalid use of #{__method__}"
170
168
  end
171
169
  end
172
170
  end
173
171
  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
205
172
  end
206
173
 
207
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.5.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-03-20 00:00:00.000000000 Z
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 wraps FactoryBot::Syntax::Methods to make it
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 wraps FactoryBot::Syntax::Methods to make it a
71
- little easier to use
71
+ summary: a FactoryBot extension that enhances usability by wrapping factory methods
72
72
  test_files: []