| | 1 | module ActionController |
|---|
| | 2 | module Routing |
|---|
| | 3 | # Route recognition is slow due to one-by-one iterating over |
|---|
| | 4 | # a whole routeset (each map.resources generates at least 14 routes) |
|---|
| | 5 | # and matching weird regexps on each step. |
|---|
| | 6 | # |
|---|
| | 7 | # We optimize this by moving through forwarding prefix anchors first, |
|---|
| | 8 | # and stupidly iterating thru routes when anchors' prefix is matched. |
|---|
| | 9 | # In other words, we're skipping a bunch of routes with a same prefix |
|---|
| | 10 | # if that prefix doesn't match. |
|---|
| | 11 | # |
|---|
| | 12 | # Example. Given the routes: |
|---|
| | 13 | # /posts/ |
|---|
| | 14 | # /posts/:id |
|---|
| | 15 | # /posts/:id/comments |
|---|
| | 16 | # /posts/blah |
|---|
| | 17 | # /users/ |
|---|
| | 18 | # /users/:id |
|---|
| | 19 | # |
|---|
| | 20 | # request_uri = /users/123 |
|---|
| | 21 | # |
|---|
| | 22 | # There will be only 3 iterations: |
|---|
| | 23 | # 1) test for /posts prefix, skip all /posts/* routes |
|---|
| | 24 | # 2) test for /users/ |
|---|
| | 25 | # 3) test for /users/:id => success |
|---|
| | 26 | |
|---|
| | 27 | class RouteSet |
|---|
| | 28 | def recognize_path(path, environment={}) |
|---|
| | 29 | result = recognize_optimized(path, environment) and return result |
|---|
| | 30 | |
|---|
| | 31 | # Route was not recognized. Try to find out why (maybe wrong verb). |
|---|
| | 32 | allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } } |
|---|
| | 33 | |
|---|
| | 34 | if environment[:method] && !HTTP_METHODS.include?(environment[:method]) |
|---|
| | 35 | raise NotImplemented.new(*allows) |
|---|
| | 36 | elsif !allows.empty? |
|---|
| | 37 | raise MethodNotAllowed.new(*allows) |
|---|
| | 38 | else |
|---|
| | 39 | raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}" |
|---|
| | 40 | end |
|---|
| | 41 | end |
|---|
| | 42 | |
|---|
| | 43 | def recognize_optimized(path, env) |
|---|
| | 44 | (compile_recognize_optimized!; @compiled_recognize_optimized=true) unless @compiled_recognize_optimized |
|---|
| | 45 | |
|---|
| | 46 | anch = @first_anchor |
|---|
| | 47 | loop do |
|---|
| | 48 | r = anch.recognize(path, env) and return r |
|---|
| | 49 | anch = anch.next_anchor |
|---|
| | 50 | break unless anch |
|---|
| | 51 | end |
|---|
| | 52 | nil |
|---|
| | 53 | end |
|---|
| | 54 | |
|---|
| | 55 | # one-level version. |
|---|
| | 56 | def compile_recognize_optimized! |
|---|
| | 57 | |
|---|
| | 58 | @first_anchor = current_anchor = PrefixAnchor.new(routes, 0) |
|---|
| | 59 | i = 0 |
|---|
| | 60 | routes.each do |route| |
|---|
| | 61 | # init linked list |
|---|
| | 62 | pfx = prefix_for_anchor(route.segments) |
|---|
| | 63 | unless current_anchor.extend(pfx) |
|---|
| | 64 | new_anchor = PrefixAnchor.new(routes, current_anchor.last_index+1) |
|---|
| | 65 | current_anchor.next_anchor = new_anchor |
|---|
| | 66 | new_anchor.extend(pfx) |
|---|
| | 67 | current_anchor = new_anchor |
|---|
| | 68 | end |
|---|
| | 69 | end |
|---|
| | 70 | end |
|---|
| | 71 | |
|---|
| | 72 | def prefix_for_anchor(segments) |
|---|
| | 73 | return "" if segments.size == 1 && DividerSegment === segments[0] |
|---|
| | 74 | return "/"+segments[1].value if StaticSegment === segments[1] |
|---|
| | 75 | return :dynamic |
|---|
| | 76 | end |
|---|
| | 77 | end |
|---|
| | 78 | |
|---|
| | 79 | # Contains a prefix to test for. |
|---|
| | 80 | # If the test fails, all the anchor's routes are skipped. |
|---|
| | 81 | # Otherwise, all of them are iterated. |
|---|
| | 82 | class PrefixAnchor |
|---|
| | 83 | attr_accessor :next_anchor |
|---|
| | 84 | |
|---|
| | 85 | def initialize(rs, start = 0) |
|---|
| | 86 | @routes = rs |
|---|
| | 87 | @range = (start..start) |
|---|
| | 88 | @just_inited = true |
|---|
| | 89 | end |
|---|
| | 90 | def extend(pfx) |
|---|
| | 91 | return false if @prefix && pfx != @prefix |
|---|
| | 92 | @prefix = pfx |
|---|
| | 93 | @len = pfx == :dynamic ? -1 : pfx.size |
|---|
| | 94 | @range = (@range.first..(@range.last + (@just_inited ? 0 : 1))) |
|---|
| | 95 | @just_inited = false |
|---|
| | 96 | true |
|---|
| | 97 | end |
|---|
| | 98 | def last_index |
|---|
| | 99 | @range.last |
|---|
| | 100 | end |
|---|
| | 101 | def recognize(path, env) |
|---|
| | 102 | # prefix not matched |
|---|
| | 103 | if (@len > 0 && path[0, @len] != @prefix || @len == 0 && path !~ /^\/*$/) |
|---|
| | 104 | return nil |
|---|
| | 105 | end |
|---|
| | 106 | @range.each do |i| |
|---|
| | 107 | route = @routes[i] |
|---|
| | 108 | result = route.recognize(path, env) and return result |
|---|
| | 109 | end |
|---|
| | 110 | nil |
|---|
| | 111 | end |
|---|
| | 112 | end |
|---|
| | 113 | end |
|---|
| | 114 | end |