~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/actionpack/lib/action_controller/routing/route_set.rb

  • Committer: Michael Forrest
  • Date: 2010-10-15 16:28:50 UTC
  • Revision ID: michael.forrest@canonical.com-20101015162850-tj2vchanv0kr0dun
refrozeĀ gems

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
module ActionController
 
2
  module Routing
 
3
    class RouteSet #:nodoc:
 
4
      # Mapper instances are used to build routes. The object passed to the draw
 
5
      # block in config/routes.rb is a Mapper instance.
 
6
      #
 
7
      # Mapper instances have relatively few instance methods, in order to avoid
 
8
      # clashes with named routes.
 
9
      class Mapper #:doc:
 
10
        include ActionController::Resources
 
11
 
 
12
        def initialize(set) #:nodoc:
 
13
          @set = set
 
14
        end
 
15
 
 
16
        # Create an unnamed route with the provided +path+ and +options+. See
 
17
        # ActionController::Routing for an introduction to routes.
 
18
        def connect(path, options = {})
 
19
          @set.add_route(path, options)
 
20
        end
 
21
 
 
22
        # Creates a named route called "root" for matching the root level request.
 
23
        def root(options = {})
 
24
          if options.is_a?(Symbol)
 
25
            if source_route = @set.named_routes.routes[options]
 
26
              options = source_route.defaults.merge({ :conditions => source_route.conditions })
 
27
            end
 
28
          end
 
29
          named_route("root", '', options)
 
30
        end
 
31
 
 
32
        def named_route(name, path, options = {}) #:nodoc:
 
33
          @set.add_named_route(name, path, options)
 
34
        end
 
35
 
 
36
        # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
 
37
        # Example:
 
38
        #
 
39
        #   map.namespace(:admin) do |admin|
 
40
        #     admin.resources :products,
 
41
        #       :has_many => [ :tags, :images, :variants ]
 
42
        #   end
 
43
        #
 
44
        # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController.
 
45
        # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for
 
46
        # Admin::TagsController.
 
47
        def namespace(name, options = {}, &block)
 
48
          if options[:namespace]
 
49
            with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
 
50
          else
 
51
            with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
 
52
          end
 
53
        end
 
54
 
 
55
        def method_missing(route_name, *args, &proc) #:nodoc:
 
56
          super unless args.length >= 1 && proc.nil?
 
57
          @set.add_named_route(route_name, *args)
 
58
        end
 
59
      end
 
60
 
 
61
      # A NamedRouteCollection instance is a collection of named routes, and also
 
62
      # maintains an anonymous module that can be used to install helpers for the
 
63
      # named routes.
 
64
      class NamedRouteCollection #:nodoc:
 
65
        include Enumerable
 
66
        include ActionController::Routing::Optimisation
 
67
        attr_reader :routes, :helpers
 
68
 
 
69
        def initialize
 
70
          clear!
 
71
        end
 
72
 
 
73
        def clear!
 
74
          @routes = {}
 
75
          @helpers = []
 
76
 
 
77
          @module ||= Module.new
 
78
          @module.instance_methods.each do |selector|
 
79
            @module.class_eval { remove_method selector }
 
80
          end
 
81
        end
 
82
 
 
83
        def add(name, route)
 
84
          routes[name.to_sym] = route
 
85
          define_named_route_methods(name, route)
 
86
        end
 
87
 
 
88
        def get(name)
 
89
          routes[name.to_sym]
 
90
        end
 
91
 
 
92
        alias []=   add
 
93
        alias []    get
 
94
        alias clear clear!
 
95
 
 
96
        def each
 
97
          routes.each { |name, route| yield name, route }
 
98
          self
 
99
        end
 
100
 
 
101
        def names
 
102
          routes.keys
 
103
        end
 
104
 
 
105
        def length
 
106
          routes.length
 
107
        end
 
108
 
 
109
        def reset!
 
110
          old_routes = routes.dup
 
111
          clear!
 
112
          old_routes.each do |name, route|
 
113
            add(name, route)
 
114
          end
 
115
        end
 
116
 
 
117
        def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
 
118
          reset! if regenerate
 
119
          Array(destinations).each do |dest|
 
120
            dest.__send__(:include, @module)
 
121
          end
 
122
        end
 
123
 
 
124
        private
 
125
          def url_helper_name(name, kind = :url)
 
126
            :"#{name}_#{kind}"
 
127
          end
 
128
 
 
129
          def hash_access_name(name, kind = :url)
 
130
            :"hash_for_#{name}_#{kind}"
 
131
          end
 
132
 
 
133
          def define_named_route_methods(name, route)
 
134
            {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
 
135
              hash = route.defaults.merge(:use_route => name).merge(opts)
 
136
              define_hash_access route, name, kind, hash
 
137
              define_url_helper route, name, kind, hash
 
138
            end
 
139
          end
 
140
 
 
141
          def named_helper_module_eval(code, *args)
 
142
            @module.module_eval(code, *args)
 
143
          end
 
144
 
 
145
          def define_hash_access(route, name, kind, options)
 
146
            selector = hash_access_name(name, kind)
 
147
            named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
 
148
              def #{selector}(options = nil)                                      # def hash_for_users_url(options = nil)
 
149
                options ? #{options.inspect}.merge(options) : #{options.inspect}  #   options ? {:only_path=>false}.merge(options) : {:only_path=>false}
 
150
              end                                                                 # end
 
151
              protected :#{selector}                                              # protected :hash_for_users_url
 
152
            end_eval
 
153
            helpers << selector
 
154
          end
 
155
 
 
156
          def define_url_helper(route, name, kind, options)
 
157
            selector = url_helper_name(name, kind)
 
158
            # The segment keys used for positional paramters
 
159
 
 
160
            hash_access_method = hash_access_name(name, kind)
 
161
 
 
162
            # allow ordered parameters to be associated with corresponding
 
163
            # dynamic segments, so you can do
 
164
            #
 
165
            #   foo_url(bar, baz, bang)
 
166
            #
 
167
            # instead of
 
168
            #
 
169
            #   foo_url(:bar => bar, :baz => baz, :bang => bang)
 
170
            #
 
171
            # Also allow options hash, so you can do
 
172
            #
 
173
            #   foo_url(bar, baz, bang, :sort_by => 'baz')
 
174
            #
 
175
            named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
 
176
              def #{selector}(*args)                                                        # def users_url(*args)
 
177
                                                                                            #
 
178
                #{generate_optimisation_block(route, kind)}                                 #   #{generate_optimisation_block(route, kind)}
 
179
                                                                                            #
 
180
                opts = if args.empty? || Hash === args.first                                #   opts = if args.empty? || Hash === args.first
 
181
                  args.first || {}                                                          #     args.first || {}
 
182
                else                                                                        #   else
 
183
                  options = args.extract_options!                                           #     options = args.extract_options!
 
184
                  args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|  #     args = args.zip([]).inject({}) do |h, (v, k)|
 
185
                    h[k] = v                                                                #       h[k] = v
 
186
                    h                                                                       #       h
 
187
                  end                                                                       #     end
 
188
                  options.merge(args)                                                       #     options.merge(args)
 
189
                end                                                                         #   end
 
190
                                                                                            #
 
191
                url_for(#{hash_access_method}(opts))                                        #   url_for(hash_for_users_url(opts))
 
192
                                                                                            #
 
193
              end                                                                           # end
 
194
              #Add an alias to support the now deprecated formatted_* URL.                  # #Add an alias to support the now deprecated formatted_* URL.
 
195
              def formatted_#{selector}(*args)                                              # def formatted_users_url(*args)
 
196
                ActiveSupport::Deprecation.warn(                                            #   ActiveSupport::Deprecation.warn(
 
197
                  "formatted_#{selector}() has been deprecated. " +                         #     "formatted_users_url() has been deprecated. " +
 
198
                  "Please pass format to the standard " +                                   #     "Please pass format to the standard " +
 
199
                  "#{selector} method instead.", caller)                                    #     "users_url method instead.", caller)
 
200
                #{selector}(*args)                                                          #   users_url(*args)
 
201
              end                                                                           # end
 
202
              protected :#{selector}                                                        # protected :users_url
 
203
            end_eval
 
204
            helpers << selector
 
205
          end
 
206
      end
 
207
 
 
208
      attr_accessor :routes, :named_routes, :configuration_files
 
209
 
 
210
      def initialize
 
211
        self.configuration_files = []
 
212
 
 
213
        self.routes = []
 
214
        self.named_routes = NamedRouteCollection.new
 
215
 
 
216
        clear_recognize_optimized!
 
217
      end
 
218
 
 
219
      # Subclasses and plugins may override this method to specify a different
 
220
      # RouteBuilder instance, so that other route DSL's can be created.
 
221
      def builder
 
222
        @builder ||= RouteBuilder.new
 
223
      end
 
224
 
 
225
      def draw
 
226
        yield Mapper.new(self)
 
227
        install_helpers
 
228
      end
 
229
 
 
230
      def clear!
 
231
        routes.clear
 
232
        named_routes.clear
 
233
        @combined_regexp = nil
 
234
        @routes_by_controller = nil
 
235
        # This will force routing/recognition_optimization.rb
 
236
        # to refresh optimisations.
 
237
        clear_recognize_optimized!
 
238
      end
 
239
 
 
240
      def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
 
241
        Array(destinations).each { |d| d.module_eval { include Helpers } }
 
242
        named_routes.install(destinations, regenerate_code)
 
243
      end
 
244
 
 
245
      def empty?
 
246
        routes.empty?
 
247
      end
 
248
 
 
249
      def add_configuration_file(path)
 
250
        self.configuration_files << path
 
251
      end
 
252
 
 
253
      # Deprecated accessor
 
254
      def configuration_file=(path)
 
255
        add_configuration_file(path)
 
256
      end
 
257
      
 
258
      # Deprecated accessor
 
259
      def configuration_file
 
260
        configuration_files
 
261
      end
 
262
 
 
263
      def load!
 
264
        Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones
 
265
        clear!
 
266
        load_routes!
 
267
      end
 
268
 
 
269
      # reload! will always force a reload whereas load checks the timestamp first
 
270
      alias reload! load!
 
271
 
 
272
      def reload
 
273
        if configuration_files.any? && @routes_last_modified
 
274
          if routes_changed_at == @routes_last_modified
 
275
            return # routes didn't change, don't reload
 
276
          else
 
277
            @routes_last_modified = routes_changed_at
 
278
          end
 
279
        end
 
280
 
 
281
        load!
 
282
      end
 
283
 
 
284
      def load_routes!
 
285
        if configuration_files.any?
 
286
          configuration_files.each { |config| load(config) }
 
287
          @routes_last_modified = routes_changed_at
 
288
        else
 
289
          add_route ":controller/:action/:id"
 
290
        end
 
291
      end
 
292
      
 
293
      def routes_changed_at
 
294
        routes_changed_at = nil
 
295
        
 
296
        configuration_files.each do |config|
 
297
          config_changed_at = File.stat(config).mtime
 
298
 
 
299
          if routes_changed_at.nil? || config_changed_at > routes_changed_at
 
300
            routes_changed_at = config_changed_at 
 
301
          end
 
302
        end
 
303
        
 
304
        routes_changed_at
 
305
      end
 
306
 
 
307
      def add_route(path, options = {})
 
308
        options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) }
 
309
        route = builder.build(path, options)
 
310
        routes << route
 
311
        route
 
312
      end
 
313
 
 
314
      def add_named_route(name, path, options = {})
 
315
        # TODO - is options EVER used?
 
316
        name = options[:name_prefix] + name.to_s if options[:name_prefix]
 
317
        named_routes[name.to_sym] = add_route(path, options)
 
318
      end
 
319
 
 
320
      def options_as_params(options)
 
321
        # If an explicit :controller was given, always make :action explicit
 
322
        # too, so that action expiry works as expected for things like
 
323
        #
 
324
        #   generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
 
325
        #
 
326
        # (the above is from the unit tests). In the above case, because the
 
327
        # controller was explicitly given, but no action, the action is implied to
 
328
        # be "index", not the recalled action of "show".
 
329
        #
 
330
        # great fun, eh?
 
331
 
 
332
        options_as_params = options.clone
 
333
        options_as_params[:action] ||= 'index' if options[:controller]
 
334
        options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
 
335
        options_as_params
 
336
      end
 
337
 
 
338
      def build_expiry(options, recall)
 
339
        recall.inject({}) do |expiry, (key, recalled_value)|
 
340
          expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
 
341
          expiry
 
342
        end
 
343
      end
 
344
 
 
345
      # Generate the path indicated by the arguments, and return an array of
 
346
      # the keys that were not used to generate it.
 
347
      def extra_keys(options, recall={})
 
348
        generate_extras(options, recall).last
 
349
      end
 
350
 
 
351
      def generate_extras(options, recall={})
 
352
        generate(options, recall, :generate_extras)
 
353
      end
 
354
 
 
355
      def generate(options, recall = {}, method=:generate)
 
356
        named_route_name = options.delete(:use_route)
 
357
        generate_all = options.delete(:generate_all)
 
358
        if named_route_name
 
359
          named_route = named_routes[named_route_name]
 
360
          options = named_route.parameter_shell.merge(options)
 
361
        end
 
362
 
 
363
        options = options_as_params(options)
 
364
        expire_on = build_expiry(options, recall)
 
365
 
 
366
        if options[:controller]
 
367
          options[:controller] = options[:controller].to_s
 
368
        end
 
369
        # if the controller has changed, make sure it changes relative to the
 
370
        # current controller module, if any. In other words, if we're currently
 
371
        # on admin/get, and the new controller is 'set', the new controller
 
372
        # should really be admin/set.
 
373
        if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
 
374
          old_parts = recall[:controller].split('/')
 
375
          new_parts = options[:controller].split('/')
 
376
          parts = old_parts[0..-(new_parts.length + 1)] + new_parts
 
377
          options[:controller] = parts.join('/')
 
378
        end
 
379
 
 
380
        # drop the leading '/' on the controller name
 
381
        options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
 
382
        merged = recall.merge(options)
 
383
 
 
384
        if named_route
 
385
          path = named_route.generate(options, merged, expire_on)
 
386
          if path.nil?
 
387
            raise_named_route_error(options, named_route, named_route_name)
 
388
          else
 
389
            return path
 
390
          end
 
391
        else
 
392
          merged[:action] ||= 'index'
 
393
          options[:action] ||= 'index'
 
394
 
 
395
          controller = merged[:controller]
 
396
          action = merged[:action]
 
397
 
 
398
          raise RoutingError, "Need controller and action!" unless controller && action
 
399
 
 
400
          if generate_all
 
401
            # Used by caching to expire all paths for a resource
 
402
            return routes.collect do |route|
 
403
              route.__send__(method, options, merged, expire_on)
 
404
            end.compact
 
405
          end
 
406
 
 
407
          # don't use the recalled keys when determining which routes to check
 
408
          future_routes, deprecated_routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }]
 
409
          routes = Routing.generate_best_match ? deprecated_routes : future_routes
 
410
 
 
411
          routes.each_with_index do |route, index|
 
412
            results = route.__send__(method, options, merged, expire_on)
 
413
            if results && (!results.is_a?(Array) || results.first)
 
414
              return results
 
415
            end
 
416
          end
 
417
        end
 
418
 
 
419
        raise RoutingError, "No route matches #{options.inspect}"
 
420
      end
 
421
 
 
422
      # try to give a helpful error message when named route generation fails
 
423
      def raise_named_route_error(options, named_route, named_route_name)
 
424
        diff = named_route.requirements.diff(options)
 
425
        unless diff.empty?
 
426
          raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
 
427
        else
 
428
          required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) }
 
429
          required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment
 
430
          raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route.  content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?"
 
431
        end
 
432
      end
 
433
 
 
434
      def call(env)
 
435
        request = Request.new(env)
 
436
        app = Routing::Routes.recognize(request)
 
437
        app.call(env).to_a
 
438
      end
 
439
 
 
440
      def recognize(request)
 
441
        params = recognize_path(request.path, extract_request_environment(request))
 
442
        request.path_parameters = params.with_indifferent_access
 
443
        "#{params[:controller].to_s.camelize}Controller".constantize
 
444
      end
 
445
 
 
446
      def recognize_path(path, environment={})
 
447
        raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path."
 
448
      end
 
449
 
 
450
      def routes_by_controller
 
451
        @routes_by_controller ||= Hash.new do |controller_hash, controller|
 
452
          controller_hash[controller] = Hash.new do |action_hash, action|
 
453
            action_hash[action] = Hash.new do |key_hash, keys|
 
454
              key_hash[keys] = [
 
455
                routes_for_controller_and_action_and_keys(controller, action, keys),
 
456
                deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
 
457
              ]
 
458
            end
 
459
          end
 
460
        end
 
461
      end
 
462
 
 
463
      def routes_for(options, merged, expire_on)
 
464
        raise "Need controller and action!" unless controller && action
 
465
        controller = merged[:controller]
 
466
        merged = options if expire_on[:controller]
 
467
        action = merged[:action] || 'index'
 
468
 
 
469
        routes_by_controller[controller][action][merged.keys][1]
 
470
      end
 
471
 
 
472
      def routes_for_controller_and_action(controller, action)
 
473
        ActiveSupport::Deprecation.warn "routes_for_controller_and_action() has been deprecated. Please use routes_for()"
 
474
        selected = routes.select do |route|
 
475
          route.matches_controller_and_action? controller, action
 
476
        end
 
477
        (selected.length == routes.length) ? routes : selected
 
478
      end
 
479
 
 
480
      def routes_for_controller_and_action_and_keys(controller, action, keys)
 
481
        routes.select do |route|
 
482
          route.matches_controller_and_action? controller, action
 
483
        end
 
484
      end
 
485
 
 
486
      def deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
 
487
        selected = routes.select do |route|
 
488
          route.matches_controller_and_action? controller, action
 
489
        end
 
490
        selected.sort_by do |route|
 
491
          (keys - route.significant_keys).length
 
492
        end
 
493
      end
 
494
 
 
495
      # Subclasses and plugins may override this method to extract further attributes
 
496
      # from the request, for use by route conditions and such.
 
497
      def extract_request_environment(request)
 
498
        { :method => request.method }
 
499
      end
 
500
    end
 
501
  end
 
502
end