factory_bot 4.11.1 → 6.2.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +58 -13
  3. data/GETTING_STARTED.md +785 -153
  4. data/LICENSE +1 -1
  5. data/NEWS.md +379 -0
  6. data/README.md +20 -30
  7. data/lib/factory_bot/aliases.rb +2 -2
  8. data/lib/factory_bot/attribute/association.rb +2 -2
  9. data/lib/factory_bot/attribute/dynamic.rb +3 -2
  10. data/lib/factory_bot/attribute.rb +4 -39
  11. data/lib/factory_bot/attribute_assigner.rb +24 -10
  12. data/lib/factory_bot/attribute_list.rb +3 -2
  13. data/lib/factory_bot/callback.rb +4 -11
  14. data/lib/factory_bot/configuration.rb +15 -19
  15. data/lib/factory_bot/declaration/association.rb +33 -3
  16. data/lib/factory_bot/declaration/dynamic.rb +3 -1
  17. data/lib/factory_bot/declaration/implicit.rb +7 -2
  18. data/lib/factory_bot/declaration.rb +5 -5
  19. data/lib/factory_bot/declaration_list.rb +3 -3
  20. data/lib/factory_bot/decorator/attribute_hash.rb +1 -1
  21. data/lib/factory_bot/decorator/invocation_tracker.rb +2 -1
  22. data/lib/factory_bot/decorator.rb +20 -4
  23. data/lib/factory_bot/definition.rb +69 -21
  24. data/lib/factory_bot/definition_hierarchy.rb +1 -11
  25. data/lib/factory_bot/definition_proxy.rb +119 -64
  26. data/lib/factory_bot/enum.rb +27 -0
  27. data/lib/factory_bot/errors.rb +7 -4
  28. data/lib/factory_bot/evaluation.rb +1 -1
  29. data/lib/factory_bot/evaluator.rb +10 -11
  30. data/lib/factory_bot/evaluator_class_definer.rb +1 -1
  31. data/lib/factory_bot/factory.rb +12 -12
  32. data/lib/factory_bot/factory_runner.rb +4 -4
  33. data/lib/factory_bot/find_definitions.rb +2 -2
  34. data/lib/factory_bot/internal.rb +91 -0
  35. data/lib/factory_bot/linter.rb +41 -28
  36. data/lib/factory_bot/null_factory.rb +13 -4
  37. data/lib/factory_bot/null_object.rb +2 -6
  38. data/lib/factory_bot/registry.rb +17 -8
  39. data/lib/factory_bot/reload.rb +2 -3
  40. data/lib/factory_bot/sequence.rb +5 -6
  41. data/lib/factory_bot/strategy/stub.rb +37 -32
  42. data/lib/factory_bot/strategy_calculator.rb +1 -1
  43. data/lib/factory_bot/strategy_syntax_method_registrar.rb +13 -2
  44. data/lib/factory_bot/syntax/default.rb +13 -25
  45. data/lib/factory_bot/syntax/methods.rb +32 -9
  46. data/lib/factory_bot/syntax.rb +2 -2
  47. data/lib/factory_bot/trait.rb +7 -4
  48. data/lib/factory_bot/version.rb +1 -1
  49. data/lib/factory_bot.rb +71 -140
  50. metadata +46 -34
  51. data/NEWS +0 -306
  52. data/lib/factory_bot/attribute/static.rb +0 -16
  53. data/lib/factory_bot/declaration/static.rb +0 -26
  54. data/lib/factory_bot/decorator/class_key_hash.rb +0 -28
@@ -4,7 +4,7 @@ module FactoryBot
4
4
  include Enumerable
5
5
 
6
6
  def initialize(name = nil, attributes = [])
7
- @name = name
7
+ @name = name
8
8
  @attributes = attributes
9
9
  end
10
10
 
@@ -54,7 +54,8 @@ module FactoryBot
54
54
 
55
55
  def ensure_attribute_not_self_referencing!(attribute)
56
56
  if attribute.respond_to?(:factory) && attribute.factory == @name
57
- raise AssociationDefinitionError, "Self-referencing association '#{attribute.name}' in '#{attribute.factory}'"
57
+ message = "Self-referencing association '#{attribute.name}' in '#{attribute.factory}'"
58
+ raise AssociationDefinitionError, message
58
59
  end
59
60
  end
60
61
 
@@ -3,16 +3,15 @@ module FactoryBot
3
3
  attr_reader :name
4
4
 
5
5
  def initialize(name, block)
6
- @name = name.to_sym
6
+ @name = name.to_sym
7
7
  @block = block
8
- ensure_valid_callback_name!
9
8
  end
10
9
 
11
10
  def run(instance, evaluator)
12
11
  case block.arity
13
- when 1, -1 then syntax_runner.instance_exec(instance, &block)
12
+ when 1, -1, -2 then syntax_runner.instance_exec(instance, &block)
14
13
  when 2 then syntax_runner.instance_exec(instance, evaluator, &block)
15
- else syntax_runner.instance_exec(&block)
14
+ else syntax_runner.instance_exec(&block)
16
15
  end
17
16
  end
18
17
 
@@ -22,17 +21,11 @@ module FactoryBot
22
21
  end
23
22
 
24
23
  protected
24
+
25
25
  attr_reader :block
26
26
 
27
27
  private
28
28
 
29
- def ensure_valid_callback_name!
30
- unless FactoryBot.callback_names.include?(name)
31
- raise InvalidCallbackNameError, "#{name} is not a valid callback name. " +
32
- "Valid callback names are #{FactoryBot.callback_names.inspect}"
33
- end
34
- end
35
-
36
29
  def syntax_runner
37
30
  @syntax_runner ||= SyntaxRunner.new
38
31
  end
@@ -1,21 +1,25 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  class Configuration
4
- attr_reader :factories, :sequences, :traits, :strategies, :callback_names
5
-
6
- attr_accessor :allow_class_lookup, :use_parent_strategy
4
+ attr_reader(
5
+ :callback_names,
6
+ :factories,
7
+ :inline_sequences,
8
+ :sequences,
9
+ :strategies,
10
+ :traits
11
+ )
7
12
 
8
13
  def initialize
9
- @factories = Decorator::DisallowsDuplicatesRegistry.new(Registry.new('Factory'))
10
- @sequences = Decorator::DisallowsDuplicatesRegistry.new(Registry.new('Sequence'))
11
- @traits = Decorator::DisallowsDuplicatesRegistry.new(Registry.new('Trait'))
12
- @strategies = Registry.new('Strategy')
14
+ @factories = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Factory"))
15
+ @sequences = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Sequence"))
16
+ @traits = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Trait"))
17
+ @strategies = Registry.new("Strategy")
13
18
  @callback_names = Set.new
14
- @definition = Definition.new
15
-
16
- @allow_class_lookup = true
19
+ @definition = Definition.new(:configuration)
20
+ @inline_sequences = []
17
21
 
18
- to_create { |instance| instance.save! }
22
+ to_create(&:save!)
19
23
  initialize_with { new }
20
24
  end
21
25
 
@@ -25,13 +29,5 @@ module FactoryBot
25
29
  def initialize_with(&block)
26
30
  @definition.define_constructor(&block)
27
31
  end
28
-
29
- def duplicate_attribute_assignment_from_initialize_with
30
- false
31
- end
32
-
33
- def duplicate_attribute_assignment_from_initialize_with=(value)
34
- ActiveSupport::Deprecation.warn 'Assignment of duplicate_attribute_assignment_from_initialize_with is unnecessary as this is now default behavior in FactoryBot 4.0; this line can be removed', caller
35
- end
36
32
  end
37
33
  end
@@ -6,22 +6,52 @@ module FactoryBot
6
6
  super(name, false)
7
7
  @options = options.dup
8
8
  @overrides = options.extract_options!
9
+ @factory_name = @overrides.delete(:factory) || name
9
10
  @traits = options
10
11
  end
11
12
 
12
13
  def ==(other)
13
- name == other.name &&
14
+ self.class == other.class &&
15
+ name == other.name &&
14
16
  options == other.options
15
17
  end
16
18
 
17
19
  protected
20
+
18
21
  attr_reader :options
19
22
 
20
23
  private
21
24
 
25
+ attr_reader :factory_name, :overrides, :traits
26
+
22
27
  def build
23
- factory_name = @overrides[:factory] || name
24
- [Attribute::Association.new(name, factory_name, [@traits, @overrides.except(:factory)].flatten)]
28
+ raise_if_arguments_are_declarations!
29
+
30
+ [
31
+ Attribute::Association.new(
32
+ name,
33
+ factory_name,
34
+ [traits, overrides].flatten
35
+ )
36
+ ]
37
+ end
38
+
39
+ def raise_if_arguments_are_declarations!
40
+ if factory_name.is_a?(Declaration)
41
+ raise ArgumentError.new(<<~MSG)
42
+ Association '#{name}' received an invalid factory argument.
43
+ Did you mean? 'factory: :#{factory_name.name}'
44
+ MSG
45
+ end
46
+
47
+ overrides.each do |attribute, value|
48
+ if value.is_a?(Declaration)
49
+ raise ArgumentError.new(<<~MSG)
50
+ Association '#{name}' received an invalid attribute override.
51
+ Did you mean? '#{attribute}: :#{value.name}'
52
+ MSG
53
+ end
54
+ end
25
55
  end
26
56
  end
27
57
  end
@@ -8,12 +8,14 @@ module FactoryBot
8
8
  end
9
9
 
10
10
  def ==(other)
11
- name == other.name &&
11
+ self.class == other.class &&
12
+ name == other.name &&
12
13
  ignored == other.ignored &&
13
14
  block == other.block
14
15
  end
15
16
 
16
17
  protected
18
+
17
19
  attr_reader :block
18
20
 
19
21
  private
@@ -8,12 +8,14 @@ module FactoryBot
8
8
  end
9
9
 
10
10
  def ==(other)
11
- name == other.name &&
11
+ self.class == other.class &&
12
+ name == other.name &&
12
13
  factory == other.factory &&
13
14
  ignored == other.ignored
14
15
  end
15
16
 
16
17
  protected
18
+
17
19
  attr_reader :factory
18
20
 
19
21
  private
@@ -21,8 +23,11 @@ module FactoryBot
21
23
  def build
22
24
  if FactoryBot.factories.registered?(name)
23
25
  [Attribute::Association.new(name, name, {})]
24
- elsif FactoryBot.sequences.registered?(name)
26
+ elsif FactoryBot::Internal.sequences.registered?(name)
25
27
  [Attribute::Sequence.new(name, name, @ignored)]
28
+ elsif @factory.name.to_s == name.to_s
29
+ message = "Self-referencing trait '#{@name}'"
30
+ raise TraitDefinitionError, message
26
31
  else
27
32
  @factory.inherit_traits([name])
28
33
  []
@@ -1,7 +1,6 @@
1
- require 'factory_bot/declaration/static'
2
- require 'factory_bot/declaration/dynamic'
3
- require 'factory_bot/declaration/association'
4
- require 'factory_bot/declaration/implicit'
1
+ require "factory_bot/declaration/dynamic"
2
+ require "factory_bot/declaration/association"
3
+ require "factory_bot/declaration/implicit"
5
4
 
6
5
  module FactoryBot
7
6
  # @api private
@@ -9,7 +8,7 @@ module FactoryBot
9
8
  attr_reader :name
10
9
 
11
10
  def initialize(name, ignored = false)
12
- @name = name
11
+ @name = name
13
12
  @ignored = ignored
14
13
  end
15
14
 
@@ -18,6 +17,7 @@ module FactoryBot
18
17
  end
19
18
 
20
19
  protected
20
+
21
21
  attr_reader :ignored
22
22
  end
23
23
  end
@@ -5,8 +5,8 @@ module FactoryBot
5
5
 
6
6
  def initialize(name = nil)
7
7
  @declarations = []
8
- @name = name
9
- @overridable = false
8
+ @name = name
9
+ @overridable = false
10
10
  end
11
11
 
12
12
  def declare_attribute(declaration)
@@ -39,7 +39,7 @@ module FactoryBot
39
39
  end
40
40
 
41
41
  def to_attributes
42
- @declarations.inject([]) { |result, declaration| result += declaration.to_attributes }
42
+ @declarations.reduce([]) { |result, declaration| result + declaration.to_attributes }
43
43
  end
44
44
 
45
45
  def overridable?
@@ -8,7 +8,7 @@ module FactoryBot
8
8
 
9
9
  def attributes
10
10
  @attributes.each_with_object({}) do |attribute_name, result|
11
- result[attribute_name] = send(attribute_name)
11
+ result[attribute_name] = @component.send(attribute_name)
12
12
  end
13
13
  end
14
14
  end
@@ -6,10 +6,11 @@ module FactoryBot
6
6
  @invoked_methods = []
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block)
9
+ def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing
10
10
  @invoked_methods << name
11
11
  super
12
12
  end
13
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
13
14
 
14
15
  def __invoked_methods__
15
16
  @invoked_methods.uniq
@@ -6,12 +6,28 @@ module FactoryBot
6
6
  @component = component
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block)
10
- @component.send(name, *args, &block)
9
+ if ::Gem::Version.new(::RUBY_VERSION) >= ::Gem::Version.new("2.7")
10
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
11
+ def method_missing(...) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
12
+ @component.send(...)
13
+ end
14
+
15
+ def send(...)
16
+ __send__(...)
17
+ end
18
+ RUBY
19
+ else
20
+ def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
21
+ @component.send(name, *args, &block)
22
+ end
23
+
24
+ def send(symbol, *args, &block)
25
+ __send__(symbol, *args, &block)
26
+ end
11
27
  end
12
28
 
13
- def send(symbol, *args, &block)
14
- __send__(symbol, *args, &block)
29
+ def respond_to_missing?(name, include_private = false)
30
+ @component.respond_to?(name, true) || super
15
31
  end
16
32
 
17
33
  def self.const_missing(name)
@@ -1,18 +1,21 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  class Definition
4
- attr_reader :defined_traits, :declarations
5
-
6
- def initialize(name = nil, base_traits = [])
7
- @declarations = DeclarationList.new(name)
8
- @callbacks = []
9
- @defined_traits = Set.new
10
- @to_create = nil
11
- @base_traits = base_traits
4
+ attr_reader :defined_traits, :declarations, :name, :registered_enums
5
+
6
+ def initialize(name, base_traits = [])
7
+ @name = name
8
+ @declarations = DeclarationList.new(name)
9
+ @callbacks = []
10
+ @defined_traits = Set.new
11
+ @registered_enums = []
12
+ @to_create = nil
13
+ @base_traits = base_traits
12
14
  @additional_traits = []
13
- @constructor = nil
14
- @attributes = nil
15
- @compiled = false
15
+ @constructor = nil
16
+ @attributes = nil
17
+ @compiled = false
18
+ @expanded_enum_traits = false
16
19
  end
17
20
 
18
21
  delegate :declare_attribute, to: :declarations
@@ -27,7 +30,7 @@ module FactoryBot
27
30
  end
28
31
 
29
32
  def to_create(&block)
30
- if block_given?
33
+ if block
31
34
  @to_create = block
32
35
  else
33
36
  aggregate_from_traits_and_self(:to_create) { @to_create }.last
@@ -42,13 +45,15 @@ module FactoryBot
42
45
  aggregate_from_traits_and_self(:callbacks) { @callbacks }
43
46
  end
44
47
 
45
- def compile
48
+ def compile(klass = nil)
46
49
  unless @compiled
50
+ expand_enum_traits(klass) unless klass.nil?
51
+
47
52
  declarations.attributes
48
53
 
49
54
  defined_traits.each do |defined_trait|
50
- base_traits.each { |bt| bt.define_trait defined_trait }
51
- additional_traits.each { |bt| bt.define_trait defined_trait }
55
+ base_traits.each { |bt| bt.define_trait defined_trait }
56
+ additional_traits.each { |at| at.define_trait defined_trait }
52
57
  end
53
58
 
54
59
  @compiled = true
@@ -73,13 +78,17 @@ module FactoryBot
73
78
  end
74
79
 
75
80
  def skip_create
76
- @to_create = ->(instance) { }
81
+ @to_create = ->(instance) {}
77
82
  end
78
83
 
79
84
  def define_trait(trait)
80
85
  @defined_traits.add(trait)
81
86
  end
82
87
 
88
+ def register_enum(enum)
89
+ @registered_enums << enum
90
+ end
91
+
83
92
  def define_constructor(&block)
84
93
  @constructor = block
85
94
  end
@@ -94,7 +103,6 @@ module FactoryBot
94
103
 
95
104
  def callback(*names, &block)
96
105
  names.each do |name|
97
- FactoryBot.register_callback(name)
98
106
  add_callback(Callback.new(name, block))
99
107
  end
100
108
  end
@@ -103,6 +111,20 @@ module FactoryBot
103
111
 
104
112
  def base_traits
105
113
  @base_traits.map { |name| trait_by_name(name) }
114
+ rescue KeyError => error
115
+ raise error_with_definition_name(error)
116
+ end
117
+
118
+ def error_with_definition_name(error)
119
+ message = error.message
120
+ message.insert(
121
+ message.index("\nDid you mean?") || message.length,
122
+ " referenced within \"#{name}\" definition"
123
+ )
124
+
125
+ error.class.new(message).tap do |new_error|
126
+ new_error.set_backtrace(error.backtrace)
127
+ end
106
128
  end
107
129
 
108
130
  def additional_traits
@@ -110,17 +132,19 @@ module FactoryBot
110
132
  end
111
133
 
112
134
  def trait_by_name(name)
113
- trait_for(name) || FactoryBot.trait_by_name(name)
135
+ trait_for(name) || Internal.trait_by_name(name)
114
136
  end
115
137
 
116
138
  def trait_for(name)
117
- defined_traits.detect { |trait| trait.name == name }
139
+ @defined_traits_by_name ||= defined_traits.each_with_object({}) { |t, memo| memo[t.name] ||= t }
140
+ @defined_traits_by_name[name.to_s]
118
141
  end
119
142
 
120
143
  def initialize_copy(source)
121
144
  super
122
145
  @attributes = nil
123
- @compiled = false
146
+ @compiled = false
147
+ @defined_traits_by_name = nil
124
148
  end
125
149
 
126
150
  def aggregate_from_traits_and_self(method_name, &block)
@@ -129,8 +153,32 @@ module FactoryBot
129
153
  [
130
154
  base_traits.map(&method_name),
131
155
  instance_exec(&block),
132
- additional_traits.map(&method_name),
156
+ additional_traits.map(&method_name)
133
157
  ].flatten.compact
134
158
  end
159
+
160
+ def expand_enum_traits(klass)
161
+ return if @expanded_enum_traits
162
+
163
+ if automatically_register_defined_enums?(klass)
164
+ automatically_register_defined_enums(klass)
165
+ end
166
+
167
+ registered_enums.each do |enum|
168
+ traits = enum.build_traits(klass)
169
+ traits.each { |trait| define_trait(trait) }
170
+ end
171
+
172
+ @expanded_enum_traits = true
173
+ end
174
+
175
+ def automatically_register_defined_enums(klass)
176
+ klass.defined_enums.each_key { |name| register_enum(Enum.new(name)) }
177
+ end
178
+
179
+ def automatically_register_defined_enums?(klass)
180
+ FactoryBot.automatically_define_enum_traits &&
181
+ klass.respond_to?(:defined_enums)
182
+ end
135
183
  end
136
184
  end
@@ -1,16 +1,6 @@
1
1
  module FactoryBot
2
2
  class DefinitionHierarchy
3
- def callbacks
4
- FactoryBot.callbacks
5
- end
6
-
7
- def constructor
8
- FactoryBot.constructor
9
- end
10
-
11
- def to_create
12
- FactoryBot.to_create
13
- end
3
+ delegate :callbacks, :constructor, :to_create, to: Internal
14
4
 
15
5
  def self.build_from_definition(definition)
16
6
  build_to_create(&definition.to_create)