require "hokusai/version"
require "active_support"
require "yaml"
module Hokusai
##
# Templatable models support taking a snapshot of their data, for later use stamping out clones.
#
# Include this module to obtain the +as_template+ and +from_template+ methods that support a simple container
# such as Hokusai::Container.
#
# Configure it with the +template+ declaration, which accepts a list of columns and an +includes+ option
# for nested assocations.
#
# === Example
#
# class Device < ActiveRecord::Base
# include Hokusai::Templatable
#
# has_many :interfaces
#
# template :name, :model, :location, :year, include: [:interfaces]
# end
module Templatable
extend ActiveSupport::Concern
included do
class_attribute :🌊
end
module ClassMethods
# Define the template specification for the model.
def template(*template_columns, **options)
template_columns = Array(template_columns).map(&:to_s)
included_associations = Array(options[:include]).map(&:to_s)
self.🌊 = {
columns: template_columns,
associations: included_associations,
}
end
##
# Build a new, unsaved instance of the model (and any included associations) from the template supplied.
# The block will be called with the new instance.
def from_template(template, &block)
if template.is_a?(Array)
template.map { |tpl| from_template(tpl, &block) }
else
new_attrs = template.slice(*🌊[:columns])
template.slice(*🌊[:associations]).each do |association, association_template|
new_attrs[association] = reflect_on_association(association).klass.from_template(association_template)
end
new(new_attrs, &block)
end
end
end
##
# Serialize this object (and any included associations) according to the template specification.
def as_template
result_hash = {}
🌊[:columns].each_with_object(result_hash) do |column|
result_hash[column] = read_attribute_for_template(column)
end
🌊[:associations].each do |association|
records = send(association)
result_hash[association] = if records.respond_to?(:to_ary)
records.to_ary.map { |r| r.as_template }
else
records.as_template
end
end
result_hash
end
private
alias :read_attribute_for_template :send
end
##
# This module supplies a simple container for snapshots of template-style data.
# These templates are used when stamping out new objects.
#
# A Hokusai container communicates with models via the +as_template+ method and +from_template+ class method.
# The data will be serialized as YAML; this container class is otherwise not concerned with its structure.
#
# Relies on the presence of two columns: +hokusai_class+ (string) and +hokusai_template+ (text).
module Container
extend ActiveSupport::Concern
included do
validates :hokusai_class, :hokusai_template, presence: true
end
# Set current template data, calling +as_template+ on the origin.
#
# Intended for use via @template = Template.new(origin: project, ...attrs...)
#
def origin=(object)
self.hokusai_class = object.class.to_s
self.hokusai_template = YAML.dump(object.as_template)
end
# Stamp out a new object from the template. Calls +from_template+ on the applicable class with
# the deserialized template data, passing on any supplied block.
#
# The semantics of +from_template+ are left to the receiving model. If using the supplied
# concern Hokusai::Templatable then a new, unsaved model object will be instantiated,
# with nested models included as specified.
#
def stamp(&block)
hokusai_class.constantize.from_template(YAML.load(hokusai_template), &block)
end
end
end