route_set.rb 20.1 KB
Newer Older
A
Aaron Patterson 已提交
1
require 'journey'
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
        # 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
40
        # a user error. However, if the controller was retrieved through a dynamic
41
        # 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

        def initialize
86 87 88
          @routes  = {}
          @helpers = []
          @module  = Module.new
89 90
        end

91 92 93 94
        def helper_names
          self.module.instance_methods.map(&:to_s)
        end

95
        def clear!
96 97
          @routes.clear
          @helpers.clear
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
        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

        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 已提交
145

146 147 148 149 150 151
            @module.module_eval do
              remove_possible_method selector

              define_method(selector) do |*args|
                inner_options = args.extract_options!
                result = options.dup
152 153

                if args.any?
154
                  result[:_positional_args] = args
155
                  result[:_positional_keys] = route.segment_keys
156 157
                end

158
                result.merge(inner_options)
159
              end
160 161 162

              protected selector
            end
163 164 165
            helpers << selector
          end

J
José Valim 已提交
166 167 168 169 170 171 172 173 174 175 176 177 178
          # 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')
          #
179 180 181 182
          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 已提交
183
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
184
              remove_possible_method :#{selector}
185
              def #{selector}(*args)
186
                url_for(#{hash_access_method}(*args))
187
              end
J
José Valim 已提交
188
            END_EVAL
189 190 191 192
            helpers << selector
          end
      end

193
      attr_accessor :formatter, :set, :named_routes, :default_scope, :router
194
      attr_accessor :disable_clear_and_finalize, :resources_path_names
195
      attr_accessor :default_url_options, :request_class, :valid_conditions
196

197 198
      alias :routes :set

199 200 201 202
      def self.default_resources_path_names
        { :new => 'new', :edit => 'edit' }
      end

203
      def initialize(request_class = ActionDispatch::Request)
204
        self.named_routes = NamedRouteCollection.new
205
        self.resources_path_names = self.class.default_resources_path_names.dup
206
        self.default_url_options = {}
207

208
        self.request_class = request_class
209 210 211 212 213 214 215 216
        @valid_conditions = {}

        request_class.public_instance_methods.each { |m|
          @valid_conditions[m.to_sym] = true
        }
        @valid_conditions[:controller] = true
        @valid_conditions[:action] = true

217
        self.valid_conditions.delete(:id)
218

219 220
        @append                     = []
        @prepend                    = []
J
Joshua Peek 已提交
221
        @disable_clear_and_finalize = false
222
        @finalized                  = false
223 224 225 226 227 228

        @set    = Journey::Routes.new
        @router = Journey::Router.new(@set, {
          :parameters_key => PARAMETERS_KEY,
          :request_class  => request_class})
        @formatter = Journey::Formatter.new @set
229 230
      end

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

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

242 243 244 245
      def prepend(&block)
        @prepend << block
      end

C
Carl Lerche 已提交
246
      def eval_block(block)
247 248 249 250 251
        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
252
        mapper = Mapper.new(self)
253 254
        if default_scope
          mapper.with_default_scope(default_scope, &block)
255
        else
256
          mapper.instance_exec(&block)
257
        end
258 259
      end

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

266
      def clear!
267
        @finalized = false
268
        named_routes.clear
269 270
        set.clear
        formatter.clear
271
        @prepend.each { |blk| eval_block(blk) }
272 273
      end

274 275 276
      module MountedHelpers #:nodoc:
        extend ActiveSupport::Concern
        include UrlFor
P
Piotr Sarnacki 已提交
277 278
      end

279 280 281 282
      # Contains all the mounted helpers accross different
      # engines and the `main_app` helper for the application.
      # You can include this in your classes if you want to
      # access routes for other engines.
283
      def mounted_helpers
P
Piotr Sarnacki 已提交
284 285 286
        MountedHelpers
      end

287 288 289
      def define_mounted_helper(name)
        return if MountedHelpers.method_defined?(name)

P
Piotr Sarnacki 已提交
290 291 292
        routes = self
        MountedHelpers.class_eval do
          define_method "_#{name}" do
293
            RoutesProxy.new(routes, _routes_context)
P
Piotr Sarnacki 已提交
294 295 296 297 298 299 300 301 302 303
          end
        end

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

304
      def url_helpers
305 306 307 308 309 310 311 312 313 314 315 316 317
        @url_helpers ||= begin
          routes = self

          Module.new do
            extend ActiveSupport::Concern
            include UrlFor

            # Define url_for in the singleton level so one can do:
            # Rails.application.routes.url_helpers.url_for(args)
            @_routes = routes
            class << self
              delegate :url_for, :to => '@_routes'
            end
C
Carlhuda 已提交
318

319 320 321 322
            # Make named_routes available in the module singleton
            # as well, so one can do:
            # Rails.application.routes.url_helpers.posts_path
            extend routes.named_routes.module
C
Carlhuda 已提交
323

324 325 326
            # Any class that includes this module will get all
            # named routes...
            include routes.named_routes.module
327

328 329 330 331
            # plus a singleton class method called _routes ...
            included do
              singleton_class.send(:redefine_method, :_routes) { routes }
            end
332

333 334 335 336
            # And an instance method _routes. Note that
            # UrlFor (included in this module) add extra
            # conveniences for working with @_routes.
            define_method(:_routes) { @_routes || routes }
C
Carlhuda 已提交
337
          end
338
        end
C
Carlhuda 已提交
339 340
      end

341 342 343 344
      def empty?
        routes.empty?
      end

345
      def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
346
        raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
347 348 349 350 351

        path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
        conditions = build_conditions(conditions, valid_conditions, path.names.map { |x| x.to_sym })

        route = @set.add_route(app, path, conditions, defaults, name)
352
        named_routes[name] = route if name && !named_routes[name]
353 354 355
        route
      end

356 357 358 359 360 361 362
      def build_path(path, requirements, separators, anchor)
        strexp = Journey::Router::Strexp.new(
            path,
            requirements,
            SEPARATORS,
            anchor)

363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
        pattern = Journey::Path::Pattern.new(strexp)

        builder = Journey::GTG::Builder.new pattern.spec

        # Get all the symbol nodes followed by literals that are not the
        # dummy node.
        symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n|
          builder.followpos(n).first.literal?
        }

        # Get all the symbol nodes preceded by literals.
        symbols.concat pattern.spec.find_all(&:literal?).map { |n|
          builder.followpos(n).first
        }.find_all(&:symbol?)

        symbols.each { |x|
          x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/
        }

        pattern
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
      end
      private :build_path

      def build_conditions(current_conditions, req_predicates, path_values)
        conditions = current_conditions.dup

        verbs = conditions[:request_method] || []

        # Rack-Mount requires that :request_method be a regular expression.
        # :request_method represents the HTTP verb that matches this route.
        #
        # Here we munge values before they get sent on to rack-mount.
        unless verbs.empty?
          conditions[:request_method] = %r[^#{verbs.join('|')}$]
        end
        conditions.delete_if { |k,v| !(req_predicates.include?(k) || path_values.include?(k)) }

        conditions
      end
      private :build_conditions

404
      class Generator #:nodoc:
405 406 407 408
        PARAMETERIZE = lambda do |name, value|
          if name == :controller
            value
          elsif value.is_a?(Array)
J
Jeremy Kemper 已提交
409 410 411
            value.map { |v| v.to_param }.join('/')
          elsif param = value.to_param
            param
412
          end
413
        end
414

415
        attr_reader :options, :recall, :set, :named_route
416 417 418 419 420 421 422 423 424 425 426 427 428 429

        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
430

431 432
        def controller
          @controller ||= @options[:controller]
433 434
        end

435 436 437
        def current_controller
          @recall[:controller]
        end
438

439 440
        def use_recall_for(key)
          if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
441 442 443 444 445
            if named_route_exists?
              @options[key] = @recall.delete(key) if segment_keys.include?(key)
            else
              @options[key] = @recall.delete(key)
            end
446 447
          end
        end
448

449 450 451 452 453 454 455 456 457
        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".
458

459 460 461 462
          if options[:controller]
            options[:action]     ||= 'index'
            options[:controller]   = options[:controller].to_s
          end
463

464 465 466 467
          if options[:action]
            options[:action] = options[:action].to_s
          end
        end
468

469 470 471 472 473 474 475 476 477 478 479 480
        # 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
481

482 483 484
        # if the current controller is "foo/bar/baz" and :controller => "baz/bat"
        # is specified, the controller becomes "foo/baz/bat"
        def use_relative_controller!
485
          if !named_route && different_controller? && !controller.start_with?("/")
486 487 488 489 490 491
            old_parts = current_controller.split('/')
            size = controller.count("/") + 1
            parts = old_parts[0...-size] << controller
            @controller = @options[:controller] = parts.join("/")
          end
        end
492

493 494 495 496 497
        # 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'
498
          end
499
          recall[:action] = options.delete(:action) if options[:action] == 'index'
500
        end
501

502
        def generate
503
          path, params = @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
504

505
          raise_routing_error unless path
506

507
          return [path, params.keys] if @extras
508

509
          [path, params]
510
        rescue Journey::Router::RoutingError
511
          raise_routing_error
512 513
        end

514
        def raise_routing_error
515
          raise ActionController::RoutingError, "No route matches #{options.inspect}"
516
        end
517

518 519 520
        def different_controller?
          return false unless current_controller
          controller.to_param != current_controller.to_param
521
        end
522 523 524 525 526 527 528

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

          def segment_keys
529
            set.named_routes[named_route].segment_keys
530
          end
531 532 533 534 535 536 537 538 539 540 541 542 543
      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)
544
        Generator.new(options, recall, self, extras).generate
545 546
      end

547
      RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
548
                          :trailing_slash, :anchor, :params, :only_path, :script_name]
549 550 551 552

      def _generate_prefix(options = {})
        nil
      end
553

J
Joshua Peek 已提交
554
      def url_for(options)
555
        options = (options || {}).reverse_merge!(default_url_options)
556

557 558
        handle_positional_args(options)

559 560 561
        user, password = extract_authentication(options)
        path_segments  = options.delete(:_path_segments)
        script_name    = options.delete(:script_name)
562

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

565 566 567
        path_options = options.except(*RESERVED_OPTIONS)
        path_options = yield(path_options) if block_given?

568 569
        path_addition, params = generate(path_options, path_segments || {})
        path << path_addition
570
        params.merge!(options[:params] || {})
571

572
        ActionDispatch::Http::URL.url_for(options.merge!({
573 574 575 576 577
          :path => path,
          :params => params,
          :user => user,
          :password => password
        }))
578 579
      end

580
      def call(env)
581
        @router.call(env)
582 583
      end

584
      def recognize_path(path, environment = {})
585
        method = (environment[:method] || "GET").to_s.upcase
586
        path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://}
587

588 589 590
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
591
          raise ActionController::RoutingError, e.message
592 593
        end

594
        req = @request_class.new(env)
595
        @router.recognize(req) do |route, matches, params|
596 597
          params.each do |key, value|
            if value.is_a?(String)
598
              value = value.dup.force_encoding(Encoding::BINARY)
599
              params[key] = URI.parser.unescape(value)
600 601
            end
          end
602 603
          old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
          env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params)
604
          dispatcher = route.app
605 606 607
          while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do
            dispatcher = dispatcher.app
          end
608

J
José Valim 已提交
609
          if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
610 611 612 613 614
            dispatcher.prepare_params!(params)
            return params
          end
        end

J
Joshua Peek 已提交
615
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
616
      end
617 618

      private
619

620 621 622 623 624
        def extract_authentication(options)
          if options[:user] && options[:password]
            [options.delete(:user), options.delete(:password)]
          else
            nil
625 626 627
          end
        end

628 629 630 631 632 633 634
        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 已提交
635
          options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
636 637
        end

638 639
    end
  end
J
Joshua Peek 已提交
640
end