1
0
Fork 0
mirror of https://github.com/thoughtbot/factory_bot.git synced 2022-11-09 11:43:51 -05:00
thoughtbot--factory_bot/lib/factory_bot/definition_proxy.rb

257 lines
6.9 KiB
Ruby
Raw Normal View History

module FactoryBot
class DefinitionProxy
UNPROXIED_METHODS = %w[
__send__
__id__
nil?
send
object_id
extend
instance_eval
initialize
block_given?
raise
caller
method
].freeze
(instance_methods + private_instance_methods).each do |method|
undef_method(method) unless UNPROXIED_METHODS.include?(method.to_s)
end
delegate :before, :after, :callback, to: :@definition
attr_reader :child_factories
2011-08-12 14:49:21 -04:00
def initialize(definition, ignore = false)
@definition = definition
@ignore = ignore
@child_factories = []
end
def singleton_method_added(name)
message = "Defining methods in blocks (trait or factory) is not supported (#{name})"
raise FactoryBot::MethodDefinitionError, message
end
# Adds an attribute to the factory.
# The attribute value will be generated "lazily"
# by calling the block whenever an instance is generated.
# The block will not be called if the
# attribute is overridden for a specific instance.
#
# Arguments:
# * name: +Symbol+ or +String+
2011-02-07 17:48:00 -05:00
# The name of this attribute. This will be assigned using "name=" for
# generated instances.
def add_attribute(name, &block)
declaration = Declaration::Dynamic.new(name, @ignore, block)
@definition.declare_attribute(declaration)
end
def transient(&block)
proxy = DefinitionProxy.new(@definition, true)
proxy.instance_eval(&block)
end
# Calls add_attribute using the missing method name as the name of the
# attribute, so that:
#
2010-10-01 18:50:01 -04:00
# factory :user do
# name { 'Billy Idol' }
# end
#
# and:
#
2010-10-01 18:50:01 -04:00
# factory :user do
# add_attribute(:name) { 'Billy Idol' }
2010-10-01 18:50:01 -04:00
# end
#
2011-07-01 19:08:03 -04:00
# are equivalent.
2010-10-01 18:50:01 -04:00
#
# If no argument or block is given, factory_bot will first look for an
# association, then for a sequence, and finally for a trait with the same
# name. This means that given an "admin" trait, an "email" sequence, and an
# "account" factory:
2010-10-01 18:50:01 -04:00
#
# factory :user, traits: [:admin] do
# email { generate(:email) }
2010-10-01 18:50:01 -04:00
# association :account
# end
#
# and:
#
# factory :user do
# admin
2010-10-01 18:50:01 -04:00
# email
# account
# end
#
2011-07-01 19:08:03 -04:00
# are equivalent.
def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing
association_options = args.first
if association_options.nil?
__declare_attribute__(name, block)
elsif __valid_association_options?(association_options)
association(name, association_options)
else
raise NoMethodError.new(<<~MSG)
undefined method '#{name}' in '#{@definition.name}' factory
Did you mean? '#{name} { #{association_options.inspect} }'
MSG
end
end
# Adds an attribute that will have unique values generated by a sequence with
# a specified format.
#
# The result of:
2010-10-01 18:50:01 -04:00
# factory :user do
# sequence(:email) { |n| "person#{n}@example.com" }
# end
#
# Is equal to:
2010-10-01 18:50:01 -04:00
# sequence(:email) { |n| "person#{n}@example.com" }
#
2010-10-01 18:50:01 -04:00
# factory :user do
# email { FactoryBot.generate(:email) }
# end
#
# Except that no globally available sequence will be defined.
2012-04-06 14:41:13 -04:00
def sequence(name, *args, &block)
sequence = Sequence.new(name, *args, &block)
FactoryBot::Internal.register_inline_sequence(sequence)
add_attribute(name) { increment_sequence(sequence) }
end
# Adds an attribute that builds an association. The associated instance will
# be built using the same build strategy as the parent instance.
#
# Example:
2010-10-01 18:50:01 -04:00
# factory :user do
# name 'Joey'
# end
#
2010-10-01 18:50:01 -04:00
# factory :post do
2012-03-09 17:20:38 -05:00
# association :author, factory: :user
# end
#
# Arguments:
# * name: +Symbol+
# The name of this attribute.
# * options: +Hash+
#
# Options:
# * factory: +Symbol+ or +String+
# The name of the factory to use when building the associated instance.
# If no name is given, the name of the attribute is assumed to be the
# name of the factory. For example, a "user" association will by
# default use the "user" factory.
def association(name, *options)
if block_given?
raise AssociationDefinitionError.new(
"Unexpected block passed to '#{name}' association "\
"in '#{@definition.name}' factory"
)
else
declaration = Declaration::Association.new(name, *options)
@definition.declare_attribute(declaration)
end
end
def to_create(&block)
@definition.to_create(&block)
end
def skip_create
@definition.skip_create
end
def factory(name, options = {}, &block)
@child_factories << [name, options, block]
end
2011-08-12 14:49:21 -04:00
2011-08-12 16:16:17 -04:00
def trait(name, &block)
@definition.define_trait(Trait.new(name, &block))
2011-08-09 20:29:02 -04:00
end
Add functionality for enum traits (#1380) ## Enum traits Given a Rails model with an enum attribute: ```rb class Task < ActiveRecord::Base enum status: {queued: 0, started: 1, finished: 2} end ``` It is common for people to define traits for each possible value of the enum: ```rb FactoryBot.define do factory :task do trait :queued do status { :queued } end trait :started do status { :started } end trait :finished do status { :finished } end end end ``` With this commit, those trait definitions are no longer necessary—they are defined automatically by factory_bot. If automatically defining traits for enum attributes on every factory is not desired, it is possible to disable the feature by setting `FactoryBot.automatically_define_enum_traits = false` (see commit: [Allow opting out of automatically defining traits](https://github.com/thoughtbot/factory_bot/pull/1380/commits/5a20017351b08ce2ec9918d799e187e9eaa3ec32)). In that case, it is still possible to explicitly define traits for an enum attribute in a particular factory: ```rb FactoryBot.automatically_define_enum_traits = false FactoryBot.define do factory :task do traits_for_enum(:status) end end ``` It is also possible to use this feature for other enumerable values, not specifically tied to ActiveRecord enum attributes: ```rb class Task attr_accessor :status end FactoryBot.define do factory :task do traits_for_enum(:status, ["queued", "started", "finished"]) end end ``` The second argument here can be an enumerable object, including a Hash or Array. Closes #1049 Co-authored-by: Lance Johnson <lancejjohnson@gmail.com> Co-authored-by: PoTa <pota@mosfet.hu> Co-authored-by: Frida Casas <fridacasas.fc@gmail.com> Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
2020-05-01 17:50:51 -04:00
# Creates traits for enumerable values.
#
# Example:
# factory :task do
# traits_for_enum :status, [:started, :finished]
# end
#
# Equivalent to:
# factory :task do
# trait :started do
# status { :started }
# end
#
# trait :finished do
# status { :finished }
# end
# end
#
# Example:
# factory :task do
# traits_for_enum :status, {started: 1, finished: 2}
# end
#
# Example:
# class Task
# def statuses
# {started: 1, finished: 2}
# end
# end
#
# factory :task do
# traits_for_enum :status
# end
#
# Both equivalent to:
# factory :task do
# trait :started do
# status { 1 }
# end
#
# trait :finished do
# status { 2 }
# end
# end
#
#
# Arguments:
# attribute_name: +Symbol+ or +String+
# the name of the attribute these traits will set the value of
# values: +Array+, +Hash+, or other +Enumerable+
# An array of trait names, or a mapping of trait names to values for
# those traits. When this argument is not provided, factory_bot will
# attempt to get the values by calling the pluralized `attribute_name`
# class method.
def traits_for_enum(attribute_name, values = nil)
@definition.register_enum(Enum.new(attribute_name, values))
end
def initialize_with(&block)
@definition.define_constructor(&block)
end
private
def __declare_attribute__(name, block)
if block.nil?
declaration = Declaration::Implicit.new(name, @definition, @ignore)
@definition.declare_attribute(declaration)
else
add_attribute(name, &block)
end
end
def __valid_association_options?(options)
options.respond_to?(:has_key?) && options.has_key?(:factory)
end
end
end