module Doozer class << self def object_list #:nodoc: @object_list ||= {} end def object_stack #:nodoc: @object_stack ||= [] end # Before you can specify any fixture data, you have to tell Doozer what # fixture you want to generate. # # Doozer.define :page # # This tell Doozer that you are going to be referencing/generate page fixtures. # It will then generate three macros for you: +page()+, +pages()+, +page_id()+. def define(name, options={}, &block) # TODO: What happens if an object is defined twice? Should it merge the two definitions? unless object_list.keys.include?(name) # initialize new object structure object_list[name] = {} object_list[name][:row_names] = [] object_list[name][:data_rows] = {} object_list[name][:default_values] = {} object_list[name][:target] = name object_list[name][:block_affects] = [{:child=>:belongs_to}] object_list[name][:alias] = options[:as] if options.has_key? :as # Create macros for this object type macro_name = object_list[name][:alias] || name Kernel.class_eval(<<-EORB) def #{macro_name}(*params, &block) Doozer.add_object(:#{name}, *params, &block) end def #{macro_name}_id(key) Doozer.get_object_id_from_name(:#{name}, key) end def #{macro_name}_name(id) Doozer.get_object_name_from_id(:#{name}, id) end def #{macro_name.to_s.pluralize}(key) Doozer.get_object_by_name(:#{name}, key) end EORB @current_object = name class_eval( &block ) if block_given? end end # USED IN THE DEFINITION BLOCK # Sets the default values in a fixture # # Doozer.define :page do # defaults :created_on=>Time.now.to_s(:db) # end # def defaults(values={}) object_list[@current_object][:default_values] = values end # Sets the order of the fields that will be provided, positionally, in the generated macro # # Doozer.define :page do # field_order :title, :author, :body # end # # When the macro is called: # # page 'Hello', 'Matthew', "This is some content" # ^ Title ^ Author ^ Body def field_order(*field_ary) object_list[@current_object][:field_order] = field_ary.flatten.map{|f| f.to_sym} end def target(ar_classname) #:nodoc: object_list[@current_object][:target] = ar_classname end # Specifies the relationship of fixtures referenced in a block. # Defaults to +:child=>:belongs_to+ def block_affects(*options) object_list[@current_object][:block_affects] = options end # A hook that's called right before the data is generated into YAML fixture. You # should attach a block that accepts an attribute hash as the parameter: # # before_generate do |atts| # atts[:target_field] = "From source: #{atts[:source_field]}" # end # def before_generate(&block) if block_given? object_list[@current_object][:before_generate] = block end end # GENERATED MACROS CALL THESE TO GENERATE/ACCESS FIXTURE DATA def add_object(obj_type, *params, &block) #:nodoc: # Get/Generate a row name row_name = if params.first.is_a? Symbol params.shift else gen_name = "#{obj_type}_#{next_object_id(obj_type)}".to_sym end # Get the named attributes atts = params.last.is_a?(Hash) ? params.pop : {} # Populate attributes from array, using field_order as key reference fields = object_list[obj_type][:field_order] || [] params.each_with_index do |value, i| atts[ fields[i] ] = value end # For the first time we define a data row... unless object_list[obj_type][:data_rows].has_key?(row_name) # Set the id atts[:id] = next_object_id(obj_type) # Add row_name to name list object_list[obj_type][:row_names] << row_name # Create a default data row for row_name object_list[obj_type][:data_rows][row_name] = object_list[obj_type][:default_values].clone end # Merge the new data attributes = object_list[obj_type][:data_rows][row_name].merge(atts) # Look in the stack to see if we have a parent object, if so, do something! if parent = object_stack[0] affected = parent[:rules].last.keys.first behavior = parent[:rules].last.values.first case behavior when :belongs_to case affected when :parent; parent[:data]["#{obj_type}_id".to_sym] = attributes[:id] when :child; attributes["#{parent[:type]}_id".to_sym] = parent[:data][:id] else; puts "#{affected} isn't a valid relationship type" end when :parent_id case affected when :parent; parent[:data][:parent_id] = attributes[:id] when :child; attributes[:parent_id] = parent[:data][:id] else; puts "#{affected} isn't a valid relationship type" end else puts "#{behavior} isn't a valid behavior type" end end if block_given? # Add these attributes to the 'object' stack object_stack.unshift( {:data=>attributes, :type=>obj_type, :rules=>object_list[obj_type][:block_affects]} ) # Call the block block.call # Pop off the stack object_stack.shift end # Add the attributes to our data cache object_list[obj_type][:data_rows][row_name] = attributes end def get_object_by_name(obj_type, name) #:nodoc: # TODO: fetch the name symbol if the name param is a Fixnum name = get_object_name_from_id(obj_type, name) if name.is_a?( Fixnum ) object_list[obj_type][:data_rows][name] end def get_object_id_from_name(obj_type, name) #:nodoc: (object_list[obj_type][:row_names].index(name)) +1 end def get_object_name_from_id(obj_type, id) #:nodoc: object_list[obj_type][:row_names][(id -1)] end def next_object_id(obj_type) #:nodoc: (object_list[obj_type][:row_names].length) +1 end # Force a reset of the Doozer data structure... Note: This will remove all # object definitions and data! def reset! object_list.each do |obj_name, obj_def| # TODO: Handle aliased macros! macro_name = obj_def[:alias] || obj_name Kernel.class_eval(<<-EORB) undef #{macro_name} if defined? #{macro_name} undef #{macro_name}_id if defined? #{macro_name}_id undef #{macro_name}_name if defined? #{macro_name}_name undef #{macro_name.to_s.pluralize} if defined? #{macro_name.to_s.pluralize} EORB end @object_list = {} end end end