route_set.rb 18.3 KB
Newer Older
1
require 'rack/mount'
2
require 'forwardable'
3
require 'active_support/core_ext/object/blank'
4
require 'active_support/core_ext/object/to_query'
5
require 'active_support/core_ext/hash/slice'
6
require 'active_support/core_ext/module/remove_method'
7
require 'action_controller/metal/exceptions'
8

J
Joshua Peek 已提交
9
module ActionDispatch
10
  module Routing
J
Joshua Peek 已提交
11
    class RouteSet #:nodoc:
12 13
      PARAMETERS_KEY = 'action_dispatch.request.path_parameters'

14
      class Dispatcher #:nodoc:
J
José Valim 已提交
15 16
        def initialize(options={})
          @defaults = options[:defaults]
17
          @glob_param = options.delete(:glob)
W
wycats 已提交
18
          @controllers = {}
19 20 21 22
        end

        def call(env)
          params = env[PARAMETERS_KEY]
23
          prepare_params!(params)
J
José Valim 已提交
24 25 26 27 28 29

          # Just raise undefined constant errors if a controller was specified as default.
          unless controller = controller(params, @defaults.key?(:controller))
            return [404, {'X-Cascade' => 'pass'}, []]
          end

30
          dispatch(controller, params[:action], env)
31 32 33
        end

        def prepare_params!(params)
34 35
          merge_default_action!(params)
          split_glob_param!(params) if @glob_param
36
        end
37

38 39 40 41
        # If this is a default_controller (i.e. a controller specified by the user)
        # we should raise an error in case it's not found, because it usually means
        # an user error. However, if the controller was retrieved through a dynamic
        # segment, as in :controller(/:action), we should simply return nil and
42
        # delegate the control back to Rack cascade. Besides, if this is not a default
43 44
        # controller, it means we should respect the @scope[:module] parameter.
        def controller(params, default_controller=true)
W
wycats 已提交
45
          if params && params.key?(:controller)
46
            controller_param = params[:controller]
47
            controller_reference(controller_param)
48
          end
49
        rescue NameError => e
50
          raise ActionController::RoutingError, e.message, e.backtrace if default_controller
51 52
        end

53
      private
54

55
        def controller_reference(controller_param)
56 57
          controller_name = "#{controller_param.camelize}Controller"

58 59
          unless controller = @controllers[controller_param]
            controller = @controllers[controller_param] =
A
Aaron Patterson 已提交
60
              ActiveSupport::Dependencies.reference(controller_name)
61
          end
62
          controller.get(controller_name)
63 64 65 66 67 68 69 70 71 72 73
        end

        def dispatch(controller, action, env)
          controller.action(action).call(env)
        end

        def merge_default_action!(params)
          params[:action] ||= 'index'
        end

        def split_glob_param!(params)
74
          params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
75
        end
76 77
      end

78 79 80 81 82
      # A NamedRouteCollection instance is a collection of named routes, and also
      # maintains an anonymous module that can be used to install helpers for the
      # named routes.
      class NamedRouteCollection #:nodoc:
        include Enumerable
83
        attr_reader :routes, :helpers, :module
84 85 86 87 88

        def initialize
          clear!
        end

89 90 91 92
        def helper_names
          self.module.instance_methods.map(&:to_s)
        end

93 94 95 96
        def clear!
          @routes = {}
          @helpers = []

97 98
          @module ||= Module.new do
            instance_methods.each { |selector| remove_method(selector) }
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
          end
        end

        def add(name, route)
          routes[name.to_sym] = route
          define_named_route_methods(name, route)
        end

        def get(name)
          routes[name.to_sym]
        end

        alias []=   add
        alias []    get
        alias clear clear!

        def each
          routes.each { |name, route| yield name, route }
          self
        end

        def names
          routes.keys
        end

        def length
          routes.length
        end

        def reset!
          old_routes = routes.dup
          clear!
          old_routes.each do |name, route|
            add(name, route)
          end
        end

        def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
          reset! if regenerate
          Array(destinations).each do |dest|
139
            dest.__send__(:include, @module)
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
          end
        end

        private
          def url_helper_name(name, kind = :url)
            :"#{name}_#{kind}"
          end

          def hash_access_name(name, kind = :url)
            :"hash_for_#{name}_#{kind}"
          end

          def define_named_route_methods(name, route)
            {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
              hash = route.defaults.merge(:use_route => name).merge(opts)
              define_hash_access route, name, kind, hash
              define_url_helper route, name, kind, hash
            end
          end

          def define_hash_access(route, name, kind, options)
            selector = hash_access_name(name, kind)
J
José Valim 已提交
162 163 164

            # We use module_eval to avoid leaks
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
165
              remove_possible_method :#{selector}
166 167 168 169 170 171 172 173 174 175 176
              def #{selector}(*args)
                options = args.extract_options!

                if args.any?
                  options[:_positional_args] = args
                  options[:_positional_keys] = #{route.segment_keys.inspect}
                end

                options ? #{options.inspect}.merge(options) : #{options.inspect}
              end
              protected :#{selector}
J
José Valim 已提交
177
            END_EVAL
178 179 180
            helpers << selector
          end

J
José Valim 已提交
181 182 183 184 185 186 187 188 189 190 191 192 193
          # Create a url helper allowing ordered parameters to be associated
          # with corresponding dynamic segments, so you can do:
          #
          #   foo_url(bar, baz, bang)
          #
          # Instead of:
          #
          #   foo_url(:bar => bar, :baz => baz, :bang => bang)
          #
          # Also allow options hash, so you can do:
          #
          #   foo_url(bar, baz, bang, :sort_by => 'baz')
          #
194 195 196 197
          def define_url_helper(route, name, kind, options)
            selector = url_helper_name(name, kind)
            hash_access_method = hash_access_name(name, kind)

J
José Valim 已提交
198
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
199
              remove_possible_method :#{selector}
200
              def #{selector}(*args)
201
                url_for(#{hash_access_method}(*args))
202
              end
J
José Valim 已提交
203
            END_EVAL
204 205 206 207
            helpers << selector
          end
      end

208
      attr_accessor :set, :routes, :named_routes, :default_scope
209
      attr_accessor :disable_clear_and_finalize, :resources_path_names
210
      attr_accessor :default_url_options, :request_class, :valid_conditions
211

212 213 214 215
      def self.default_resources_path_names
        { :new => 'new', :edit => 'edit' }
      end

216
      def initialize(request_class = ActionDispatch::Request)
217 218
        self.routes = []
        self.named_routes = NamedRouteCollection.new
219
        self.resources_path_names = self.class.default_resources_path_names.dup
220
        self.default_url_options = {}
221

222
        self.request_class = request_class
223 224 225
        self.valid_conditions = request_class.public_instance_methods.map { |m| m.to_sym }
        self.valid_conditions.delete(:id)
        self.valid_conditions.push(:controller, :action)
226

C
Carl Lerche 已提交
227
        @append = []
228
        @prepend = []
J
Joshua Peek 已提交
229
        @disable_clear_and_finalize = false
230
        clear!
231 232
      end

233
      def draw(&block)
J
Joshua Peek 已提交
234
        clear! unless @disable_clear_and_finalize
C
Carl Lerche 已提交
235 236 237 238 239 240 241 242
        eval_block(block)
        finalize! unless @disable_clear_and_finalize
        nil
      end

      def append(&block)
        @append << block
      end
243

244 245 246 247
      def prepend(&block)
        @prepend << block
      end

C
Carl Lerche 已提交
248
      def eval_block(block)
249 250 251 252 253
        if block.arity == 1
          raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
            "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/ " <<
            "or add the rails_legacy_mapper gem to your Gemfile"
        end
254
        mapper = Mapper.new(self)
255 256
        if default_scope
          mapper.with_default_scope(default_scope, &block)
257
        else
258
          mapper.instance_exec(&block)
259
        end
260 261
      end

J
Joshua Peek 已提交
262
      def finalize!
263
        return if @finalized
C
Carl Lerche 已提交
264
        @append.each { |blk| eval_block(blk) }
265
        @finalized = true
J
Joshua Peek 已提交
266 267 268
        @set.freeze
      end

269
      def clear!
270
        @finalized = false
271 272
        routes.clear
        named_routes.clear
273 274 275 276
        @set = ::Rack::Mount::RouteSet.new(
          :parameters_key => PARAMETERS_KEY,
          :request_class  => request_class
        )
277
        @prepend.each { |blk| eval_block(blk) }
278 279 280 281 282 283 284
      end

      def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
        Array(destinations).each { |d| d.module_eval { include Helpers } }
        named_routes.install(destinations, regenerate_code)
      end

P
Piotr Sarnacki 已提交
285 286 287
      module MountedHelpers
      end

288
      def mounted_helpers
P
Piotr Sarnacki 已提交
289 290 291
        MountedHelpers
      end

292 293 294
      def define_mounted_helper(name)
        return if MountedHelpers.method_defined?(name)

P
Piotr Sarnacki 已提交
295 296 297
        routes = self
        MountedHelpers.class_eval do
          define_method "_#{name}" do
298
            RoutesProxy.new(routes, self._routes_context)
P
Piotr Sarnacki 已提交
299 300 301 302 303 304 305 306 307 308
          end
        end

        MountedHelpers.class_eval <<-RUBY
          def #{name}
            @#{name} ||= _#{name}
          end
        RUBY
      end

309 310
      def url_helpers
        @url_helpers ||= begin
311
          routes = self
C
Carlhuda 已提交
312

313
          helpers = Module.new do
C
Carlhuda 已提交
314
            extend ActiveSupport::Concern
315
            include UrlFor
C
Carlhuda 已提交
316

317
            @_routes = routes
318
            class << self
319
              delegate :url_for, :to => '@_routes'
320 321 322
            end
            extend routes.named_routes.module

C
Carlhuda 已提交
323 324
            # ROUTES TODO: install_helpers isn't great... can we make a module with the stuff that
            # we can include?
325
            # Yes plz - JP
C
Carlhuda 已提交
326
            included do
327
              routes.install_helpers(self)
328
              singleton_class.send(:redefine_method, :_routes) { routes }
329 330
            end

331
            define_method(:_routes) { @_routes || routes }
C
Carlhuda 已提交
332
          end
333 334

          helpers
C
Carlhuda 已提交
335 336 337
        end
      end

338 339 340 341
      def empty?
        routes.empty?
      end

342
      def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
343
        raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
344
        route = Route.new(self, app, conditions, requirements, defaults, name, anchor)
345
        @set.add_route(route.app, route.conditions, route.defaults, route.name)
346
        named_routes[name] = route if name
347 348 349 350
        routes << route
        route
      end

351
      class Generator #:nodoc:
352 353 354 355 356 357 358 359 360 361 362 363 364
        PARAMETERIZE = {
          :parameterize => lambda do |name, value|
            if name == :controller
              value
            elsif value.is_a?(Array)
              value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/')
            else
              return nil unless param = value.to_param
              param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/")
            end
          end
        }

365
        attr_reader :options, :recall, :set, :named_route
366 367 368 369 370 371 372 373 374 375 376 377 378 379

        def initialize(options, recall, set, extras = false)
          @named_route = options.delete(:use_route)
          @options     = options.dup
          @recall      = recall.dup
          @set         = set
          @extras      = extras

          normalize_options!
          normalize_controller_action_id!
          use_relative_controller!
          controller.sub!(%r{^/}, '') if controller
          handle_nil_action!
        end
380

381 382
        def controller
          @controller ||= @options[:controller]
383 384
        end

385 386 387
        def current_controller
          @recall[:controller]
        end
388

389 390
        def use_recall_for(key)
          if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
391 392 393 394 395
            if named_route_exists?
              @options[key] = @recall.delete(key) if segment_keys.include?(key)
            else
              @options[key] = @recall.delete(key)
            end
396 397
          end
        end
398

399 400 401 402 403 404 405 406 407
        def normalize_options!
          # If an explicit :controller was given, always make :action explicit
          # too, so that action expiry works as expected for things like
          #
          #   generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
          #
          # (the above is from the unit tests). In the above case, because the
          # controller was explicitly given, but no action, the action is implied to
          # be "index", not the recalled action of "show".
408

409 410 411 412
          if options[:controller]
            options[:action]     ||= 'index'
            options[:controller]   = options[:controller].to_s
          end
413

414 415 416 417
          if options[:action]
            options[:action] = options[:action].to_s
          end
        end
418

419 420 421 422 423 424 425 426 427 428 429 430
        # This pulls :controller, :action, and :id out of the recall.
        # The recall key is only used if there is no key in the options
        # or if the key in the options is identical. If any of
        # :controller, :action or :id is not found, don't pull any
        # more keys from the recall.
        def normalize_controller_action_id!
          @recall[:action] ||= 'index' if current_controller

          use_recall_for(:controller) or return
          use_recall_for(:action) or return
          use_recall_for(:id)
        end
431

432 433 434 435 436 437 438 439 440 441
        # if the current controller is "foo/bar/baz" and :controller => "baz/bat"
        # is specified, the controller becomes "foo/baz/bat"
        def use_relative_controller!
          if !named_route && different_controller?
            old_parts = current_controller.split('/')
            size = controller.count("/") + 1
            parts = old_parts[0...-size] << controller
            @controller = @options[:controller] = parts.join("/")
          end
        end
442

443 444 445 446 447
        # This handles the case of :action => nil being explicitly passed.
        # It is identical to :action => "index"
        def handle_nil_action!
          if options.has_key?(:action) && options[:action].nil?
            options[:action] = 'index'
448
          end
449
          recall[:action] = options.delete(:action) if options[:action] == 'index'
450
        end
451

452
        def generate
453
          path, params = @set.set.generate(:path_info, named_route, options, recall, PARAMETERIZE)
454

455
          raise_routing_error unless path
456

457
          return [path, params.keys] if @extras
458

459
          [path, params]
460
        rescue Rack::Mount::RoutingError
461
          raise_routing_error
462 463
        end

464
        def raise_routing_error
465
          raise ActionController::RoutingError, "No route matches #{options.inspect}"
466
        end
467

468 469 470
        def different_controller?
          return false unless current_controller
          controller.to_param != current_controller.to_param
471
        end
472 473 474 475 476 477 478

        private
          def named_route_exists?
            named_route && set.named_routes[named_route]
          end

          def segment_keys
479
            set.named_routes[named_route].segment_keys
480
          end
481 482 483 484 485 486 487 488 489 490 491 492 493
      end

      # Generate the path indicated by the arguments, and return an array of
      # the keys that were not used to generate it.
      def extra_keys(options, recall={})
        generate_extras(options, recall).last
      end

      def generate_extras(options, recall={})
        generate(options, recall, true)
      end

      def generate(options, recall = {}, extras = false)
494
        Generator.new(options, recall, self, extras).generate
495 496
      end

497
      RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
498
                          :trailing_slash, :anchor, :params, :only_path, :script_name]
499 500 501 502

      def _generate_prefix(options = {})
        nil
      end
503

J
Joshua Peek 已提交
504
      def url_for(options)
505
        finalize!
506
        options = (options || {}).reverse_merge!(default_url_options)
507

508 509
        handle_positional_args(options)

510 511 512
        user, password = extract_authentication(options)
        path_segments  = options.delete(:_path_segments)
        script_name    = options.delete(:script_name)
513

P
Piotr Sarnacki 已提交
514
        path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s
515

516 517 518
        path_options = options.except(*RESERVED_OPTIONS)
        path_options = yield(path_options) if block_given?

519 520
        path_addition, params = generate(path_options, path_segments || {})
        path << path_addition
521

522 523 524 525 526 527
        ActionDispatch::Http::URL.url_for(options.merge({
          :path => path,
          :params => params,
          :user => user,
          :password => password
        }))
528 529
      end

530
      def call(env)
531
        finalize!
532
        @set.call(env)
533 534
      end

535
      def recognize_path(path, environment = {})
536
        method = (environment[:method] || "GET").to_s.upcase
537
        path = Rack::Mount::Utils.normalize_path(path) unless path =~ %r{://}
538

539 540 541
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
542
          raise ActionController::RoutingError, e.message
543 544
        end

545
        req = @request_class.new(env)
J
Joshua Peek 已提交
546
        @set.recognize(req) do |route, matches, params|
547 548 549
          params.each do |key, value|
            if value.is_a?(String)
              value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware?
550
              params[key] = URI.parser.unescape(value)
551 552 553
            end
          end

554
          dispatcher = route.app
555 556 557
          while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do
            dispatcher = dispatcher.app
          end
558

J
José Valim 已提交
559
          if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
560 561 562 563 564
            dispatcher.prepare_params!(params)
            return params
          end
        end

J
Joshua Peek 已提交
565
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
566
      end
567 568

      private
569

570 571 572 573 574
        def extract_authentication(options)
          if options[:user] && options[:password]
            [options.delete(:user), options.delete(:password)]
          else
            nil
575 576 577
          end
        end

578 579 580 581 582 583 584
        def handle_positional_args(options)
          return unless args = options.delete(:_positional_args)

          keys = options.delete(:_positional_keys)
          keys -= options.keys if args.size < keys.size - 1 # take format into account

          # Tell url_for to skip default_url_options
E
Emilio Tagua 已提交
585
          options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
586 587
        end

588 589
    end
  end
J
Joshua Peek 已提交
590
end