aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/routing/route.rb
blob: eba05a3c5a5217a30c88958639ff408e5a393249 (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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
require 'active_support/core_ext/object/misc'

module ActionController
  module Routing
    class Route #:nodoc:
      attr_accessor :segments, :requirements, :conditions, :optimise

      def initialize(segments = [], requirements = {}, conditions = {})
        @segments = segments
        @requirements = requirements
        @conditions = conditions

        if !significant_keys.include?(:action) && !requirements[:action]
          @requirements[:action] = "index"
          @significant_keys << :action
        end

        # Routes cannot use the current string interpolation method
        # if there are user-supplied <tt>:requirements</tt> as the interpolation
        # code won't raise RoutingErrors when generating
        has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp }
        if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
          @optimise = false
        else
          @optimise = true
        end
      end

      # Indicates whether the routes should be optimised with the string interpolation
      # version of the named routes methods.
      def optimise?
        @optimise && ActionController::Base::optimise_named_routes
      end

      def segment_keys
        segments.collect do |segment|
          segment.key if segment.respond_to? :key
        end.compact
      end
      
      def required_segment_keys
        required_segments = segments.select {|seg| (!seg.optional? && !seg.is_a?(DividerSegment)) || seg.is_a?(PathSegment) }
        required_segments.collect { |seg| seg.key if seg.respond_to?(:key)}.compact
      end

      # Build a query string from the keys of the given hash. If +only_keys+
      # is given (as an array), only the keys indicated will be used to build
      # the query string. The query string will correctly build array parameter
      # values.
      def build_query_string(hash, only_keys = nil)
        elements = []

        (only_keys || hash.keys).each do |key|
          if value = hash[key]
            elements << value.to_query(key)
          end
        end

        elements.empty? ? '' : "?#{elements.sort * '&'}"
      end

      # A route's parameter shell contains parameter values that are not in the
      # route's path, but should be placed in the recognized hash.
      #
      # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
      #
      #   map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
      #
      def parameter_shell
        @parameter_shell ||= {}.tap do |shell|
          requirements.each do |key, requirement|
            shell[key] = requirement unless requirement.is_a? Regexp
          end
        end
      end

      # Return an array containing all the keys that are used in this route. This
      # includes keys that appear inside the path, and keys that have requirements
      # placed upon them.
      def significant_keys
        @significant_keys ||= [].tap do |sk|
          segments.each { |segment| sk << segment.key if segment.respond_to? :key }
          sk.concat requirements.keys
          sk.uniq!
        end
      end

      # Return a hash of key/value pairs representing the keys in the route that
      # have defaults, or which are specified by non-regexp requirements.
      def defaults
        @defaults ||= {}.tap do |hash|
          segments.each do |segment|
            next unless segment.respond_to? :default
            hash[segment.key] = segment.default unless segment.default.nil?
          end
          requirements.each do |key,req|
            next if Regexp === req || req.nil?
            hash[key] = req
          end
        end
      end

      def matches_controller_and_action?(controller, action)
        prepare_matching!
        (@controller_requirement.nil? || @controller_requirement === controller) &&
        (@action_requirement.nil? || @action_requirement === action)
      end

      def to_s
        @to_s ||= begin
          segs = segments.inject("") { |str,s| str << s.to_s }
          "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
        end
      end

      # TODO: Route should be prepared and frozen on initialize
      def freeze
        unless frozen?
          write_generation!
          write_recognition!
          prepare_matching!

          parameter_shell
          significant_keys
          defaults
          to_s
        end

        super
      end

      def generate(options, hash, expire_on = {})
        path, hash = generate_raw(options, hash, expire_on)
        append_query_string(path, hash, extra_keys(options))
      end

      def generate_extras(options, hash, expire_on = {})
        path, hash = generate_raw(options, hash, expire_on)
        [path, extra_keys(options)]
      end

      private
        def requirement_for(key)
          return requirements[key] if requirements.key? key
          segments.each do |segment|
            return segment.regexp if segment.respond_to?(:key) && segment.key == key
          end
          nil
        end

        # Write and compile a +generate+ method for this Route.
        def write_generation!
          # Build the main body of the generation
          body = "expired = false\n#{generation_extraction}\n#{generation_structure}"

          # If we have conditions that must be tested first, nest the body inside an if
          body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
          args = "options, hash, expire_on = {}"

          # Nest the body inside of a def block, and then compile it.
          raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
          instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"

          # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
          # are the same as the keys that were recalled from the previous request. Thus,
          # we can use the expire_on.keys to determine which keys ought to be used to build
          # the query string. (Never use keys from the recalled request when building the
          # query string.)

          raw_method
        end

        # Build several lines of code that extract values from the options hash. If any
        # of the values are missing or rejected then a return will be executed.
        def generation_extraction
          segments.collect do |segment|
            segment.extraction_code
          end.compact * "\n"
        end

        # Produce a condition expression that will check the requirements of this route
        # upon generation.
        def generation_requirements
          requirement_conditions = requirements.collect do |key, req|
            if req.is_a? Regexp
              value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
              "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
            else
              "hash[:#{key}] == #{req.inspect}"
            end
          end
          requirement_conditions * ' && ' unless requirement_conditions.empty?
        end

        def generation_structure
          segments.last.string_structure segments[0..-2]
        end

        # Write and compile a +recognize+ method for this Route.
        def write_recognition!
          # Create an if structure to extract the params from a match if it occurs.
          body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
          body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"

          # Build the method declaration and compile it
          method_decl = "def recognize(path, env = {})\n#{body}\nend"
          instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
          method_decl
        end

        # Plugins may override this method to add other conditions, like checks on
        # host, subdomain, and so forth. Note that changes here only affect route
        # recognition, not generation.
        def recognition_conditions
          result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
          result << "[conditions[:method]].flatten.include?(env[:method])" if conditions[:method]
          result
        end

        # Build the regular expression pattern that will match this route.
        def recognition_pattern(wrap = true)
          pattern = ''
          segments.reverse_each do |segment|
            pattern = segment.build_pattern pattern
          end
          wrap ? ("\\A" + pattern + "\\Z") : pattern
        end

        # Write the code to extract the parameters from a matched route.
        def recognition_extraction
          next_capture = 1
          extraction = segments.collect do |segment|
            x = segment.match_extraction(next_capture)
            next_capture += segment.number_of_captures
            x
          end
          extraction.compact
        end

        # Generate the query string with any extra keys in the hash and append
        # it to the given path, returning the new path.
        def append_query_string(path, hash, query_keys = nil)
          return nil unless path
          query_keys ||= extra_keys(hash)
          "#{path}#{build_query_string(hash, query_keys)}"
        end

        # Determine which keys in the given hash are "extra". Extra keys are
        # those that were not used to generate a particular route. The extra
        # keys also do not include those recalled from the prior request, nor
        # do they include any keys that were implied in the route (like a
        # <tt>:controller</tt> that is required, but not explicitly used in the
        # text of the route.)
        def extra_keys(hash, recall = {})
          (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
        end

        def prepare_matching!
          unless defined? @matching_prepared
            @controller_requirement = requirement_for(:controller)
            @action_requirement = requirement_for(:action)
            @matching_prepared = true
          end
        end
    end
  end
end