aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/routing_optimisation.rb
blob: 1b447b17e71a3d6514d4c410d793a85e7218ad83 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
module ActionController
  module Routing
    # Much of the slow performance from routes comes from the 
    # complexity of expiry, :requirements matching, defaults providing
    # and figuring out which url pattern to use.  With named routes 
    # we can avoid the expense of finding the right route.  So if 
    # they've provided the right number of arguments, and have no
    # :requirements, we can just build up a string and return it.
    # 
    # To support building optimisations for other common cases, the 
    # generation code is seperated into several classes 
    module Optimisation
      def generate_optimisation_block(route, kind)
        return "" unless route.optimise?
        OPTIMISERS.inject("") do |memo, klazz|
          optimiser = klazz.new(route, kind)
          memo << "return #{optimiser.generation_code} if #{optimiser.guard_condition}\n"
        end
      end

      class Optimiser
        attr_reader :route, :kind
        def initialize(route, kind)
          @route = route
          @kind  = kind
        end

        def guard_condition
          'false'
        end

        def generation_code
          'nil'
        end
      end

      # Given a route:
      # map.person '/people/:id'
      #
      # If the user calls person_url(@person), we can simply
      # return a string like "/people/#{@person.to_param}" 
      # rather than triggering the expensive logic in url_for
      class PositionalArguments < Optimiser
        def guard_condition
          number_of_arguments = route.segment_keys.size
          # if they're using foo_url(:id=>2) it's one 
          # argument, but we don't want to generate /foos/id2
          if number_of_arguments == 1
            "args.size == 1 && !args.first.is_a?(Hash)"
          else
            "args.size == #{number_of_arguments}"
          end
        end

        def generation_code
          elements = []
          idx = 0


          if kind == :url
            elements << '#{request.protocol}'
            elements << '#{request.host_with_port}'
          end

          # The last entry in route.segments appears to # *always* be a
          # 'divider segment' for '/' but we have assertions to ensure that
          # we don't include the trailing slashes, so skip them.
          ((route.segments.size == 1 && kind == :path) ? route.segments : route.segments[0..-2]).each do |segment|
            if segment.is_a?(DynamicSegment)
              elements << "\#{URI.escape(args[#{idx}].to_param, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
              idx += 1
            else
              elements << segment.to_s
            end
          end
          %("#{elements * ''}")
        end
      end

      # This case is mostly the same as the positional arguments case
      # above, but it supports additional query parameters as the last 
      # argument
      class PositionalArgumentsWithAdditionalParams < PositionalArguments
        def guard_condition
          "args.size == #{route.segment_keys.size + 1}"
        end

        # This case uses almost the Use the same code as positional arguments, 
        # but add an args.last.to_query on the end
        def generation_code
          super.insert(-2, '?#{args.last.to_query}')
        end
      end

      OPTIMISERS = [PositionalArguments, PositionalArgumentsWithAdditionalParams]
    end
  end
end