aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/journey
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/journey')
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb47
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/dot.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/nodes/node.rb16
-rw-r--r--actionpack/lib/action_dispatch/journey/parser_extras.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb71
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb77
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb5
-rw-r--r--actionpack/lib/action_dispatch/journey/router/strexp.rb27
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb129
10 files changed, 241 insertions, 149 deletions
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index c0566c6fc9..0323360faa 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -14,7 +14,7 @@ module ActionDispatch
def generate(name, options, path_parameters, parameterize = nil)
constraints = path_parameters.merge(options)
- missing_keys = []
+ missing_keys = nil # need for variable scope
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
@@ -25,22 +25,22 @@ module ActionDispatch
next unless name || route.dispatcher?
missing_keys = missing_keys(route, parameterized_parts)
- next unless missing_keys.empty?
+ next if missing_keys && !missing_keys.empty?
params = options.dup.delete_if do |key, _|
parameterized_parts.key?(key) || route.defaults.key?(key)
end
defaults = route.defaults
required_parts = route.required_parts
- parameterized_parts.delete_if do |key, value|
- value.to_s == defaults[key].to_s && !required_parts.include?(key)
+ parameterized_parts.keep_if do |key, value|
+ (defaults[key].nil? && value.present?) || value.to_s != defaults[key].to_s || required_parts.include?(key)
end
return [route.format(parameterized_parts), params]
end
message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty?
+ message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
raise ActionController::UrlGenerationError, message
end
@@ -54,12 +54,12 @@ module ActionDispatch
def extract_parameterized_parts(route, options, recall, parameterize = nil)
parameterized_parts = recall.merge(options)
- keys_to_keep = route.parts.reverse.drop_while { |part|
+ keys_to_keep = route.parts.reverse_each.drop_while { |part|
!options.key?(part) || (options[part] || recall[part]).nil?
} | route.required_parts
- (parameterized_parts.keys - keys_to_keep).each do |bad_key|
- parameterized_parts.delete(bad_key)
+ parameterized_parts.delete_if do |bad_key, _|
+ !keys_to_keep.include?(bad_key)
end
if parameterize
@@ -110,15 +110,36 @@ module ActionDispatch
routes
end
+ module RegexCaseComparator
+ DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/
+ DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/
+
+ def self.===(regex)
+ DEFAULT_INPUT == regex
+ end
+ end
+
# Returns an array populated with missing keys if any are present.
def missing_keys(route, parts)
- missing_keys = []
+ missing_keys = nil
tests = route.path.requirements
route.required_parts.each { |key|
- if tests.key?(key)
- missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key]
+ case tests[key]
+ when nil
+ unless parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
+ when RegexCaseComparator
+ unless RegexCaseComparator::DEFAULT_REGEX === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
else
- missing_keys << key unless parts[key]
+ unless /\A#{tests[key]}\Z/ === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
end
}
missing_keys
@@ -134,7 +155,7 @@ module ActionDispatch
def build_cache
root = { ___routes: [] }
- routes.each_with_index do |route, i|
+ routes.routes.each_with_index do |route, i|
leaf = route.required_defaults.inject(root) do |h, tuple|
h[tuple] ||= {}
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
index 47bf76bdbf..7063b44bb5 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
module ActionDispatch
module Journey # :nodoc:
module NFA # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
index cf6542b370..2793c5668d 100644
--- a/actionpack/lib/action_dispatch/journey/nodes/node.rb
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -14,15 +14,15 @@ module ActionDispatch
end
def each(&block)
- Visitors::Each.new(block).accept(self)
+ Visitors::Each::INSTANCE.accept(self, block)
end
def to_s
- Visitors::String.new.accept(self)
+ Visitors::String::INSTANCE.accept(self, '')
end
def to_dot
- Visitors::Dot.new.accept(self)
+ Visitors::Dot::INSTANCE.accept(self)
end
def to_sym
@@ -39,10 +39,15 @@ module ActionDispatch
def symbol?; false; end
def literal?; false; end
+ def terminal?; false; end
+ def star?; false; end
+ def cat?; false; end
+ def group?; false; end
end
class Terminal < Node # :nodoc:
alias :symbol :left
+ def terminal?; true; end
end
class Literal < Terminal # :nodoc:
@@ -69,11 +74,13 @@ module ActionDispatch
class Symbol < Terminal # :nodoc:
attr_accessor :regexp
alias :symbol :regexp
+ attr_reader :name
DEFAULT_EXP = /[^\.\/\?]+/
def initialize(left)
super
@regexp = DEFAULT_EXP
+ @name = left.tr '*:'.freeze, ''.freeze
end
def default_regexp?
@@ -89,9 +96,11 @@ module ActionDispatch
class Group < Unary # :nodoc:
def type; :GROUP; end
+ def group?; true; end
end
class Star < Unary # :nodoc:
+ def star?; true; end
def type; :STAR; end
def name
@@ -111,6 +120,7 @@ module ActionDispatch
end
class Cat < Binary # :nodoc:
+ def cat?; true; end
def type; :CAT; end
end
diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb
index 14892f4321..fff0299812 100644
--- a/actionpack/lib/action_dispatch/journey/parser_extras.rb
+++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb
@@ -6,6 +6,10 @@ module ActionDispatch
class Parser < Racc::Parser # :nodoc:
include Journey::Nodes
+ def self.parse(string)
+ new.parse string
+ end
+
def initialize
@scanner = Scanner.new
end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 64b48ca45f..5ee8810066 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -1,5 +1,3 @@
-require 'action_dispatch/journey/router/strexp'
-
module ActionDispatch
module Journey # :nodoc:
module Path # :nodoc:
@@ -7,14 +5,20 @@ module ActionDispatch
attr_reader :spec, :requirements, :anchored
def self.from_string string
- new Journey::Router::Strexp.build(string, {}, ["/.?"], true)
+ build(string, {}, "/.?", true)
+ end
+
+ def self.build(path, requirements, separators, anchored)
+ parser = Journey::Parser.new
+ ast = parser.parse path
+ new ast, requirements, separators, anchored
end
- def initialize(strexp)
- @spec = strexp.ast
- @requirements = strexp.requirements
- @separators = strexp.separators.join
- @anchored = strexp.anchor
+ def initialize(ast, requirements, separators, anchored)
+ @spec = ast
+ @requirements = requirements
+ @separators = separators
+ @anchored = anchored
@names = nil
@optional_names = nil
@@ -28,12 +32,12 @@ module ActionDispatch
end
def ast
- @spec.grep(Nodes::Symbol).each do |node|
+ @spec.find_all(&:symbol?).each do |node|
re = @requirements[node.to_sym]
node.regexp = re if re
end
- @spec.grep(Nodes::Star).each do |node|
+ @spec.find_all(&:star?).each do |node|
node = node.left
node.regexp = @requirements[node.to_sym] || /(.+)/
end
@@ -42,7 +46,7 @@ module ActionDispatch
end
def names
- @names ||= spec.grep(Nodes::Symbol).map(&:name)
+ @names ||= spec.find_all(&:symbol?).map(&:name)
end
def required_names
@@ -50,36 +54,11 @@ module ActionDispatch
end
def optional_names
- @optional_names ||= spec.grep(Nodes::Group).flat_map { |group|
- group.grep(Nodes::Symbol)
+ @optional_names ||= spec.find_all(&:group?).flat_map { |group|
+ group.find_all(&: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
@@ -189,8 +168,20 @@ module ActionDispatch
def offsets
return @offsets if @offsets
- viz = RegexpOffsets.new(@requirements)
- @offsets = viz.accept(spec)
+ @offsets = [0]
+
+ spec.find_all(&:symbol?).each do |node|
+ node = node.to_sym
+
+ if @requirements.key?(node)
+ re = /#{@requirements[node]}|/
+ @offsets.push((re.match('').length - 1) + @offsets.last)
+ else
+ @offsets << @offsets.last
+ end
+ end
+
+ @offsets
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index cbc985640a..35c2b1b86e 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -1,36 +1,81 @@
module ActionDispatch
module Journey # :nodoc:
class Route # :nodoc:
- attr_reader :app, :path, :defaults, :name
+ attr_reader :app, :path, :defaults, :name, :precedence
attr_reader :constraints
alias :conditions :constraints
- attr_accessor :precedence
+ module VerbMatchers
+ VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
+ VERBS.each do |v|
+ class_eval <<-eoc
+ class #{v}
+ def self.verb; name.split("::").last; end
+ def self.call(req); req.#{v.downcase}?; end
+ end
+ eoc
+ end
+
+ class Unknown
+ attr_reader :verb
+
+ def initialize(verb)
+ @verb = verb
+ end
+
+ def call(request); @verb === request.request_method; end
+ end
+
+ class All
+ def self.call(_); true; end
+ def self.verb; ''; end
+ end
+
+ VERB_TO_CLASS = VERBS.each_with_object({ :all => All }) do |verb, hash|
+ klass = const_get verb
+ hash[verb] = klass
+ hash[verb.downcase] = klass
+ hash[verb.downcase.to_sym] = klass
+ end
+
+ end
+
+ def self.verb_matcher(verb)
+ VerbMatchers::VERB_TO_CLASS.fetch(verb) do
+ VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
+ end
+ end
+
+ def self.build(name, app, path, constraints, required_defaults, defaults)
+ request_method_match = verb_matcher(constraints.delete(:request_method))
+ new name, app, path, constraints, required_defaults, defaults, request_method_match, 0
+ end
##
# +path+ is a path constraint.
# +constraints+ is a hash of constraints to be applied to this route.
- def initialize(name, app, path, constraints, required_defaults, defaults)
+ def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence)
@name = name
@app = app
@path = path
+ @request_method_match = request_method_match
@constraints = constraints
@defaults = defaults
@required_defaults = nil
- @_required_defaults = required_defaults || []
+ @_required_defaults = required_defaults
@required_parts = nil
@parts = nil
@decorated_ast = nil
- @precedence = 0
+ @precedence = precedence
@path_formatter = @path.build_formatter
end
def ast
@decorated_ast ||= begin
decorated_ast = path.ast
- decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self }
+ decorated_ast.find_all(&:terminal?).each { |n| n.memo = self }
decorated_ast
end
end
@@ -92,7 +137,8 @@ module ActionDispatch
end
def matches?(request)
- constraints.all? do |method, value|
+ match_verb(request) &&
+ constraints.all? { |method, value|
case value
when Regexp, String
value === request.send(method).to_s
@@ -105,15 +151,28 @@ module ActionDispatch
else
value === request.send(method)
end
- end
+ }
end
def ip
constraints[:ip] || //
end
+ def requires_matching_verb?
+ !@request_method_match.all? { |x| x == VerbMatchers::All }
+ end
+
def verb
- constraints[:request_method] || //
+ verbs.join('|')
+ end
+
+ private
+ def verbs
+ @request_method_match.map(&:verb)
+ end
+
+ def match_verb(request)
+ @request_method_match.any? { |m| m.call request }
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index b84aad8eb6..f649588520 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -1,5 +1,4 @@
require 'action_dispatch/journey/router/utils'
-require 'action_dispatch/journey/router/strexp'
require 'action_dispatch/journey/routes'
require 'action_dispatch/journey/formatter'
@@ -102,7 +101,7 @@ module ActionDispatch
}
routes =
- if req.request_method == "HEAD"
+ if req.head?
match_head_routes(routes, req)
else
match_routes(routes, req)
@@ -121,7 +120,7 @@ module ActionDispatch
end
def match_head_routes(routes, req)
- verb_specific_routes = routes.reject { |route| route.verb == // }
+ verb_specific_routes = routes.select(&:requires_matching_verb?)
head_routes = match_routes(verb_specific_routes, req)
if head_routes.empty?
diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb
deleted file mode 100644
index 4b7738f335..0000000000
--- a/actionpack/lib/action_dispatch/journey/router/strexp.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module ActionDispatch
- module Journey # :nodoc:
- class Router # :nodoc:
- class Strexp # :nodoc:
- class << self
- alias :compile :new
- end
-
- attr_reader :path, :requirements, :separators, :anchor, :ast
-
- def self.build(path, requirements, separators, anchor = true)
- parser = Journey::Parser.new
- ast = parser.parse path
- new ast, path, requirements, separators, anchor
- end
-
- def initialize(ast, path, requirements, separators, anchor = true)
- @ast = ast
- @path = path
- @requirements = requirements
- @separators = separators
- @anchor = anchor
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
index 5990964b57..f7b009109e 100644
--- a/actionpack/lib/action_dispatch/journey/routes.rb
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -5,11 +5,10 @@ module ActionDispatch
class Routes # :nodoc:
include Enumerable
- attr_reader :routes, :named_routes, :custom_routes, :anchored_routes
+ attr_reader :routes, :custom_routes, :anchored_routes
def initialize
@routes = []
- @named_routes = {}
@ast = nil
@anchored_routes = []
@custom_routes = []
@@ -37,7 +36,6 @@ module ActionDispatch
routes.clear
anchored_routes.clear
custom_routes.clear
- named_routes.clear
end
def partition_route(route)
@@ -62,13 +60,9 @@ module ActionDispatch
end
end
- # Add a route to the routing table.
- def add_route(app, path, conditions, required_defaults, defaults, name = nil)
- route = Route.new(name, app, path, conditions, required_defaults, defaults)
-
- route.precedence = routes.length
+ def add_route(name, mapping)
+ route = mapping.make_route name, routes.length
routes << route
- named_routes[name] = route if name && !named_routes[name]
partition_route(route)
clear_cache!
route
diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
index 52b4c8b489..306d2e674a 100644
--- a/actionpack/lib/action_dispatch/journey/visitors.rb
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
module ActionDispatch
module Journey # :nodoc:
class Format
@@ -92,6 +90,45 @@ module ActionDispatch
end
end
+ class FunctionalVisitor # :nodoc:
+ DISPATCH_CACHE = {}
+
+ def accept(node, seed)
+ visit(node, seed)
+ end
+
+ def visit node, seed
+ send(DISPATCH_CACHE[node.type], node, seed)
+ end
+
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
+ end
+ def visit_CAT(n, seed); binary(n, seed); end
+
+ def nary(node, seed)
+ node.children.inject(seed) { |s, c| visit(c, s) }
+ end
+ def visit_OR(n, seed); nary(n, seed); end
+
+ def unary(node, seed)
+ visit(node.left, seed)
+ end
+ def visit_GROUP(n, seed); unary(n, seed); end
+ def visit_STAR(n, seed); unary(n, seed); end
+
+ def terminal(node, seed); seed; end
+ def visit_LITERAL(n, seed); terminal(n, seed); end
+ def visit_SYMBOL(n, seed); terminal(n, seed); end
+ def visit_SLASH(n, seed); terminal(n, seed); end
+ def visit_DOT(n, seed); terminal(n, seed); end
+
+ instance_methods(false).each do |pim|
+ next unless pim =~ /^visit_(.*)$/
+ DISPATCH_CACHE[$1.to_sym] = pim
+ end
+ end
+
class FormatBuilder < Visitor # :nodoc:
def accept(node); Journey::Format.new(super); end
def terminal(node); [node.left]; end
@@ -117,104 +154,110 @@ module ActionDispatch
end
# Loop through the requirements AST
- class Each < Visitor # :nodoc:
- attr_reader :block
-
- def initialize(block)
- @block = block
- end
-
- def visit(node)
+ class Each < FunctionalVisitor # :nodoc:
+ def visit(node, block)
block.call(node)
super
end
+
+ INSTANCE = new
end
- class String < Visitor # :nodoc:
+ class String < FunctionalVisitor # :nodoc:
private
- def binary(node)
- [visit(node.left), visit(node.right)].join
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
end
- def nary(node)
- node.children.map { |c| visit(c) }.join '|'
+ def nary(node, seed)
+ last_child = node.children.last
+ node.children.inject(seed) { |s, c|
+ string = visit(c, s)
+ string << "|".freeze unless last_child == c
+ string
+ }
end
- def terminal(node)
- node.left
+ def terminal(node, seed)
+ seed + node.left
end
- def visit_GROUP(node)
- "(#{visit(node.left)})"
+ def visit_GROUP(node, seed)
+ visit(node.left, seed << "(".freeze) << ")".freeze
end
+
+ INSTANCE = new
end
- class Dot < Visitor # :nodoc:
+ class Dot < FunctionalVisitor # :nodoc:
def initialize
@nodes = []
@edges = []
end
- def accept(node)
+ def accept(node, seed = [[], []])
super
+ nodes, edges = seed
<<-eodot
digraph parse_tree {
size="8,5"
node [shape = none];
edge [dir = none];
- #{@nodes.join "\n"}
- #{@edges.join("\n")}
+ #{nodes.join "\n"}
+ #{edges.join("\n")}
}
eodot
end
private
- def binary(node)
- node.children.each do |c|
- @edges << "#{node.object_id} -> #{c.object_id};"
- end
+ def binary(node, seed)
+ seed.last.concat node.children.map { |c|
+ "#{node.object_id} -> #{c.object_id};"
+ }
super
end
- def nary(node)
- node.children.each do |c|
- @edges << "#{node.object_id} -> #{c.object_id};"
- end
+ def nary(node, seed)
+ seed.last.concat node.children.map { |c|
+ "#{node.object_id} -> #{c.object_id};"
+ }
super
end
- def unary(node)
- @edges << "#{node.object_id} -> #{node.left.object_id};"
+ def unary(node, seed)
+ seed.last << "#{node.object_id} -> #{node.left.object_id};"
super
end
- def visit_GROUP(node)
- @nodes << "#{node.object_id} [label=\"()\"];"
+ def visit_GROUP(node, seed)
+ seed.first << "#{node.object_id} [label=\"()\"];"
super
end
- def visit_CAT(node)
- @nodes << "#{node.object_id} [label=\"○\"];"
+ def visit_CAT(node, seed)
+ seed.first << "#{node.object_id} [label=\"○\"];"
super
end
- def visit_STAR(node)
- @nodes << "#{node.object_id} [label=\"*\"];"
+ def visit_STAR(node, seed)
+ seed.first << "#{node.object_id} [label=\"*\"];"
super
end
- def visit_OR(node)
- @nodes << "#{node.object_id} [label=\"|\"];"
+ def visit_OR(node, seed)
+ seed.first << "#{node.object_id} [label=\"|\"];"
super
end
- def terminal(node)
+ def terminal(node, seed)
value = node.left
- @nodes << "#{node.object_id} [label=\"#{value}\"];"
+ seed.first << "#{node.object_id} [label=\"#{value}\"];"
+ seed
end
+ INSTANCE = new
end
end
end