module Machinist

  # Extend classes with this module to define the blueprint and make methods.
  module Machinable
    # Define a blueprint with the given name for this class.
    #
    # e.g.
    #   Post.blueprint do
    #     title { "A Post" }
    #     body  { "Lorem ipsum..." }
    #   end
    #
    # If you provide the +name+ argument, a named blueprint will be created.
    # See the +blueprint_name+ argument to the make method.
    def blueprint(name = :master, &block)
      @blueprints ||= {}
      if block_given?
        parent = (name == :master ? superclass : self) # Where should we look for the parent blueprint?
        @blueprints[name] = blueprint_class.new(self, :parent => parent, &block)
      end
      @blueprints[name]
    end

    # Construct an object from a blueprint.
    #
    # :call-seq:
    #   make([count], [blueprint_name], [attributes = {}])
    #
    # [+count+]
    #   The number of objects to construct. If +count+ is provided, make
    #   returns an array of objects rather than a single object.
    # [+blueprint_name+]
    #   Construct the object from the named blueprint, rather than the master
    #   blueprint.
    # [+attributes+]
    #   Override the attributes from the blueprint with values from this hash.
    def make(*args)
      decode_args_to_make(*args) do |blueprint, attributes|
        blueprint.make(attributes)
      end
    end

    # Construct and save an object from a blueprint, if the class allows saving.
    #
    # :call-seq:
    #   make!([count], [blueprint_name], [attributes = {}])
    #
    # Arguments are the same as for make.
    def make!(*args)
      decode_args_to_make(*args) do |blueprint, attributes|
        raise BlueprintCantSaveError.new(blueprint) unless blueprint.respond_to?(:make!)
        blueprint.make!(attributes)
      end
    end

    # Remove all blueprints defined on this class.
    def clear_blueprints!
      @blueprints = {}
    end

    # Classes that include Machinable can override this method if they want to
    # use a custom blueprint class when constructing blueprints.
    #
    # The default is Machinist::Blueprint.
    def blueprint_class
      Machinist::Blueprint
    end

  private

    # Parses the arguments to make.
    #
    # Yields a blueprint and an attributes hash to the block, which should
    # construct an object from them. The block may be called multiple times to
    # construct multiple objects.
    def decode_args_to_make(*args) #:nodoc:
      shift_arg = lambda {|klass| args.shift if args.first.is_a?(klass) }
      count      = shift_arg[Fixnum]
      name       = shift_arg[Symbol] || :master
      attributes = shift_arg[Hash]   || {}
      raise ArgumentError.new("Couldn't understand arguments") unless args.empty?

      @blueprints ||= {}
      blueprint = @blueprints[name]
      raise NoBlueprintError.new(self, name) unless blueprint

      if count.nil?
        yield(blueprint, attributes)
      else
        Array.new(count) { yield(blueprint, attributes) }
      end
    end

  end
end
