lib/temple/mixins/dispatcher.rb
# frozen_string_literal: true
module Temple
module Mixins
# @api private
module CoreDispatcher
def on_multi(*exps)
multi = [:multi]
exps.each {|exp| multi << compile(exp) }
multi
end
def on_capture(name, exp)
[:capture, name, compile(exp)]
end
end
# @api private
module EscapeDispatcher
def on_escape(flag, exp)
[:escape, flag, compile(exp)]
end
end
# @api private
module ControlFlowDispatcher
def on_if(condition, *cases)
[:if, condition, *cases.compact.map {|e| compile(e) }]
end
def on_case(arg, *cases)
[:case, arg, *cases.map {|condition, exp| [condition, compile(exp)] }]
end
def on_block(code, content)
[:block, code, compile(content)]
end
def on_cond(*cases)
[:cond, *cases.map {|condition, exp| [condition, compile(exp)] }]
end
end
# @api private
module CompiledDispatcher
def call(exp)
compile(exp)
end
def compile(exp)
dispatcher(exp)
end
private
def dispatcher(exp)
replace_dispatcher(exp)
end
def replace_dispatcher(exp)
tree = DispatchNode.new
dispatched_methods.each do |method|
method.split('_'.freeze)[1..-1].inject(tree) {|node, type| node[type.to_sym] }.method = method
end
self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def dispatcher(exp)
return replace_dispatcher(exp) if self.class != #{self.class}
#{tree.compile.gsub("\n", "\n ")}
end
RUBY
dispatcher(exp)
end
def dispatched_methods
re = /^on(_[a-zA-Z0-9]+)*$/
self.methods.map(&:to_s).select(&re.method(:=~))
end
# @api private
class DispatchNode < Hash
attr_accessor :method
def initialize
super { |hsh,key| hsh[key] = DispatchNode.new }
@method = nil
end
def compile(level = 0, call_parent = nil)
call_method = method ? (level == 0 ? "#{method}(*exp)" :
"#{method}(*exp[#{level}..-1])") : call_parent
if empty?
raise 'Invalid dispatcher node' unless method
call_method
else
code = String.new
code << "case(exp[#{level}])\n"
each do |key, child|
code << "when #{key.inspect}\n " <<
child.compile(level + 1, call_method).gsub("\n".freeze, "\n ".freeze) << "\n".freeze
end
code << "else\n " << (call_method || 'exp') << "\nend"
end
end
end
end
# @api public
#
# Implements a compatible call-method
# based on the including classe's methods.
#
# It uses every method starting with
# "on" and uses the rest of the method
# name as prefix of the expression it
# will receive. So, if a dispatcher
# has a method named "on_x", this method
# will be called with arg0,..,argN
# whenever an expression like [:x, arg0,..,argN ]
# is encountered.
#
# This works with longer prefixes, too.
# For example a method named "on_y_z"
# will be called whenever an expression
# like [:y, :z, .. ] is found. Furthermore,
# if additionally a method named "on_y"
# is present, it will be called when an
# expression starts with :y but then does
# not contain with :z. This way a
# dispatcher can implement namespaces.
#
# @note
# Processing does not reach into unknown
# expression types by default.
#
# @example
# class MyAwesomeDispatch
# include Temple::Mixins::Dispatcher
# def on_awesome(thing) # keep awesome things
# return [:awesome, thing]
# end
# def on_boring(thing) # make boring things awesome
# return [:awesome, thing+" with bacon"]
# end
# def on(type,*args) # unknown stuff is boring too
# return [:awesome, 'just bacon']
# end
# end
# filter = MyAwesomeDispatch.new
# # Boring things are converted:
# filter.call([:boring, 'egg']) #=> [:awesome, 'egg with bacon']
# # Unknown things too:
# filter.call([:foo]) #=> [:awesome, 'just bacon']
# # Known but not boring things won't be touched:
# filter.call([:awesome, 'chuck norris']) #=>[:awesome, 'chuck norris']
#
module Dispatcher
include CompiledDispatcher
include CoreDispatcher
include EscapeDispatcher
include ControlFlowDispatcher
end
end
end