mailcatcher/vendor/gems/mustermann/lib/mustermann/ast/expander.rb

144 lines
4.2 KiB
Ruby

require 'mustermann/ast/translator'
require 'mustermann/ast/compiler'
module Mustermann
module AST
# Looks at an AST, remembers the important bits of information to do an
# ultra fast expansion.
#
# @!visibility private
class Expander < Translator
raises ExpandError
translate Array do |*args|
inject(t.pattern) do |pattern, element|
t.add_to(pattern, t(element, *args))
end
end
translate :capture do |**options|
t.for_capture(node, **options)
end
translate :named_splat, :splat do
t.pattern + t.for_capture(node)
end
translate :expression do
t(payload, allow_reserved: operator.allow_reserved)
end
translate :root, :group do
t(payload)
end
translate :char do
t.pattern(t.escape(payload, also_escape: /[\/\?#\&\=%]/).gsub(?%, "%%"))
end
translate :separator do
t.pattern(payload.gsub(?%, "%%"))
end
translate :with_look_ahead do
t.add_to(t(head), t(payload))
end
translate :optional do
nested = t(payload)
nested += t.pattern unless nested.any? { |n| n.first.empty? }
nested
end
translate :union do
payload.map { |e| t(e) }.inject(:+)
end
# helper method for captures
# @!visibility private
def for_capture(node, **options)
name = node.name.to_sym
pattern('%s', name, name => /(?!#{pattern_for(node, **options)})./)
end
# maps sorted key list to sprintf patterns and filters
# @!visibility private
def mappings
@mappings ||= {}
end
# all the known keys
# @!visibility private
def keys
@keys ||= []
end
# add a tree for expansion
# @!visibility private
def add(ast)
translate(ast).each do |keys, pattern, filter|
self.keys.concat(keys).uniq!
mappings[keys.sort] ||= [keys, pattern, filter]
end
end
# helper method for getting a capture's pattern.
# @!visibility private
def pattern_for(node, **options)
Compiler.new.decorator_for(node).pattern(**options)
end
# @see Mustermann::Pattern#expand
# @!visibility private
def expand(values)
values = values.each_with_object({}){ |(key, value), new_hash|
new_hash[value.instance_of?(Array) ? [key] * value.length : key] = value }
keys, pattern, filters = mappings.fetch(values.keys.flatten.sort) { error_for(values) }
filters.each { |key, filter| values[key] &&= escape(values[key], also_escape: filter) }
pattern % (values[keys] || values.values_at(*keys))
end
# @see Mustermann::Pattern#expandable?
# @!visibility private
def expandable?(values)
values = values.keys if values.respond_to? :keys
values = values.sort if values.respond_to? :sort
mappings.include? values
end
# @see Mustermann::Expander#with_rest
# @!visibility private
def expandable_keys(keys)
mappings.keys.select { |k| (k - keys).empty? }.max_by(&:size) || keys
end
# helper method for raising an error for unexpandable values
# @!visibility private
def error_for(values)
expansions = mappings.keys.map(&:inspect).join(" or ")
raise error_class, "cannot expand with keys %p, possible expansions: %s" % [values.keys.sort, expansions]
end
# @see Mustermann::AST::Translator#expand
# @!visibility private
def escape(string, *args)
# URI::Parser is pretty slow, let's not send every string to it, even if it's unnecessary
string =~ /\A\w*\Z/ ? string : super
end
# Turns a sprintf pattern into our secret internal data structure.
# @!visibility private
def pattern(string = "", *keys, **filters)
[[keys, string, filters]]
end
# Creates the product of two of our secret internal data structures.
# @!visibility private
def add_to(list, result)
list << [[], ""] if list.empty?
list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, **f1, **f2] } }
end
end
end
end