dominicsayers/machinist

View on GitHub
lib/machinist/machinable.rb

Summary

Maintainability
A
25 mins
Test Coverage
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, 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:
      count, name, attributes = *decode_args(args)
      blueprint = ensure_blueprint(name)

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

    def decode_args(args)
      shift_arg  = ->(klass) { args.shift if args.first.is_a?(klass) }
      count      = shift_arg[0.class]
      name       = shift_arg[Symbol] || :master
      attributes = shift_arg[Hash]   || {}
      raise ArgumentError, "Couldn't understand arguments" unless args.empty?
      [count, name, attributes]
    end

    def ensure_blueprint(name)
      @blueprints ||= {}
      @blueprints[name] || raise(NoBlueprintError.new(self, name))
    end
  end
end