lib/machinist/blueprint.rb
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