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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abe0a558801602a366eb21e5aacf753e8cb3fb75a2e45b6df492308c4244d85f
4
- data.tar.gz: 514e5d0190f7dfa9cd0744062217a99c2ba5964be8f2bcf2fa64864044a84465
3
+ metadata.gz: 880e6e75c055c95bf4c8afab93288afaeb01f1a8ea7d162ecec04c3adaba1498
4
+ data.tar.gz: d59eb2dbc2c24256e1c1b4053d194de41378cdd3773c06375bf8617ae63af642
5
5
  SHA512:
6
- metadata.gz: 8243dd0f2ea730d9266964806a48934856254b2ecd1f73bf839f19198e2540061c373a8778413ddcfcc5a68aface18cd3debfa9ae0b5f8b1f8416a1ed2459d98
7
- data.tar.gz: 78031d8c55fef101fb7cf9c5511beeae3374813faa62f5925deb3ed70a80872e01baf1c127192d694f1a7546f10e1156534408a91fb61e8e39d5e6830b4d320a
6
+ metadata.gz: 74ee4b6d1718cfef30a3ddfaaca2486f854c8c6db5c4b1fa63c1947f2ae3f2430e8ec67afd8dac052ff049df4d61c7f091b66a0080d187e51e58c4795534b66f
7
+ data.tar.gz: f01718b24e04fe024189d77cec858f0aad8ec88fac9de520ced36bf954863775e6c0ecb079d0831016fac2d863edec204c8a23bec82f304ecd2b8c4186c9ab5e
@@ -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,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 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.2.1"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -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 [:unit, :pair, :list]
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[unit pair list].include? variation
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
- &self.class.merge_block(block, other.block)
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
- # @param build_strategy [:build, :build_stubbed, :create, :attributes_for, :with]
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 = nil)
99
+ def instantiate(build_strategy, ancestors = self.class.scoped_ancestors)
71
100
  return self if build_strategy == :with
72
101
 
73
- factory_bot_method = Methods::VARIATIONS[variation][build_strategy]
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 == :unit ? [result] : result
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 [:unit, :pair, :list]
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
- with = new(variation, factory, &)
107
- insert_args!(with, *)
108
- with.attrs.merge!(**)
109
- with
136
+ new(variation, factory, &).extend!(*, { ** })
110
137
  end
111
138
 
112
- def insert_args!(with, *args)
113
- args.each do |arg|
114
- case arg
115
- when self
116
- with.withes << arg
117
- when Symbol, Numeric
118
- with.traits << arg
119
- when Array
120
- insert_args!(with, *arg)
121
- when Hash
122
- with.attrs.merge!(arg)
123
- when false, nil
124
- # Ignored. This behavior is useful for conditional arguments like `is_premium && :premium`
125
- else
126
- raise ArgumentError, "Unsupported type for factory argument: #{arg}"
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 first [Proc, nil]
133
- # @param second [Proc, nil]
134
- # @return [Proc, nil]
135
- def merge_block(first, second)
136
- return first unless second
137
- return second unless first
138
-
139
- lambda do |*args|
140
- first.call(*args)
141
- second.call(*args)
142
- end
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.2.1
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: 2024-11-29 00:00:00.000000000 Z
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.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