1
module ActionController
3
# BEFORE: 0.191446860631307 ms/url
4
# AFTER: 0.029847304022858 ms/url
7
# Route recognition is slow due to one-by-one iterating over
8
# a whole routeset (each map.resources generates at least 14 routes)
9
# and matching weird regexps on each step.
11
# We optimize this by skipping all URI segments that 100% sure can't
12
# be matched, moving deeper in a tree of routes (where node == segment)
13
# until first possible match is accured. In such case, we start walking
14
# a flat list of routes, matching them with accurate matcher.
15
# So, first step: search a segment tree for the first relevant index.
16
# Second step: iterate routes starting with that index.
18
# How tree is walked? We can do a recursive tests, but it's smarter:
19
# We just create a tree of if-s and elsif-s matching segments.
21
# We have segments of 3 flavors:
22
# 1) nil (no segment, route finished)
23
# 2) const-dot-dynamic (like "/posts.:xml", "/preview.:size.jpg")
24
# 3) const (like "/posts", "/comments")
25
# 4) dynamic ("/:id", "file.:size.:extension")
27
# We split incoming string into segments and iterate over them.
28
# When segment is nil, we drop immediately, on a current node index.
29
# When segment is equal to some const, we step into branch.
30
# If none constants matched, we step into 'dynamic' branch (it's a last).
31
# If we can't match anything, we drop to last index on a level.
33
# Note: we maintain the original routes order, so we finish building
34
# steps on a first dynamic segment.
37
# Example. Given the routes:
40
# 2 /posts/:id/comments
44
# 6 /users/:id/profile
46
# request_uri = /users/123
48
# There will be only 4 iterations:
49
# 1) segm test for /posts prefix, skip all /posts/* routes
50
# 2) segm test for /users/
51
# 3) segm test for /users/:id
52
# (jump to list index = 5)
53
# 4) full test for /users/:id => here we are!
55
def recognize_path(path, environment={})
56
result = recognize_optimized(path, environment) and return result
58
# Route was not recognized. Try to find out why (maybe wrong verb).
59
allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, environment.merge(:method => verb)) } }
61
if environment[:method] && !HTTP_METHODS.include?(environment[:method])
62
raise NotImplemented.new(*allows)
64
raise MethodNotAllowed.new(*allows)
66
raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
70
def segment_tree(routes)
74
routes.each do |route|
76
# not fast, but runs only once
77
segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s })
80
segments.each do |seg|
81
seg = :dynamic if seg && seg[0] == ?:
82
node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg
83
node = node[node.size - 1][1]
89
def generate_code(list, padding=' ', level = 0)
91
return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0])
93
body = padding + "(seg = segments[#{level}]; \n"
103
body += padding + "#{start ? 'if' : 'elsif'} true\n"
104
body += generate_code(sub, padding + " ", level + 1)
106
elsif tag == nil && !was_nil
108
body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n"
109
body += generate_code(sub, padding + " ", level + 1)
111
body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n"
112
body += generate_code(sub, padding + " ", level + 1)
116
body += padding + "else\n"
117
body += padding + " #{list[0]}\n"
118
body += padding + "end)\n"
122
# this must be really fast
123
def to_plain_segments(str)
127
segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also
133
def write_recognize_optimized!
134
tree = segment_tree(routes)
135
body = generate_code(tree)
137
remove_recognize_optimized!
140
def recognize_optimized(path, env)
141
segments = to_plain_segments(path)
143
return nil unless index
144
while index < routes.size
145
result = routes[index].recognize(path, env) and return result
150
}, '(recognize_optimized)', 1
153
def clear_recognize_optimized!
154
remove_recognize_optimized!
155
write_recognize_optimized!
158
def remove_recognize_optimized!
159
if respond_to?(:recognize_optimized)
161
remove_method :recognize_optimized