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 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+ # 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: # # factory :user do # name { 'Billy Idol' } # end # # and: # # factory :user do # add_attribute(:name) { 'Billy Idol' } # end # # are equivalent. # # 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: # # factory :user, traits: [:admin] do # email { generate(:email) } # association :account # end # # and: # # factory :user do # admin # email # account # end # # are equivalent. def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing, Style/MethodMissingSuper 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: # factory :user do # sequence(:email) { |n| "person#{n}@example.com" } # end # # Is equal to: # sequence(:email) { |n| "person#{n}@example.com" } # # factory :user do # email { FactoryBot.generate(:email) } # end # # Except that no globally available sequence will be defined. 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: # factory :user do # name 'Joey' # end # # factory :post do # 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 def trait(name, &block) @definition.define_trait(Trait.new(name, &block)) end # 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