class MachineNotFoundError < StandardError; end class Machine cattr_accessor :machines #:nodoc: self.machines = {} cattr_accessor :sequences #:nodoc: self.sequences = {} # An Array of strings specifying locations that should be searched for # machine definitions. By default, machine will attempt to require # "machines," "test/machines," and "spec/machines." Only the first # existing file will be loaded. cattr_accessor :definition_file_paths def self.find_definitions #:nodoc: definition_file_paths.each do |file_path| begin require(file_path) break rescue LoadError end end end # Defines a new machine that sets up the default attributes for new objects. # # Arguments: # name: (Symbol) # A unique name used to identify this machine. # options: (Hash) # class: (Class) the class that will be used when generating instances for this # machine. If not specified, the class will be guessed from the # machine name. # extends: (Symbol) the name of a machine that will be extended by this one. # If provided, the attributes of the extended machine will be applied # to the object before the one being defined. # # Yields: # The object being created and an association helper # # Example: # # Machine.define :car do |car, machine| # car.make = 'GMC' # car.model = 'S-15' # end def self.define(name, options={}, &block) self.machines[name] = Machine.new(name, options, block) end # Defines a group of machines with a base set of attributes and then a set of # machines as children. This is useful as a namespacing technique or for any # case where you wish to define a set of objects that share a set of base # attributes. # # Arguments: # name: (Symbol) # A unique name to identify the group. This name will itself become a machine # that will build from the base attributes. # options: (Hash) # class: (Class) the class that will be used when generating instances for this # machine. If not specified, the class will be guessed from the # machine name. # # Example # # Machine.define_group :user do |group| # group.base do |user, machine| # user.password = 'password' # user.password_confirmation = 'password' # user.login = Machine.next(:login) # user.email = Machine.next(:email) # end # # group.define :super_user do |user, machine| # user.permissions = [machine.permission(:user => user)] # end # end # # Machine.build(:user) # Machine.build(:super_user) # def self.define_group(name, options={}, &block) group = MachineGroup.new(name, options) yield group end # Creates an unsaved object using the machine with the given name. # # Arguments: # name: (Symbol) # The name of the machine to apply. # attributes: (Hash) # A set of attributes to use as a replacement for the default ones provided by # the machine. # # Example # # Machine.build(:car, :model => 'Civic', :make => 'Honda') def self.build(name, attributes={}) machines = machines_for(name) raise MachineNotFoundError if machines.empty? object = machines.shift.build(attributes) while machine = machines.shift machine.apply_to(object, attributes) end object end # Creates an saved object using the machine with the given name. # # Arguments: # name: (Symbol) # The name of the machine to apply. # attributes: (Hash) # A set of attributes to use as a replacement for the default ones provided by # the machine. # # Example # # Machine.build!(:car, :model => 'Civic', :make => 'Honda') def self.build!(name, attributes={}) result = build(name, attributes) result.save! if result.respond_to?(:save!) result end # Apply the machine with the given name to the provided object. This can # be used to load an existing object with the attributes defined in a machine. # # Arguments: # name: (Symbol) # The name of the machine to apply. # object: (Object) # The object whose attributes should be set. # attributes: (Hash) # A set of replacements attributes for those specified in the machine. # # Example # # car = Car.new # Machine.apply_to(:car, car, :model => 'Jetta') def self.apply_to(name, object, attributes={}) machines = machines_for(name) return if machines.empty? while machine = machines.shift machine.apply_to(object, attributes) end object end # Defines a new named sequence. Sequences can be used to set attributes # that must be unique. Once a sequence is created it can be applied by # calling Machine.next, passing the sequence name. # # Arguments: # name: (Symbol) # A unique name used to identify this sequence. # block: (Proc) # The code to generate each value in the sequence. This block will be # called with a unique number each time a value in the sequence is to be # generated. The block should return the generated value for the # sequence. # # Example # # Machine.sequence :street do |n| # "#{n} Main St." # end def self.sequence(name, &block) self.sequences[name] = Sequence.new(block) end # Get the next value produced by the sequence with the given name. This # can be used in machine definitions to fill in attributes that must be # unique. # # Arguments: # sequence: (Symbol) # The name of the sequence to use. # # Example # # Machine.define :address do |address, machine| # address.street = machine.next(:street) # end def self.next(sequence) sequences[sequence].next if sequences.has_key?(sequence) end def initialize(name, options, proc) #:nodoc @name, @options, @proc = name, options, proc end def extends #:nodoc @options[:extends] ? machines[@options[:extends]] : nil end def build(attributes={}) #:nodoc object = build_class.new apply_to(object, attributes) object end def apply_to(object, attributes={}) #:nodoc @proc.call(object, AssociationHelper.new) attributes.each { |key, value| object.send("#{key}=", value) } end protected def build_class #:nodoc @options[:class] || @name.to_s.camelize.constantize end def self.machines_for(name) #:nodoc result = [machines[name]] while result.last result << result.last.extends end result.compact.reverse end end