factory_bot 5.2.0 → 6.0.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.
data/NEWS.md CHANGED
@@ -1,8 +1,15 @@
1
1
  # News
2
2
 
3
+ ## 6.0.0 (June 18, 2020)
4
+ * Added: automatic definition of traits for Active Record enum attributes, enabled by default
5
+ * Added: `traits_for_enum` method to define traits for non-Active Record enums
6
+ * Added: `build_stubbed_starting_id=` option to define the starting id for `build_stubbed`
7
+ * Removed: deprecated methods on the top-level `FactoryBot` module meant only for internal use
8
+ * Removed: support for EOL versions of Ruby (2.3, 2.4) and Rails (4.2)
9
+
3
10
  ## 5.2.0 (April 24, 2020)
4
11
  * Added: Pass index to block for `*_list` methods
5
- * Deprecated: top-level methods meant only for internal use: `callbacks`, `configuration`, `constructor`, `initialize_with`, `register_sequence`, `resent_configuration`, `skip_create`, `to_create`
12
+ * Deprecated: methods on the top-level `FactoryBot` module meant only for internal use: `callbacks`, `configuration`, `constructor`, `initialize_with`, `register_sequence`, `resent_configuration`, `skip_create`, `to_create`
6
13
 
7
14
  ## 5.1.2 (March 25, 2020)
8
15
  * Fixed: Ruby 2.7 keyword deprecation warning in FactoryBot.lint
@@ -17,7 +24,7 @@
17
24
  * Fixed: avoid undefining inherited evaluator methods
18
25
  * Fixed: avoid stubbing id for records without a primary key
19
26
  * Fixed: raise a helpful error for self-referencing traits to avoid a `SystemStackError`
20
- * Deprecated: top-level methods meant only for internal use: `allow_class_lookup`, `allow_class_lookup`=, `register_trait`, `trait_by_name`, `traits`, `sequence_by_name`, `sequences`, `factory_by_name`, `register_factory`, `callback_names`, `register_callback`, `register_default_callbacks`, `register_default_strategies`, `strategies`
27
+ * Deprecated: methods on the top-level `FactoryBot` module meant only for internal use: `allow_class_lookup`, `allow_class_lookup`=, `register_trait`, `trait_by_name`, `traits`, `sequence_by_name`, `sequences`, `factory_by_name`, `register_factory`, `callback_names`, `register_callback`, `register_default_callbacks`, `register_default_strategies`, `strategies`
21
28
 
22
29
  ## 5.0.2 (February 22, 2019)
23
30
  * Bugfix: raise "Trait not registered" error when passing invalid trait arguments
@@ -32,6 +32,7 @@ require "factory_bot/declaration"
32
32
  require "factory_bot/sequence"
33
33
  require "factory_bot/attribute_list"
34
34
  require "factory_bot/trait"
35
+ require "factory_bot/enum"
35
36
  require "factory_bot/aliases"
36
37
  require "factory_bot/definition"
37
38
  require "factory_bot/definition_proxy"
@@ -48,11 +49,14 @@ require "factory_bot/linter"
48
49
  require "factory_bot/version"
49
50
 
50
51
  module FactoryBot
51
- Deprecation = ActiveSupport::Deprecation.new("6.0", "factory_bot")
52
+ Deprecation = ActiveSupport::Deprecation.new("7.0", "factory_bot")
52
53
 
53
54
  mattr_accessor :use_parent_strategy, instance_accessor: false
54
55
  self.use_parent_strategy = true
55
56
 
57
+ mattr_accessor :automatically_define_enum_traits, instance_accessor: false
58
+ self.automatically_define_enum_traits = true
59
+
56
60
  # Look for errors in factories and (optionally) their traits.
57
61
  # Parameters:
58
62
  # factories - which factories to lint; omit for all factories
@@ -66,60 +70,22 @@ module FactoryBot
66
70
  Linter.new(factories_to_lint, **options).lint!
67
71
  end
68
72
 
69
- class << self
70
- delegate :callback_names,
71
- :callbacks,
72
- :configuration,
73
- :constructor,
74
- :factories,
75
- :factory_by_name,
76
- :initialize_with,
77
- :register_callback,
78
- :register_default_callbacks,
79
- :register_default_strategies,
80
- :register_factory,
81
- :register_sequence,
82
- :register_strategy,
83
- :register_trait,
84
- :reset_configuration,
85
- :rewind_sequences,
86
- :sequence_by_name,
87
- :sequences,
88
- :skip_create,
89
- :strategies,
90
- :strategy_by_name,
91
- :to_create,
92
- :trait_by_name,
93
- :traits,
94
- to: Internal
95
-
96
- attr_accessor :allow_class_lookup
73
+ # Set the starting value for ids when using the build_stubbed strategy
74
+ #
75
+ # Arguments:
76
+ # * starting_id +Integer+
77
+ # The new starting id value.
78
+ def self.build_stubbed_starting_id=(starting_id)
79
+ Strategy::Stub.next_id = starting_id - 1
80
+ end
97
81
 
98
- deprecate :allow_class_lookup,
99
- :allow_class_lookup=,
100
- :callback_names,
101
- :callbacks,
102
- :configuration,
103
- :constructor,
104
- :factory_by_name,
105
- :initialize_with,
106
- :register_callback,
107
- :register_default_callbacks,
108
- :register_default_strategies,
109
- :register_factory,
110
- :register_sequence,
111
- :register_trait,
112
- :reset_configuration,
113
- :sequence_by_name,
114
- :sequences,
115
- :skip_create,
116
- :strategies,
117
- :to_create,
118
- :trait_by_name,
119
- :traits,
120
- deprecator: Deprecation
82
+ class << self
83
+ delegate :factories,
84
+ :register_strategy,
85
+ :rewind_sequences,
86
+ :strategy_by_name,
87
+ to: Internal
121
88
  end
122
89
  end
123
90
 
124
91
  FactoryBot::Internal.register_default_strategies
125
- FactoryBot::Internal.register_default_callbacks
@@ -5,14 +5,14 @@ module FactoryBot
5
5
 
6
6
  self.aliases = [
7
7
  [/(.+)_id/, '\1'],
8
- [/(.*)/, '\1_id'],
8
+ [/(.*)/, '\1_id']
9
9
  ]
10
10
 
11
11
  def self.aliases_for(attribute)
12
- aliases.map do |(pattern, replace)|
12
+ aliases.map { |(pattern, replace)|
13
13
  if pattern.match(attribute.to_s)
14
14
  attribute.to_s.sub(pattern, replace).to_sym
15
15
  end
16
- end.compact << attribute
16
+ }.compact << attribute
17
17
  end
18
18
  end
@@ -6,12 +6,12 @@ module FactoryBot
6
6
 
7
7
  def initialize(name, factory, overrides)
8
8
  super(name, false)
9
- @factory = factory
9
+ @factory = factory
10
10
  @overrides = overrides
11
11
  end
12
12
 
13
13
  def to_proc
14
- factory = @factory
14
+ factory = @factory
15
15
  overrides = @overrides
16
16
  traits_and_overrides = [factory, overrides].flatten
17
17
  factory_name = traits_and_overrides.shift
@@ -14,7 +14,7 @@ module FactoryBot
14
14
  value = case block.arity
15
15
  when 1, -1 then instance_exec(self, &block)
16
16
  else instance_exec(&block)
17
- end
17
+ end
18
18
  raise SequenceAbuseError if FactoryBot::Sequence === value
19
19
 
20
20
  value
@@ -2,10 +2,10 @@ module FactoryBot
2
2
  # @api private
3
3
  class AttributeAssigner
4
4
  def initialize(evaluator, build_class, &instance_builder)
5
- @build_class = build_class
6
- @instance_builder = instance_builder
7
- @evaluator = evaluator
8
- @attribute_list = evaluator.class.attribute_list
5
+ @build_class = build_class
6
+ @instance_builder = instance_builder
7
+ @evaluator = evaluator
8
+ @attribute_list = evaluator.class.attribute_list
9
9
  @attribute_names_assigned = []
10
10
  end
11
11
 
@@ -22,9 +22,8 @@ module FactoryBot
22
22
  def hash
23
23
  @evaluator.instance = build_hash
24
24
 
25
- attributes_to_set_on_hash.reduce({}) do |result, attribute|
25
+ attributes_to_set_on_hash.each_with_object({}) do |attribute, result|
26
26
  result[attribute] = get(attribute)
27
- result
28
27
  end
29
28
  end
30
29
 
@@ -33,13 +32,13 @@ module FactoryBot
33
32
  def method_tracking_evaluator
34
33
  @method_tracking_evaluator ||= Decorator::AttributeHash.new(
35
34
  decorated_evaluator,
36
- attribute_names_to_assign,
35
+ attribute_names_to_assign
37
36
  )
38
37
  end
39
38
 
40
39
  def decorated_evaluator
41
40
  Decorator::InvocationTracker.new(
42
- Decorator::NewConstructor.new(@evaluator, @build_class),
41
+ Decorator::NewConstructor.new(@evaluator, @build_class)
43
42
  )
44
43
  end
45
44
 
@@ -96,11 +95,11 @@ module FactoryBot
96
95
  end
97
96
 
98
97
  def alias_names_to_ignore
99
- @attribute_list.non_ignored.flat_map do |attribute|
98
+ @attribute_list.non_ignored.flat_map { |attribute|
100
99
  override_names.map do |override|
101
100
  attribute.name if ignorable_alias?(attribute, override)
102
101
  end
103
- end.compact
102
+ }.compact
104
103
  end
105
104
 
106
105
  def ignorable_alias?(attribute, override)
@@ -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
 
@@ -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
12
  when 1, -1 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
 
@@ -27,13 +26,6 @@ module FactoryBot
27
26
 
28
27
  private
29
28
 
30
- def ensure_valid_callback_name!
31
- unless FactoryBot::Internal.callback_names.include?(name)
32
- raise InvalidCallbackNameError, "#{name} is not a valid callback name. " +
33
- "Valid callback names are #{FactoryBot::Internal.callback_names.inspect}"
34
- end
35
- end
36
-
37
29
  def syntax_runner
38
30
  @syntax_runner ||= SyntaxRunner.new
39
31
  end
@@ -7,16 +7,16 @@ module FactoryBot
7
7
  :inline_sequences,
8
8
  :sequences,
9
9
  :strategies,
10
- :traits,
10
+ :traits
11
11
  )
12
12
 
13
13
  def initialize
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")
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")
18
18
  @callback_names = Set.new
19
- @definition = Definition.new(:configuration)
19
+ @definition = Definition.new(:configuration)
20
20
  @inline_sequences = []
21
21
 
22
22
  to_create(&:save!)
@@ -24,7 +24,7 @@ module FactoryBot
24
24
  end
25
25
 
26
26
  delegate :to_create, :skip_create, :constructor, :before, :after,
27
- :callback, :callbacks, to: :@definition
27
+ :callback, :callbacks, to: :@definition
28
28
 
29
29
  def initialize_with(&block)
30
30
  @definition.define_constructor(&block)
@@ -8,7 +8,7 @@ module FactoryBot
8
8
  attr_reader :name
9
9
 
10
10
  def initialize(name, ignored = false)
11
- @name = name
11
+ @name = name
12
12
  @ignored = ignored
13
13
  end
14
14
 
@@ -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)
@@ -17,9 +17,5 @@ module FactoryBot
17
17
  def send(symbol, *args, &block)
18
18
  __send__(symbol, *args, &block)
19
19
  end
20
-
21
- def self.const_missing(name)
22
- ::Object.const_get(name)
23
- end
24
20
  end
25
21
  end
@@ -1,19 +1,20 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  class Definition
4
- attr_reader :defined_traits, :declarations, :name
4
+ attr_reader :defined_traits, :declarations, :name, :registered_enums
5
5
 
6
6
  def initialize(name, base_traits = [])
7
- @name = name
8
- @declarations = DeclarationList.new(name)
9
- @callbacks = []
10
- @defined_traits = Set.new
11
- @to_create = nil
12
- @base_traits = 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
13
14
  @additional_traits = []
14
- @constructor = nil
15
- @attributes = nil
16
- @compiled = false
15
+ @constructor = nil
16
+ @attributes = nil
17
+ @compiled = false
17
18
  end
18
19
 
19
20
  delegate :declare_attribute, to: :declarations
@@ -43,12 +44,14 @@ module FactoryBot
43
44
  aggregate_from_traits_and_self(:callbacks) { @callbacks }
44
45
  end
45
46
 
46
- def compile
47
+ def compile(klass = nil)
47
48
  unless @compiled
49
+ expand_enum_traits(klass) unless klass.nil?
50
+
48
51
  declarations.attributes
49
52
 
50
53
  defined_traits.each do |defined_trait|
51
- base_traits.each { |bt| bt.define_trait defined_trait }
54
+ base_traits.each { |bt| bt.define_trait defined_trait }
52
55
  additional_traits.each { |at| at.define_trait defined_trait }
53
56
  end
54
57
 
@@ -81,6 +84,10 @@ module FactoryBot
81
84
  @defined_traits.add(trait)
82
85
  end
83
86
 
87
+ def register_enum(enum)
88
+ @registered_enums << enum
89
+ end
90
+
84
91
  def define_constructor(&block)
85
92
  @constructor = block
86
93
  end
@@ -95,7 +102,6 @@ module FactoryBot
95
102
 
96
103
  def callback(*names, &block)
97
104
  names.each do |name|
98
- FactoryBot::Internal.register_callback(name)
99
105
  add_callback(Callback.new(name, block))
100
106
  end
101
107
  end
@@ -122,7 +128,7 @@ module FactoryBot
122
128
  def initialize_copy(source)
123
129
  super
124
130
  @attributes = nil
125
- @compiled = false
131
+ @compiled = false
126
132
  @defined_traits_by_name = nil
127
133
  end
128
134
 
@@ -132,8 +138,28 @@ module FactoryBot
132
138
  [
133
139
  base_traits.map(&method_name),
134
140
  instance_exec(&block),
135
- additional_traits.map(&method_name),
141
+ additional_traits.map(&method_name)
136
142
  ].flatten.compact
137
143
  end
144
+
145
+ def expand_enum_traits(klass)
146
+ if automatically_register_defined_enums?(klass)
147
+ automatically_register_defined_enums(klass)
148
+ end
149
+
150
+ registered_enums.each do |enum|
151
+ traits = enum.build_traits(klass)
152
+ traits.each { |trait| define_trait(trait) }
153
+ end
154
+ end
155
+
156
+ def automatically_register_defined_enums(klass)
157
+ klass.defined_enums.each_key { |name| register_enum(Enum.new(name)) }
158
+ end
159
+
160
+ def automatically_register_defined_enums?(klass)
161
+ FactoryBot.automatically_define_enum_traits &&
162
+ klass.respond_to?(:defined_enums)
163
+ end
138
164
  end
139
165
  end
@@ -1,6 +1,6 @@
1
1
  module FactoryBot
2
2
  class DefinitionProxy
3
- UNPROXIED_METHODS = %w(
3
+ UNPROXIED_METHODS = %w[
4
4
  __send__
5
5
  __id__
6
6
  nil?
@@ -13,7 +13,7 @@ module FactoryBot
13
13
  raise
14
14
  caller
15
15
  method
16
- ).freeze
16
+ ].freeze
17
17
 
18
18
  (instance_methods + private_instance_methods).each do |method|
19
19
  undef_method(method) unless UNPROXIED_METHODS.include?(method.to_s)
@@ -24,8 +24,8 @@ module FactoryBot
24
24
  attr_reader :child_factories
25
25
 
26
26
  def initialize(definition, ignore = false)
27
- @definition = definition
28
- @ignore = ignore
27
+ @definition = definition
28
+ @ignore = ignore
29
29
  @child_factories = []
30
30
  end
31
31
 
@@ -152,7 +152,7 @@ module FactoryBot
152
152
  if block_given?
153
153
  raise AssociationDefinitionError.new(
154
154
  "Unexpected block passed to '#{name}' association "\
155
- "in '#{@definition.name}' factory",
155
+ "in '#{@definition.name}' factory"
156
156
  )
157
157
  else
158
158
  declaration = Declaration::Association.new(name, *options)
@@ -176,6 +176,64 @@ module FactoryBot
176
176
  @definition.define_trait(Trait.new(name, &block))
177
177
  end
178
178
 
179
+ # Creates traits for enumerable values.
180
+ #
181
+ # Example:
182
+ # factory :task do
183
+ # traits_for_enum :status, [:started, :finished]
184
+ # end
185
+ #
186
+ # Equivalent to:
187
+ # factory :task do
188
+ # trait :started do
189
+ # status { :started }
190
+ # end
191
+ #
192
+ # trait :finished do
193
+ # status { :finished }
194
+ # end
195
+ # end
196
+ #
197
+ # Example:
198
+ # factory :task do
199
+ # traits_for_enum :status, {started: 1, finished: 2}
200
+ # end
201
+ #
202
+ # Example:
203
+ # class Task
204
+ # def statuses
205
+ # {started: 1, finished: 2}
206
+ # end
207
+ # end
208
+ #
209
+ # factory :task do
210
+ # traits_for_enum :status
211
+ # end
212
+ #
213
+ # Both equivalent to:
214
+ # factory :task do
215
+ # trait :started do
216
+ # status { 1 }
217
+ # end
218
+ #
219
+ # trait :finished do
220
+ # status { 2 }
221
+ # end
222
+ # end
223
+ #
224
+ #
225
+ # Arguments:
226
+ # attribute_name: +Symbol+ or +String+
227
+ # the name of the attribute these traits will set the value of
228
+ # values: +Array+, +Hash+, or other +Enumerable+
229
+ # An array of trait names, or a mapping of trait names to values for
230
+ # those traits. When this argument is not provided, factory_bot will
231
+ # attempt to get the values by calling the pluralized `attribute_name`
232
+ # class method.
233
+ def traits_for_enum(attribute_name, values = nil)
234
+ @definition.register_enum(Enum.new(attribute_name, values))
235
+ end
236
+
179
237
  def initialize_with(&block)
180
238
  @definition.define_constructor(&block)
181
239
  end