dominicsayers/machinist

View on GitHub
lib/machinist/blueprint.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Machinist
  # A Blueprint defines a method of constructing objects of a particular class.
  class Blueprint
    # Construct a blueprint for the given +klass+.
    #
    # Pass in the +:parent+ option to define a parent blueprint to apply after
    # this one.  You can supply another blueprint, or a class in which to look
    # for a blueprint.  In the latter case, make will walk up the superclass
    # chain looking for blueprints to apply.
    def initialize(klass, options = {}, &block)
      @klass  = klass
      @parent = options[:parent]
      @block  = block
    end

    attr_reader :klass, :parent, :block

    # Generate an object from this blueprint.
    #
    # Pass in attributes to override values defined in the blueprint.
    def make(attributes = {})
      lathe = lathe_class.new(@klass, new_serial_number, attributes)

      lathe.instance_eval(&@block)
      each_ancestor { |blueprint| lathe.instance_eval(&blueprint.block) }

      lathe.object
    end

    # Returns the Lathe class used to make objects for this blueprint.
    #
    # Subclasses can override this to substitute a custom lathe class.
    def lathe_class
      Lathe
    end

    # Returns the parent blueprint for this blueprint.
    def parent_blueprint
      case @parent
      when nil
        nil
      when Blueprint
        # @parent references the parent blueprint directly.
        @parent
      else
        # @parent is a class in which we should look for a blueprint.
        find_blueprint_in_superclass_chain(@parent)
      end
    end

    # Yields the parent blueprint, its parent blueprint, etc.
    def each_ancestor
      ancestor = parent_blueprint
      while ancestor
        yield ancestor
        ancestor = ancestor.parent_blueprint
      end
    end

    protected

    def new_serial_number #:nodoc:
      parent_blueprint = self.parent_blueprint # Cache this for speed.
      if parent_blueprint
        parent_blueprint.new_serial_number
      else
        @serial_number ||= 0
        @serial_number += 1
        format('%04d', @serial_number)
      end
    end

    private

    def find_blueprint_in_superclass_chain(klass)
      klass = klass.superclass until has_blueprint?(klass) || klass.nil?
      klass && klass.blueprint
    end

    def has_blueprint?(klass)
      klass.respond_to?(:blueprint) && !klass.blueprint.nil?
    end
  end
end