# frozen_string_literal: true require 'mustermann/ast/node' require 'mustermann/error' require 'ruby2_keywords' require 'delegate' module Mustermann module AST # Implements translator pattern # # @abstract # @!visibility private class Translator # Encapsulates a single node translation # @!visibility private class NodeTranslator < DelegateClass(Node) # @param [Array] types list of types to register for. # @!visibility private def self.register(*types) types.each do |type| type = Node.constant_name(type) if type.is_a? Symbol translator.dispatch_table[type.to_s] = self end end # @param node [Mustermann::AST::Node, Object] # @param translator [Mustermann::AST::Translator] # # @!visibility private def initialize(node, translator) @translator = translator super(node) end # @!visibility private attr_reader :translator # shorthand for translating a nested object # @!visibility private ruby2_keywords def t(*args, &block) return translator unless args.any? translator.translate(*args, &block) end # @!visibility private alias_method :node, :__getobj__ end # maps types to translations # @!visibility private def self.dispatch_table @dispatch_table ||= {} end # some magic sauce so {NodeTranslator}s know whom to talk to for {#register} # @!visibility private def self.inherited(subclass) node_translator = Class.new(NodeTranslator) node_translator.define_singleton_method(:translator) { subclass } subclass.const_set(:NodeTranslator, node_translator) super end # DSL-ish method for specifying the exception class to use. # @!visibility private def self.raises(error) define_method(:error_class) { error } end # DSL method for defining single method translations. # @!visibility private def self.translate(*types, &block) Class.new(const_get(:NodeTranslator)) do register(*types) define_method(:translate, &block) end end # Enables quick creation of a translator object. # # @example # require 'mustermann' # require 'mustermann/ast/translator' # # translator = Mustermann::AST::Translator.create do # translate(:node) { [type, *t(payload)].flatten.compact } # translate(Array) { map { |e| t(e) } } # translate(Object) { } # end # # ast = Mustermann.new('/:name').to_ast # translator.translate(ast) # => [:root, :separator, :capture] # # @!visibility private def self.create(&block) Class.new(self, &block).new end raises Mustermann::Error # @param [Mustermann::AST::Node, Object] node to translate # @return decorator encapsulating translation # # @!visibility private def decorator_for(node) factory = node.class.ancestors.inject(nil) { |d,a| d || self.class.dispatch_table[a.name] } raise error_class, "#{self.class}: Cannot translate #{node.class}" unless factory factory.new(node, self) end # Start the translation dance for a (sub)tree. # @!visibility private ruby2_keywords def translate(node, *args, &block) result = decorator_for(node).translate(*args, &block) result = result.node while result.is_a? NodeTranslator result end # @return [String] escaped character # @!visibility private def escape(char, parser: URI::DEFAULT_PARSER, escape: parser.regexp[:UNSAFE], also_escape: nil) escape = Regexp.union(also_escape, escape) if also_escape char =~ escape ? parser.escape(char, Regexp.union(*escape)) : char end end end end