1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
|
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
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, ...)
sequence = Sequence.new(name, ...)
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
|