factory_bot 5.2.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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