c9a5ed22d5
Fix OR in Journey patterns
199 lines
4.6 KiB
Ruby
199 lines
4.6 KiB
Ruby
require 'action_dispatch/journey/router/strexp'
|
|
|
|
module ActionDispatch
|
|
module Journey # :nodoc:
|
|
module Path # :nodoc:
|
|
class Pattern # :nodoc:
|
|
attr_reader :spec, :requirements, :anchored
|
|
|
|
def self.from_string string
|
|
new Journey::Router::Strexp.build(string, {}, ["/.?"], true)
|
|
end
|
|
|
|
def initialize(strexp)
|
|
@spec = strexp.ast
|
|
@requirements = strexp.requirements
|
|
@separators = strexp.separators.join
|
|
@anchored = strexp.anchor
|
|
|
|
@names = nil
|
|
@optional_names = nil
|
|
@required_names = nil
|
|
@re = nil
|
|
@offsets = nil
|
|
end
|
|
|
|
def build_formatter
|
|
Visitors::FormatBuilder.new.accept(spec)
|
|
end
|
|
|
|
def ast
|
|
@spec.grep(Nodes::Symbol).each do |node|
|
|
re = @requirements[node.to_sym]
|
|
node.regexp = re if re
|
|
end
|
|
|
|
@spec.grep(Nodes::Star).each do |node|
|
|
node = node.left
|
|
node.regexp = @requirements[node.to_sym] || /(.+)/
|
|
end
|
|
|
|
@spec
|
|
end
|
|
|
|
def names
|
|
@names ||= spec.grep(Nodes::Symbol).map(&:name)
|
|
end
|
|
|
|
def required_names
|
|
@required_names ||= names - optional_names
|
|
end
|
|
|
|
def optional_names
|
|
@optional_names ||= spec.grep(Nodes::Group).flat_map { |group|
|
|
group.grep(Nodes::Symbol)
|
|
}.map(&:name).uniq
|
|
end
|
|
|
|
class RegexpOffsets < Journey::Visitors::Visitor # :nodoc:
|
|
attr_reader :offsets
|
|
|
|
def initialize(matchers)
|
|
@matchers = matchers
|
|
@capture_count = [0]
|
|
end
|
|
|
|
def visit(node)
|
|
super
|
|
@capture_count
|
|
end
|
|
|
|
def visit_SYMBOL(node)
|
|
node = node.to_sym
|
|
|
|
if @matchers.key?(node)
|
|
re = /#{@matchers[node]}|/
|
|
@capture_count.push((re.match('').length - 1) + (@capture_count.last || 0))
|
|
else
|
|
@capture_count << (@capture_count.last || 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
|
|
def initialize(separator, matchers)
|
|
@separator = separator
|
|
@matchers = matchers
|
|
@separator_re = "([^#{separator}]+)"
|
|
super()
|
|
end
|
|
|
|
def accept(node)
|
|
%r{\A#{visit node}\Z}
|
|
end
|
|
|
|
def visit_CAT(node)
|
|
[visit(node.left), visit(node.right)].join
|
|
end
|
|
|
|
def visit_SYMBOL(node)
|
|
node = node.to_sym
|
|
|
|
return @separator_re unless @matchers.key?(node)
|
|
|
|
re = @matchers[node]
|
|
"(#{re})"
|
|
end
|
|
|
|
def visit_GROUP(node)
|
|
"(?:#{visit node.left})?"
|
|
end
|
|
|
|
def visit_LITERAL(node)
|
|
Regexp.escape(node.left)
|
|
end
|
|
alias :visit_DOT :visit_LITERAL
|
|
|
|
def visit_SLASH(node)
|
|
node.left
|
|
end
|
|
|
|
def visit_STAR(node)
|
|
re = @matchers[node.left.to_sym] || '.+'
|
|
"(#{re})"
|
|
end
|
|
|
|
def visit_OR(node)
|
|
children = node.children.map { |n| visit n }
|
|
"(?:#{children.join(?|)})"
|
|
end
|
|
end
|
|
|
|
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
|
|
def accept(node)
|
|
%r{\A#{visit node}}
|
|
end
|
|
end
|
|
|
|
class MatchData # :nodoc:
|
|
attr_reader :names
|
|
|
|
def initialize(names, offsets, match)
|
|
@names = names
|
|
@offsets = offsets
|
|
@match = match
|
|
end
|
|
|
|
def captures
|
|
(length - 1).times.map { |i| self[i + 1] }
|
|
end
|
|
|
|
def [](x)
|
|
idx = @offsets[x - 1] + x
|
|
@match[idx]
|
|
end
|
|
|
|
def length
|
|
@offsets.length
|
|
end
|
|
|
|
def post_match
|
|
@match.post_match
|
|
end
|
|
|
|
def to_s
|
|
@match.to_s
|
|
end
|
|
end
|
|
|
|
def match(other)
|
|
return unless match = to_regexp.match(other)
|
|
MatchData.new(names, offsets, match)
|
|
end
|
|
alias :=~ :match
|
|
|
|
def source
|
|
to_regexp.source
|
|
end
|
|
|
|
def to_regexp
|
|
@re ||= regexp_visitor.new(@separators, @requirements).accept spec
|
|
end
|
|
|
|
private
|
|
|
|
def regexp_visitor
|
|
@anchored ? AnchoredRegexp : UnanchoredRegexp
|
|
end
|
|
|
|
def offsets
|
|
return @offsets if @offsets
|
|
|
|
viz = RegexpOffsets.new(@requirements)
|
|
@offsets = viz.accept(spec)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|