route_set.rb 22.3 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 14 15 16 17
      # Since the router holds references to many parts of the system
      # like engines, controllers and the application itself, inspecting
      # the route set can actually be really slow, therefore we default
      # alias inspect to to_s.
      alias inspect to_s

18 19
      PARAMETERS_KEY = 'action_dispatch.request.path_parameters'

20
      class Dispatcher #:nodoc:
J
José Valim 已提交
21 22
        def initialize(options={})
          @defaults = options[:defaults]
23
          @glob_param = options.delete(:glob)
W
wycats 已提交
24
          @controllers = {}
25 26 27 28
        end

        def call(env)
          params = env[PARAMETERS_KEY]
29
          prepare_params!(params)
J
José Valim 已提交
30 31 32 33 34 35

          # 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

36
          dispatch(controller, params[:action], env)
37 38 39
        end

        def prepare_params!(params)
40
          normalize_controller!(params)
41 42
          merge_default_action!(params)
          split_glob_param!(params) if @glob_param
43
        end
44

45 46
        # 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
47
        # a user error. However, if the controller was retrieved through a dynamic
48
        # segment, as in :controller(/:action), we should simply return nil and
49
        # delegate the control back to Rack cascade. Besides, if this is not a default
50 51
        # controller, it means we should respect the @scope[:module] parameter.
        def controller(params, default_controller=true)
W
wycats 已提交
52
          if params && params.key?(:controller)
53
            controller_param = params[:controller]
54
            controller_reference(controller_param)
55
          end
56
        rescue NameError => e
57
          raise ActionController::RoutingError, e.message, e.backtrace if default_controller
58 59
        end

60
      private
61

62
        def controller_reference(controller_param)
63 64
          controller_name = "#{controller_param.camelize}Controller"

65 66
          unless controller = @controllers[controller_param]
            controller = @controllers[controller_param] =
A
Aaron Patterson 已提交
67
              ActiveSupport::Dependencies.reference(controller_name)
68
          end
69
          controller.get(controller_name)
70 71 72 73 74 75
        end

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

76 77 78 79
        def normalize_controller!(params)
          params[:controller] = params[:controller].underscore if params.key?(:controller)
        end

80 81 82 83 84
        def merge_default_action!(params)
          params[:action] ||= 'index'
        end

        def split_glob_param!(params)
85
          params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
86
        end
87 88
      end

89 90 91 92 93
      # 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
94
        attr_reader :routes, :helpers, :module
95 96

        def initialize
97 98
          @routes  = {}
          @helpers = []
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
          @module  = Module.new do
            protected
            def handle_positional_args(args, options, route)
              inner_options = args.extract_options!
              result = options.dup

              if args.any?
                keys = route.segment_keys
                if args.size < keys.size - 1 # take format into account
                  keys -= self.url_options.keys  if self.respond_to?(:url_options)
                  keys -= options.keys
                end
                result.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
              end

              result.merge!(inner_options)
            end
          end
117 118
        end

119 120 121 122
        def helper_names
          self.module.instance_methods.map(&:to_s)
        end

123
        def clear!
124 125
          @routes.clear
          @helpers.clear
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
        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
155 156 157 158 159 160
          def url_helper_name(name, only_path)
            if only_path
              :"#{name}_path"
            else
              :"#{name}_url"
            end
161 162
          end

163 164 165 166 167 168
          def hash_access_name(name, only_path)
            if only_path 
              :"hash_for_#{name}_path"
            else
              :"hash_for_#{name}_url"
            end
169 170 171
          end

          def define_named_route_methods(name, route)
172 173 174 175
            [true, false].each do |only_path|
              hash = route.defaults.merge(:use_route => name, :only_path => only_path)
              define_hash_access route, name, hash
              define_url_helper route, name, hash
176 177 178
            end
          end

179 180
          def define_hash_access(route, name, options)
            selector = hash_access_name(name, options[:only_path])
J
José Valim 已提交
181

182
            @module.module_eval do
183 184
              redefine_method(selector) do |*args|
                self.handle_positional_args(args, options, route)
185
              end
186 187
              protected selector
            end
188 189 190
            helpers << selector
          end

J
José Valim 已提交
191 192 193 194 195 196 197 198 199 200 201 202 203
          # 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')
          #
204 205 206
          def define_url_helper(route, name, options)
            selector = url_helper_name(name, options[:only_path])
            hash_access_method = hash_access_name(name, options[:only_path])
207

208
            if optimize_helper?(route)
J
José Valim 已提交
209 210 211
              @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
                remove_possible_method :#{selector}
                def #{selector}(*args)
212
                  if args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation?
J
José Valim 已提交
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
                    options = #{options.inspect}.merge!(url_options)
                    options[:path] = "#{optimized_helper(route)}"
                    ActionDispatch::Http::URL.url_for(options)
                  else
                    url_for(#{hash_access_method}(*args))
                  end
                end
              END_EVAL
            else
              @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
                remove_possible_method :#{selector}
                def #{selector}(*args)
                  url_for(#{hash_access_method}(*args))
                end
              END_EVAL
            end

230 231
            helpers << selector
          end
J
José Valim 已提交
232

233
          # Clause check about when we need to generate an optimized helper.
234
          def optimize_helper?(route) #:nodoc:
235
            route.ast.grep(Journey::Nodes::Star).empty? && route.requirements.except(:controller, :action).empty?
J
José Valim 已提交
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
          end

          # Generates the interpolation to be used in the optimized helper.
          def optimized_helper(route)
            string_route = route.ast.to_s

            while string_route.gsub!(/\([^\)]*\)/, "")
              true
            end

            route.required_parts.each_with_index do |part, i|
              string_route.gsub!(part.inspect, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}")
            end

            string_route
          end
252 253
      end

254
      attr_accessor :formatter, :set, :named_routes, :default_scope, :router
255
      attr_accessor :disable_clear_and_finalize, :resources_path_names
256
      attr_accessor :default_url_options, :request_class, :valid_conditions
257

258 259
      alias :routes :set

260 261 262 263
      def self.default_resources_path_names
        { :new => 'new', :edit => 'edit' }
      end

264
      def initialize(request_class = ActionDispatch::Request)
265
        self.named_routes = NamedRouteCollection.new
266
        self.resources_path_names = self.class.default_resources_path_names.dup
267
        self.default_url_options = {}
268

269
        self.request_class = request_class
270 271 272 273 274 275 276 277
        @valid_conditions = {}

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

278
        self.valid_conditions.delete(:id)
279

280 281
        @append                     = []
        @prepend                    = []
J
Joshua Peek 已提交
282
        @disable_clear_and_finalize = false
283
        @finalized                  = false
284 285 286 287 288 289

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

292
      def draw(&block)
J
Joshua Peek 已提交
293
        clear! unless @disable_clear_and_finalize
C
Carl Lerche 已提交
294 295 296 297 298 299 300 301
        eval_block(block)
        finalize! unless @disable_clear_and_finalize
        nil
      end

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

303 304 305 306
      def prepend(&block)
        @prepend << block
      end

C
Carl Lerche 已提交
307
      def eval_block(block)
308 309
        if block.arity == 1
          raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
310
            "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
311
        end
312
        mapper = Mapper.new(self)
313 314
        if default_scope
          mapper.with_default_scope(default_scope, &block)
315
        else
316
          mapper.instance_exec(&block)
317
        end
318 319
      end

J
Joshua Peek 已提交
320
      def finalize!
321
        return if @finalized
C
Carl Lerche 已提交
322
        @append.each { |blk| eval_block(blk) }
323
        @finalized = true
J
Joshua Peek 已提交
324 325
      end

326
      def clear!
327
        @finalized = false
328
        named_routes.clear
329 330
        set.clear
        formatter.clear
331
        @prepend.each { |blk| eval_block(blk) }
332 333
      end

334 335 336
      module MountedHelpers #:nodoc:
        extend ActiveSupport::Concern
        include UrlFor
P
Piotr Sarnacki 已提交
337 338
      end

339 340 341 342
      # 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.
343
      def mounted_helpers
P
Piotr Sarnacki 已提交
344 345 346
        MountedHelpers
      end

347 348 349
      def define_mounted_helper(name)
        return if MountedHelpers.method_defined?(name)

P
Piotr Sarnacki 已提交
350 351 352
        routes = self
        MountedHelpers.class_eval do
          define_method "_#{name}" do
353
            RoutesProxy.new(routes, _routes_context)
P
Piotr Sarnacki 已提交
354 355 356 357 358 359 360 361 362 363
          end
        end

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

364
      def url_helpers
365 366 367 368 369 370 371 372 373 374 375
        @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
376
              delegate :url_for, :optimize_routes_generation?, :to => '@_routes'
377
            end
C
Carlhuda 已提交
378

379 380 381 382
            # 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 已提交
383

384 385 386
            # Any class that includes this module will get all
            # named routes...
            include routes.named_routes.module
387

388 389 390 391
            # plus a singleton class method called _routes ...
            included do
              singleton_class.send(:redefine_method, :_routes) { routes }
            end
392

393 394 395 396
            # 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 已提交
397
          end
398
        end
C
Carlhuda 已提交
399 400
      end

401 402 403 404
      def empty?
        routes.empty?
      end

405
      def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
406
        raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
407 408 409 410 411

        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)
412
        named_routes[name] = route if name && !named_routes[name]
413 414 415
        route
      end

416 417 418 419 420 421 422
      def build_path(path, requirements, separators, anchor)
        strexp = Journey::Router::Strexp.new(
            path,
            requirements,
            SEPARATORS,
            anchor)

423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
        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
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
      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

464
      class Generator #:nodoc:
465 466 467 468
        PARAMETERIZE = lambda do |name, value|
          if name == :controller
            value
          elsif value.is_a?(Array)
J
Jeremy Kemper 已提交
469 470 471
            value.map { |v| v.to_param }.join('/')
          elsif param = value.to_param
            param
472
          end
473
        end
474

475
        attr_reader :options, :recall, :set, :named_route
476 477 478 479 480 481 482 483 484 485 486 487 488 489

        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
490

491 492
        def controller
          @controller ||= @options[:controller]
493 494
        end

495 496 497
        def current_controller
          @recall[:controller]
        end
498

499 500
        def use_recall_for(key)
          if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
501 502 503 504 505
            if named_route_exists?
              @options[key] = @recall.delete(key) if segment_keys.include?(key)
            else
              @options[key] = @recall.delete(key)
            end
506 507
          end
        end
508

509 510 511 512 513 514 515 516 517
        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".
518

519 520 521 522
          if options[:controller]
            options[:action]     ||= 'index'
            options[:controller]   = options[:controller].to_s
          end
523

524 525 526 527
          if options[:action]
            options[:action] = options[:action].to_s
          end
        end
528

529 530 531 532 533 534 535 536 537 538 539 540
        # 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
541

542 543 544
        # if the current controller is "foo/bar/baz" and :controller => "baz/bat"
        # is specified, the controller becomes "foo/baz/bat"
        def use_relative_controller!
545
          if !named_route && different_controller? && !controller.start_with?("/")
546 547 548 549 550 551
            old_parts = current_controller.split('/')
            size = controller.count("/") + 1
            parts = old_parts[0...-size] << controller
            @controller = @options[:controller] = parts.join("/")
          end
        end
552

553 554 555 556 557
        # 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'
558
          end
559
          recall[:action] = options.delete(:action) if options[:action] == 'index'
560
        end
561

562
        def generate
563
          path, params = @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
564

565
          raise_routing_error unless path
566

567
          return [path, params.keys] if @extras
568

569
          [path, params]
570
        rescue Journey::Router::RoutingError
571
          raise_routing_error
572 573
        end

574
        def raise_routing_error
575
          raise ActionController::RoutingError, "No route matches #{options.inspect}"
576
        end
577

578 579 580
        def different_controller?
          return false unless current_controller
          controller.to_param != current_controller.to_param
581
        end
582 583 584 585 586 587 588

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

          def segment_keys
589
            set.named_routes[named_route].segment_keys
590
          end
591 592 593 594 595 596 597 598 599 600 601 602 603
      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)
604
        Generator.new(options, recall, self, extras).generate
605 606
      end

607
      RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
608
                          :trailing_slash, :anchor, :params, :only_path, :script_name]
609

J
José Valim 已提交
610 611 612 613
      def mounted?
        false
      end

614 615 616 617
      def optimize_routes_generation?
        !mounted? && default_url_options.empty?
      end

618 619 620
      def _generate_prefix(options = {})
        nil
      end
621

622
      # The +options+ argument must be +nil+ or a hash whose keys are *symbols*.
J
Joshua Peek 已提交
623
      def url_for(options)
624
        options = default_url_options.merge(options || {})
625

626 627
        user, password = extract_authentication(options)
        path_segments  = options.delete(:_path_segments)
J
José Valim 已提交
628
        script_name    = options.delete(:script_name).presence || _generate_prefix(options)
629

630 631 632
        path_options = options.except(*RESERVED_OPTIONS)
        path_options = yield(path_options) if block_given?

J
José Valim 已提交
633
        path, params = generate(path_options, path_segments || {})
634
        params.merge!(options[:params] || {})
635

636
        ActionDispatch::Http::URL.url_for(options.merge!({
637
          :path => path,
J
José Valim 已提交
638
          :script_name => script_name,
639 640 641 642
          :params => params,
          :user => user,
          :password => password
        }))
643 644
      end

645
      def call(env)
646
        @router.call(env)
647 648
      end

649
      def recognize_path(path, environment = {})
650
        method = (environment[:method] || "GET").to_s.upcase
651
        path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://}
652
        extras = environment[:extras] || {}
653

654 655 656
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
657
          raise ActionController::RoutingError, e.message
658 659
        end

660
        req = @request_class.new(env)
661
        @router.recognize(req) do |route, matches, params|
662
          params.merge!(extras)
663 664
          params.each do |key, value|
            if value.is_a?(String)
665
              value = value.dup.force_encoding(Encoding::BINARY)
666
              params[key] = URI.parser.unescape(value)
667 668
            end
          end
669 670
          old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
          env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params)
671
          dispatcher = route.app
672 673 674
          while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do
            dispatcher = dispatcher.app
          end
675

J
José Valim 已提交
676
          if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
677 678 679 680 681
            dispatcher.prepare_params!(params)
            return params
          end
        end

J
Joshua Peek 已提交
682
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
683
      end
684 685

      private
686

687 688 689 690 691
        def extract_authentication(options)
          if options[:user] && options[:password]
            [options.delete(:user), options.delete(:password)]
          else
            nil
692 693 694
          end
        end

695 696
    end
  end
J
Joshua Peek 已提交
697
end