factory_bot 6.5.4 → 6.5.6

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.
data/README.md CHANGED
@@ -1,20 +1,17 @@
1
- # factory_bot [![Build Status][ci-image]][ci] [![Code Climate][grade-image]][grade] [![Gem Version][version-image]][version]
1
+ # factory_bot
2
+
3
+ [![Build Status][ci-image]][ci] [![Code Climate][grade-image]][grade] [![Gem Version][version-image]][version]
2
4
 
3
5
  factory_bot is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute hashes, and stubbed objects), and support for multiple factories for the same class (user, admin_user, and so on), including factory inheritance.
4
6
 
5
7
  If you want to use factory_bot with Rails, see
6
8
  [factory_bot_rails](https://github.com/thoughtbot/factory_bot_rails).
7
9
 
8
- _[Interested in the history of the project name?][NAME]_
9
-
10
-
11
- ### Transitioning from factory\_girl?
10
+ Interested in the history of the project name? You can find the history [here](https://github.com/thoughtbot/factory_bot/blob/main/NAME.md)
12
11
 
13
- Check out the [guide](https://github.com/thoughtbot/factory_bot/blob/v4.9.0/UPGRADE_FROM_FACTORY_GIRL.md).
12
+ Transitioning from factory\_girl? Check out the [upgrade guide](https://github.com/thoughtbot/factory_bot/blob/v4.9.0/UPGRADE_FROM_FACTORY_GIRL.md).
14
13
 
15
-
16
- Documentation
17
- -------------
14
+ ## Documentation
18
15
 
19
16
  See our extensive reference, guides, and cookbook in [the factory_bot book][].
20
17
 
@@ -27,8 +24,7 @@ Rails, see [the factory_bot wiki][].
27
24
  [the factory_bot book]: https://thoughtbot.github.io/factory_bot
28
25
  [the factory_bot wiki]: https://github.com/thoughtbot/factory_bot/wiki
29
26
 
30
- Install
31
- --------
27
+ ## Install
32
28
 
33
29
  Run:
34
30
 
@@ -42,13 +38,11 @@ To install the gem manually from your shell, run:
42
38
  gem install factory_bot
43
39
  ```
44
40
 
45
- Supported Ruby versions
46
- -----------------------
41
+ ## Supported Ruby versions
47
42
 
48
- Supported Ruby versions are listed in [`.github/workflows/build.yml`](https://github.com/thoughtbot/factory_bot/blob/main/.github/workflows/build.yml)
43
+ Supported Ruby versions are listed in `.github/workflows/build.yml` ([source](https://github.com/thoughtbot/factory_bot/blob/main/.github/workflows/build.yml))
49
44
 
50
- More Information
51
- ----------------
45
+ ## More Information
52
46
 
53
47
  * [Rubygems](https://rubygems.org/gems/factory_bot)
54
48
  * [Stack Overflow](https://stackoverflow.com/questions/tagged/factory-bot)
@@ -56,10 +50,8 @@ More Information
56
50
  * [GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS](https://robots.thoughtbot.com/)
57
51
 
58
52
  [GETTING_STARTED]: https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md
59
- [NAME]: https://github.com/thoughtbot/factory_bot/blob/main/NAME.md
60
53
 
61
- Useful Tools
62
- ------------
54
+ ## Useful Tools
63
55
 
64
56
  * [FactoryTrace](https://github.com/djezzzl/factory_trace) - helps to find unused factories and traits.
65
57
  * [ruby-lsp-factory_bot](https://github.com/donny741/ruby-lsp-factory_bot) / [ruby-lsp-rails-factory-bot](https://github.com/johansenja/ruby-lsp-rails-factory-bot) - integration with [ruby-lsp](https://github.com/Shopify/ruby-lsp) to provide intellisense
@@ -9,6 +9,7 @@ module FactoryBot
9
9
  @attribute_names_assigned = []
10
10
  end
11
11
 
12
+ # constructs an object-based factory product
12
13
  def object
13
14
  @evaluator.instance = build_class_instance
14
15
  build_class_instance.tap do |instance|
@@ -19,6 +20,7 @@ module FactoryBot
19
20
  end
20
21
  end
21
22
 
23
+ # constructs a Hash-based factory product
22
24
  def hash
23
25
  @evaluator.instance = build_hash
24
26
 
@@ -29,6 +31,8 @@ module FactoryBot
29
31
 
30
32
  private
31
33
 
34
+ # Track evaluation of methods on the evaluator to prevent the duplicate
35
+ # assignment of attributes accessed and via `initialize_with` syntax
32
36
  def method_tracking_evaluator
33
37
  @method_tracking_evaluator ||= Decorator::AttributeHash.new(
34
38
  decorated_evaluator,
@@ -67,12 +71,15 @@ module FactoryBot
67
71
  attribute_names_to_assign - association_names
68
72
  end
69
73
 
74
+ # Builds a list of attributes names that should be assigned to the factory product
70
75
  def attribute_names_to_assign
71
- @attribute_names_to_assign ||=
72
- non_ignored_attribute_names +
73
- override_names -
74
- ignored_attribute_names -
75
- alias_names_to_ignore
76
+ @attribute_names_to_assign ||= begin
77
+ # start a list of candidates containing non-transient attributes and overrides
78
+ assignment_candidates = non_ignored_attribute_names + override_names
79
+ # then remove any transient attributes (potentially reintroduced by the overrides),
80
+ # and remove ignorable aliased attributes from the candidate list
81
+ assignment_candidates - ignored_attribute_names - attribute_names_overriden_by_alias
82
+ end
76
83
  end
77
84
 
78
85
  def non_ignored_attribute_names
@@ -91,22 +98,71 @@ module FactoryBot
91
98
  @evaluator.__override_names__
92
99
  end
93
100
 
94
- def hash_instance_methods_to_respond_to
95
- @attribute_list.names + override_names + @build_class.instance_methods
96
- end
97
-
98
- def alias_names_to_ignore
99
- @attribute_list.non_ignored.flat_map { |attribute|
100
- override_names.map do |override|
101
- attribute.name if ignorable_alias?(attribute, override)
102
- end
103
- }.compact
101
+ def attribute_names
102
+ @attribute_list.names
104
103
  end
105
104
 
105
+ def hash_instance_methods_to_respond_to
106
+ attribute_names + override_names + @build_class.instance_methods
107
+ end
108
+
109
+ # Builds a list of attribute names which are slated to be interrupted by an override.
110
+ def attribute_names_overriden_by_alias
111
+ @attribute_list
112
+ .non_ignored
113
+ .flat_map { |attribute|
114
+ override_names.map do |override|
115
+ attribute.name if ignorable_alias?(attribute, override)
116
+ end
117
+ }
118
+ .compact
119
+ end
120
+
121
+ # Is the attribute an ignorable alias of the override?
122
+ # An attribute is ignorable when it is an alias of the override AND it is
123
+ # either interrupting an assocciation OR is not the name of another attribute
124
+ #
125
+ # @note An "alias" is currently an overloaded term for two distinct cases:
126
+ # (1) attributes which are aliases and reference the same value
127
+ # (2) a logical grouping of a foreign key and an associated object
106
128
  def ignorable_alias?(attribute, override)
107
- attribute.alias_for?(override) &&
108
- attribute.name != override &&
109
- !ignored_attribute_names.include?(override)
129
+ return false unless attribute.alias_for?(override)
130
+
131
+ # The attribute alias should be ignored when the override interrupts an association
132
+ return true if override_interrupts_association?(attribute, override)
133
+
134
+ # Remaining aliases should be ignored when the override does not match a declared attribute.
135
+ # An override which is an alias to a declared attribute should not interrupt the aliased
136
+ # attribute and interrupt only the attribute with a matching name. This workaround allows a
137
+ # factory to declare both <attribute> and <attribute>_id as separate and distinct attributes.
138
+ !override_matches_declared_attribute?(override)
139
+ end
140
+
141
+ # Does this override interrupt an association?
142
+ # When true, this indicates the aliased attribute is related to a declared association and the
143
+ # override does not match the attribute name.
144
+ #
145
+ # @note Association overrides should take precedence over a declared foreign key attribute.
146
+ #
147
+ # @note An override may interrupt an association by providing the associated object or
148
+ # by providing the foreign key.
149
+ #
150
+ # @param [FactoryBot::Attribute] aliased_attribute
151
+ # @param [Symbol] override name of an override which is an alias to the attribute name
152
+ def override_interrupts_association?(aliased_attribute, override)
153
+ (aliased_attribute.association? || association_names.include?(override)) &&
154
+ aliased_attribute.name != override
155
+ end
156
+
157
+ # Does this override match the name of any declared attribute?
158
+ #
159
+ # @note Checking against the names of all attributes, resolves any issues with having both
160
+ # <attribute> and <attribute>_id in the same factory. This also takes into account ignored
161
+ # attributes that should not be assigned (aka transient attributes)
162
+ #
163
+ # @param [Symbol] override the name of an override
164
+ def override_matches_declared_attribute?(override)
165
+ attribute_names.include?(override)
110
166
  end
111
167
  end
112
168
  end
@@ -4,11 +4,15 @@ module FactoryBot
4
4
  def initialize(callbacks, evaluator)
5
5
  @callbacks = callbacks
6
6
  @evaluator = evaluator
7
+ @completed = []
7
8
  end
8
9
 
9
10
  def update(name, result_instance)
10
11
  callbacks_by_name(name).each do |callback|
11
- callback.run(result_instance, @evaluator)
12
+ if !completed?(result_instance, callback)
13
+ callback.run(result_instance, @evaluator)
14
+ record_completion!(result_instance, callback)
15
+ end
12
16
  end
13
17
  end
14
18
 
@@ -17,5 +21,19 @@ module FactoryBot
17
21
  def callbacks_by_name(name)
18
22
  @callbacks.select { |callback| callback.name == name }
19
23
  end
24
+
25
+ def completed?(instance, callback)
26
+ key = completion_key_for(instance, callback)
27
+ @completed.include?(key)
28
+ end
29
+
30
+ def record_completion!(instance, callback)
31
+ key = completion_key_for(instance, callback)
32
+ @completed << key
33
+ end
34
+
35
+ def completion_key_for(instance, callback)
36
+ "#{instance.object_id}-#{callback.object_id}"
37
+ end
20
38
  end
21
39
  end
@@ -1,4 +1,3 @@
1
- require "active_support/core_ext/hash/except"
2
1
  require "active_support/core_ext/class/attribute"
3
2
 
4
3
  module FactoryBot
@@ -51,8 +50,15 @@ module FactoryBot
51
50
  @overrides.keys
52
51
  end
53
52
 
54
- def increment_sequence(sequence)
55
- sequence.next(self)
53
+ def increment_sequence(sequence, scope: self)
54
+ value = sequence.next(scope)
55
+
56
+ raise if value.respond_to?(:start_with?) && value.start_with?("#<FactoryBot::Declaration")
57
+
58
+ value
59
+ rescue
60
+ raise ArgumentError, "Sequence '#{sequence.uri_manager.first}' failed to " \
61
+ "return a value. Perhaps it needs a scope to operate? (scope: <object>)"
56
62
  end
57
63
 
58
64
  def self.attribute_list
@@ -33,18 +33,22 @@ module FactoryBot
33
33
 
34
34
  def run(build_strategy, overrides, &block)
35
35
  block ||= ->(result) { result }
36
+
36
37
  compile
37
38
 
38
- strategy = StrategyCalculator.new(build_strategy).strategy.new
39
+ strategy = Strategy.lookup_strategy(build_strategy).new
39
40
 
40
41
  evaluator = evaluator_class.new(strategy, overrides.symbolize_keys)
41
42
  attribute_assigner = AttributeAssigner.new(evaluator, build_class, &compiled_constructor)
42
43
 
43
44
  observer = CallbacksObserver.new(callbacks, evaluator)
44
- evaluation =
45
- Evaluation.new(evaluator, attribute_assigner, compiled_to_create, observer)
45
+ evaluation = Evaluation.new(evaluator, attribute_assigner, compiled_to_create, observer)
46
+
47
+ evaluation.notify(:before_all, nil)
48
+ instance = strategy.result(evaluation).tap(&block)
49
+ evaluation.notify(:after_all, instance)
46
50
 
47
- strategy.result(evaluation).tap(&block)
51
+ instance
48
52
  end
49
53
 
50
54
  def human_names
@@ -1,4 +1,4 @@
1
- require "active_support/core_ext/hash/indifferent_access"
1
+ require "active_support/hash_with_indifferent_access"
2
2
 
3
3
  module FactoryBot
4
4
  class Registry
@@ -17,8 +17,8 @@ module FactoryBot
17
17
 
18
18
  def self.find_by_uri(uri)
19
19
  uri = uri.to_sym
20
- (FactoryBot::Internal.sequences.to_a.find { |seq| seq.has_uri?(uri) }) ||
21
- (FactoryBot::Internal.inline_sequences.find { |seq| seq.has_uri?(uri) })
20
+ FactoryBot::Internal.sequences.to_a.find { |seq| seq.has_uri?(uri) } ||
21
+ FactoryBot::Internal.inline_sequences.find { |seq| seq.has_uri?(uri) }
22
22
  end
23
23
 
24
24
  def initialize(name, *args, &proc)
@@ -150,25 +150,24 @@ module FactoryBot
150
150
  end
151
151
 
152
152
  class EnumeratorAdapter
153
- def initialize(value)
154
- @first_value = value
155
- @value = value
153
+ def initialize(initial_value)
154
+ @initial_value = initial_value
156
155
  end
157
156
 
158
157
  def peek
159
- @value
158
+ value
160
159
  end
161
160
 
162
161
  def next
163
- @value = @value.next
162
+ @value = value.next
164
163
  end
165
164
 
166
165
  def rewind
167
- @value = @first_value
166
+ @value = first_value
168
167
  end
169
168
 
170
169
  def set_value(new_value)
171
- if new_value >= @first_value
170
+ if new_value >= first_value
172
171
  @value = new_value
173
172
  else
174
173
  fail ArgumentError, "Value cannot be less than: #{@first_value}"
@@ -176,7 +175,22 @@ module FactoryBot
176
175
  end
177
176
 
178
177
  def integer_value?
179
- @first_value.is_a?(Integer)
178
+ first_value.is_a?(Integer)
179
+ end
180
+
181
+ private
182
+
183
+ def first_value
184
+ @first_value ||= initial_value
185
+ end
186
+
187
+ def value
188
+ @value ||= initial_value
189
+ end
190
+
191
+ def initial_value
192
+ @value = @initial_value.respond_to?(:call) ? @initial_value.call : @initial_value
193
+ @first_value = @value
180
194
  end
181
195
  end
182
196
  end
@@ -6,6 +6,8 @@ module FactoryBot
6
6
  end
7
7
 
8
8
  def result(evaluation)
9
+ evaluation.notify(:before_build, nil)
10
+
9
11
  evaluation.object.tap do |instance|
10
12
  evaluation.notify(:after_build, instance)
11
13
  end
@@ -6,6 +6,8 @@ module FactoryBot
6
6
  end
7
7
 
8
8
  def result(evaluation)
9
+ evaluation.notify(:before_build, nil)
10
+
9
11
  evaluation.object.tap do |instance|
10
12
  evaluation.notify(:after_build, instance)
11
13
  evaluation.notify(:before_create, instance)
@@ -0,0 +1,15 @@
1
+ require "factory_bot/strategy/build"
2
+ require "factory_bot/strategy/create"
3
+ require "factory_bot/strategy/attributes_for"
4
+ require "factory_bot/strategy/stub"
5
+ require "factory_bot/strategy/null"
6
+
7
+ module FactoryBot
8
+ module Strategy
9
+ def self.lookup_strategy(name_or_object)
10
+ return name_or_object if name_or_object.is_a?(Class)
11
+
12
+ FactoryBot::Internal.strategy_by_name(name_or_object)
13
+ end
14
+ end
15
+ end
@@ -83,20 +83,20 @@ module FactoryBot
83
83
  # (see #strategy_method_pair)
84
84
  # @return [Array<Hash>] pair of attribute hashes for the factory
85
85
 
86
- # @!method strategy_method
86
+ # @!method strategy_method(name, traits_and_overrides, &block)
87
87
  # @!visibility private
88
88
  # @param [Symbol] name the name of the factory to build
89
89
  # @param [Array<Symbol, Symbol, Hash>] traits_and_overrides splat args traits and a hash of overrides
90
90
  # @param [Proc] block block to be executed
91
91
 
92
- # @!method strategy_method_list
92
+ # @!method strategy_method_list(name, amount, traits_and_overrides, &block)
93
93
  # @!visibility private
94
94
  # @param [Symbol] name the name of the factory to execute
95
95
  # @param [Integer] amount the number of instances to execute
96
96
  # @param [Array<Symbol, Symbol, Hash>] traits_and_overrides splat args traits and a hash of overrides
97
97
  # @param [Proc] block block to be executed
98
98
 
99
- # @!method strategy_method_pair
99
+ # @!method strategy_method_pair(name, traits_and_overrides, &block)
100
100
  # @!visibility private
101
101
  # @param [Symbol] name the name of the factory to execute
102
102
  # @param [Array<Symbol, Symbol, Hash>] traits_and_overrides splat args traits and a hash of overrides
@@ -123,7 +123,7 @@ module FactoryBot
123
123
  raise(KeyError,
124
124
  "Sequence not registered: #{FactoryBot::UriManager.build_uri(uri_parts)}")
125
125
 
126
- increment_sequence(uri, sequence, scope: scope)
126
+ increment_sequence(sequence, scope: scope)
127
127
  end
128
128
 
129
129
  # Generates and returns the list of values in a global or factory sequence.
@@ -147,7 +147,7 @@ module FactoryBot
147
147
  raise(KeyError, "Sequence not registered: '#{uri}'")
148
148
 
149
149
  (1..count).map do
150
- increment_sequence(uri, sequence, scope: scope)
150
+ increment_sequence(sequence, scope: scope)
151
151
  end
152
152
  end
153
153
 
@@ -161,21 +161,19 @@ module FactoryBot
161
161
  # Increments the given sequence and returns the value.
162
162
  #
163
163
  # Arguments:
164
- # uri: (Symbol)
165
- # The URI for the sequence
166
164
  # sequence:
167
165
  # The sequence instance
168
166
  # scope: (object)(optional)
169
167
  # The object the sequence should be evaluated within
170
168
  #
171
- def increment_sequence(uri, sequence, scope: nil)
169
+ def increment_sequence(sequence, scope: nil)
172
170
  value = sequence.next(scope)
173
171
 
174
172
  raise if value.respond_to?(:start_with?) && value.start_with?("#<FactoryBot::Declaration")
175
173
 
176
174
  value
177
175
  rescue
178
- raise ArgumentError, "Sequence '#{uri}' failed to " \
176
+ raise ArgumentError, "Sequence '#{sequence.uri_manager.first}' failed to " \
179
177
  "return a value. Perhaps it needs a scope to operate? (scope: <object>)"
180
178
  end
181
179
  end
@@ -1,3 +1,3 @@
1
1
  module FactoryBot
2
- VERSION = "6.5.4".freeze
2
+ VERSION = "6.5.6".freeze
3
3
  end
data/lib/factory_bot.rb CHANGED
@@ -11,12 +11,7 @@ require "factory_bot/configuration"
11
11
  require "factory_bot/errors"
12
12
  require "factory_bot/factory_runner"
13
13
  require "factory_bot/strategy_syntax_method_registrar"
14
- require "factory_bot/strategy_calculator"
15
- require "factory_bot/strategy/build"
16
- require "factory_bot/strategy/create"
17
- require "factory_bot/strategy/attributes_for"
18
- require "factory_bot/strategy/stub"
19
- require "factory_bot/strategy/null"
14
+ require "factory_bot/strategy"
20
15
  require "factory_bot/registry"
21
16
  require "factory_bot/null_factory"
22
17
  require "factory_bot/null_object"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factory_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.5.4
4
+ version: 6.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Clayton
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: cucumber
70
+ name: mutex_m
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: mutex_m
84
+ name: ostruct
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -233,12 +233,12 @@ files:
233
233
  - lib/factory_bot/registry.rb
234
234
  - lib/factory_bot/reload.rb
235
235
  - lib/factory_bot/sequence.rb
236
+ - lib/factory_bot/strategy.rb
236
237
  - lib/factory_bot/strategy/attributes_for.rb
237
238
  - lib/factory_bot/strategy/build.rb
238
239
  - lib/factory_bot/strategy/create.rb
239
240
  - lib/factory_bot/strategy/null.rb
240
241
  - lib/factory_bot/strategy/stub.rb
241
- - lib/factory_bot/strategy_calculator.rb
242
242
  - lib/factory_bot/strategy_syntax_method_registrar.rb
243
243
  - lib/factory_bot/syntax.rb
244
244
  - lib/factory_bot/syntax/default.rb
@@ -266,7 +266,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
266
266
  - !ruby/object:Gem::Version
267
267
  version: '0'
268
268
  requirements: []
269
- rubygems_version: 3.6.7
269
+ rubygems_version: 3.7.1
270
270
  specification_version: 4
271
271
  summary: factory_bot provides a framework and DSL for defining and using model instance
272
272
  factories.
@@ -1,26 +0,0 @@
1
- module FactoryBot
2
- # @api private
3
- class StrategyCalculator
4
- def initialize(name_or_object)
5
- @name_or_object = name_or_object
6
- end
7
-
8
- def strategy
9
- if strategy_is_object?
10
- @name_or_object
11
- else
12
- strategy_name_to_object
13
- end
14
- end
15
-
16
- private
17
-
18
- def strategy_is_object?
19
- @name_or_object.is_a?(Class)
20
- end
21
-
22
- def strategy_name_to_object
23
- FactoryBot::Internal.strategy_by_name(@name_or_object)
24
- end
25
- end
26
- end