route_set.rb 23.0 KB
Newer Older
1
require 'action_dispatch/journey'
2
require 'forwardable'
3
require 'thread_safe'
4
require 'active_support/concern'
5
require 'active_support/core_ext/object/to_query'
6
require 'active_support/core_ext/hash/slice'
7
require 'active_support/core_ext/module/remove_method'
8
require 'active_support/core_ext/array/extract_options'
9
require 'action_controller/metal/exceptions'
10
require 'action_dispatch/http/request'
11

J
Joshua Peek 已提交
12
module ActionDispatch
13
  module Routing
J
Joshua Peek 已提交
14
    class RouteSet #:nodoc:
15 16 17 18 19 20
      # 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

21 22
      PARAMETERS_KEY = 'action_dispatch.request.path_parameters'

23
      class Dispatcher #:nodoc:
24 25
        def initialize(defaults)
          @defaults = defaults
26
          @controller_class_names = ThreadSafe::Cache.new
27 28 29 30
        end

        def call(env)
          params = env[PARAMETERS_KEY]
31

32
          # If any of the path parameters has an invalid encoding then
33 34
          # raise since it's likely to trigger errors further on.
          params.each do |key, value|
35 36
            next unless value.respond_to?(:valid_encoding?)

37 38 39 40 41
            unless value.valid_encoding?
              raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
            end
          end

42
          prepare_params!(params)
J
José Valim 已提交
43 44 45 46 47 48

          # 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

49
          dispatch(controller, params[:action], env)
50 51 52
        end

        def prepare_params!(params)
53
          normalize_controller!(params)
54
          merge_default_action!(params)
55
        end
56

57 58
        # 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
59
        # a user error. However, if the controller was retrieved through a dynamic
60
        # segment, as in :controller(/:action), we should simply return nil and
61
        # delegate the control back to Rack cascade. Besides, if this is not a default
62 63
        # controller, it means we should respect the @scope[:module] parameter.
        def controller(params, default_controller=true)
W
wycats 已提交
64
          if params && params.key?(:controller)
65
            controller_param = params[:controller]
66
            controller_reference(controller_param)
67
          end
68
        rescue NameError => e
69
          raise ActionController::RoutingError, e.message, e.backtrace if default_controller
70 71
        end

72
      private
73

74
        def controller_reference(controller_param)
75 76
          const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
          ActiveSupport::Dependencies.constantize(const_name)
77 78 79 80 81 82
        end

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

83 84 85 86
        def normalize_controller!(params)
          params[:controller] = params[:controller].underscore if params.key?(:controller)
        end

87 88 89
        def merge_default_action!(params)
          params[:action] ||= 'index'
        end
90 91
      end

92 93 94 95 96
      # 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
97
        attr_reader :routes, :helpers, :module
98 99

        def initialize
100 101
          @routes  = {}
          @helpers = []
A
Aaron Patterson 已提交
102
          @module  = Module.new
103 104
        end

105
        def helper_names
106
          @helpers.map(&:to_s)
107 108
        end

109
        def clear!
110
          @helpers.each do |helper|
111
            @module.remove_possible_method helper
112 113
          end

114 115
          @routes.clear
          @helpers.clear
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
        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

144
        class UrlHelper # :nodoc:
145 146 147 148 149 150 151
          def self.create(route, options)
            if optimize_helper?(route)
              OptimizedUrlHelper.new(route, options)
            else
              new route, options
            end
          end
152

153
          def self.optimize_helper?(route)
154
            !route.glob? && route.path.requirements.empty?
155 156
          end

157
          class OptimizedUrlHelper < UrlHelper # :nodoc:
158
            attr_reader :arg_size
159

160 161
            def initialize(route, options)
              super
162 163
              @required_parts = @route.required_parts
              @arg_size       = @required_parts.size
164 165
            end

166 167
            def call(t, args)
              if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t)
168
                options = t.url_options.merge @options
169 170
                options[:path] = optimized_helper(args)
                ActionDispatch::Http::URL.url_for(options)
171
              else
172 173
                super
              end
174
            end
175

176
            private
177

178
            def optimized_helper(args)
A
Aaron Patterson 已提交
179
              params = parameterize_args(args)
180
              missing_keys = missing_keys(params)
181

182 183 184
              unless missing_keys.empty?
                raise_generation_error(params, missing_keys)
              end
185

186
              @route.format params
187 188
            end

189 190
            def optimize_routes_generation?(t)
              t.send(:optimize_routes_generation?)
191
            end
192

193
            def parameterize_args(args)
A
Aaron Patterson 已提交
194 195 196
              params = {}
              @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v }
              params
197 198 199 200 201
            end

            def missing_keys(args)
              args.select{ |part, arg| arg.nil? || arg.empty? }.keys
            end
202

203
            def raise_generation_error(args, missing_keys)
204 205 206
              constraints = Hash[@route.requirements.merge(args).sort]
              message = "No route matches #{constraints.inspect}"
              message << " missing required keys: #{missing_keys.sort.inspect}"
207 208 209

              raise ActionController::UrlGenerationError, message
            end
210
          end
211

212 213
          def initialize(route, options)
            @options      = options
214
            @segment_keys = route.segment_keys.uniq
215
            @route        = route
216 217
          end

218
          def call(t, args)
219 220 221
            controller_options = t.url_options
            options = controller_options.merge @options
            hash = handle_positional_args(controller_options, args, options, @segment_keys)
222
            t._routes.url_for(hash)
223
          end
224

225
          def handle_positional_args(controller_options, args, result, path_params)
226
            inner_options = args.extract_options!
227

228
            if args.size > 0
229 230 231
              if args.size < path_params.size - 1 # take format into account
                path_params -= controller_options.keys
                path_params -= result.keys
B
Bogdan Gusiev 已提交
232
              end
233 234 235
              path_params.each { |param|
                result[param] = inner_options[param] || args.shift
              }
236
            end
J
José Valim 已提交
237

238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
            result.merge!(inner_options)
          end
        end

        private
        # 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')
        #
        def define_url_helper(route, name, options)
          helper = UrlHelper.create(route, options.dup)

259
          @module.remove_possible_method name
260 261 262 263
          @module.module_eval do
            define_method(name) do |*args|
              helper.call self, args
            end
264
          end
265 266 267 268 269 270 271 272 273 274

          helpers << name
        end

        def define_named_route_methods(name, route)
          define_url_helper route, :"#{name}_path",
            route.defaults.merge(:use_route => name, :only_path => true)
          define_url_helper route, :"#{name}_url",
            route.defaults.merge(:use_route => name, :only_path => false)
        end
275 276
      end

277
      attr_accessor :formatter, :set, :named_routes, :default_scope, :router
278
      attr_accessor :disable_clear_and_finalize, :resources_path_names
B
Bogdan Gusiev 已提交
279
      attr_accessor :default_url_options, :request_class
280

281 282
      alias :routes :set

283 284 285 286
      def self.default_resources_path_names
        { :new => 'new', :edit => 'edit' }
      end

287
      def initialize(request_class = ActionDispatch::Request)
288
        self.named_routes = NamedRouteCollection.new
289
        self.resources_path_names = self.class.default_resources_path_names.dup
290
        self.default_url_options = {}
291
        self.request_class = request_class
292

293 294
        @append                     = []
        @prepend                    = []
J
Joshua Peek 已提交
295
        @disable_clear_and_finalize = false
296
        @finalized                  = false
297 298

        @set    = Journey::Routes.new
299
        @router = Journey::Router.new @set
300
        @formatter = Journey::Formatter.new @set
301 302
      end

303
      def draw(&block)
J
Joshua Peek 已提交
304
        clear! unless @disable_clear_and_finalize
C
Carl Lerche 已提交
305 306 307 308 309 310 311 312
        eval_block(block)
        finalize! unless @disable_clear_and_finalize
        nil
      end

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

314 315 316 317
      def prepend(&block)
        @prepend << block
      end

C
Carl Lerche 已提交
318
      def eval_block(block)
319 320
        if block.arity == 1
          raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
321
            "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
322
        end
323
        mapper = Mapper.new(self)
324 325
        if default_scope
          mapper.with_default_scope(default_scope, &block)
326
        else
327
          mapper.instance_exec(&block)
328
        end
329 330
      end

J
Joshua Peek 已提交
331
      def finalize!
332
        return if @finalized
C
Carl Lerche 已提交
333
        @append.each { |blk| eval_block(blk) }
334
        @finalized = true
J
Joshua Peek 已提交
335 336
      end

337
      def clear!
338
        @finalized = false
339
        named_routes.clear
340 341
        set.clear
        formatter.clear
342
        @prepend.each { |blk| eval_block(blk) }
343 344
      end

345 346 347
      module MountedHelpers #:nodoc:
        extend ActiveSupport::Concern
        include UrlFor
P
Piotr Sarnacki 已提交
348 349
      end

A
Akshay Vishnoi 已提交
350
      # Contains all the mounted helpers across different
351 352 353
      # 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.
354
      def mounted_helpers
P
Piotr Sarnacki 已提交
355 356 357
        MountedHelpers
      end

358 359 360
      def define_mounted_helper(name)
        return if MountedHelpers.method_defined?(name)

P
Piotr Sarnacki 已提交
361 362 363
        routes = self
        MountedHelpers.class_eval do
          define_method "_#{name}" do
364
            RoutesProxy.new(routes, _routes_context)
P
Piotr Sarnacki 已提交
365 366 367
          end
        end

368
        MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
P
Piotr Sarnacki 已提交
369
          def #{name}
370
            @_#{name} ||= _#{name}
P
Piotr Sarnacki 已提交
371 372 373 374
          end
        RUBY
      end

375
      def url_helpers
376 377 378 379 380 381 382 383 384 385 386
        @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
387
              delegate :url_for, :optimize_routes_generation?, :to => '@_routes'
388 389
              attr_reader :_routes
              def url_options; {}; end
390
            end
C
Carlhuda 已提交
391

392 393 394 395
            # 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 已提交
396

397 398 399
            # Any class that includes this module will get all
            # named routes...
            include routes.named_routes.module
400

401 402 403 404
            # plus a singleton class method called _routes ...
            included do
              singleton_class.send(:redefine_method, :_routes) { routes }
            end
405

406 407 408 409
            # 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 已提交
410
          end
411
        end
C
Carlhuda 已提交
412 413
      end

414 415 416 417
      def empty?
        routes.empty?
      end

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

421 422
        if name && named_routes[name]
          raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \
V
Vipul A M 已提交
423
            "You may have defined two routes with the same name using the `:as` option, or " \
424 425
            "you may be overriding a route already defined by a resource with the same naming. " \
            "For the latter, you can restrict the routes created with `resources` as explained here: \n" \
426 427 428
            "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
        end

429
        path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
B
Bogdan Gusiev 已提交
430
        conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })
431 432

        route = @set.add_route(app, path, conditions, defaults, name)
433
        named_routes[name] = route if name
434 435 436
        route
      end

437 438 439 440 441 442 443
      def build_path(path, requirements, separators, anchor)
        strexp = Journey::Router::Strexp.new(
            path,
            requirements,
            SEPARATORS,
            anchor)

444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
        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
464 465 466
      end
      private :build_path

B
Bogdan Gusiev 已提交
467
      def build_conditions(current_conditions, path_values)
468 469 470 471 472 473
        conditions = current_conditions.dup

        # 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.
474
        verbs = conditions[:request_method] || []
475 476 477
        unless verbs.empty?
          conditions[:request_method] = %r[^#{verbs.join('|')}$]
        end
478 479

        conditions.keep_if do |k, _|
480
          k == :action || k == :controller || k == :required_defaults ||
B
Bogdan Gusiev 已提交
481 482
            @request_class.public_method_defined?(k) || path_values.include?(k)
        end
483 484 485
      end
      private :build_conditions

486
      class Generator #:nodoc:
487 488 489 490
        PARAMETERIZE = lambda do |name, value|
          if name == :controller
            value
          elsif value.is_a?(Array)
J
Jeremy Kemper 已提交
491 492 493
            value.map { |v| v.to_param }.join('/')
          elsif param = value.to_param
            param
494
          end
495
        end
496

497
        attr_reader :options, :recall, :set, :named_route
498

499
        def initialize(options, recall, set)
500 501 502 503 504
          @named_route = options.delete(:use_route)
          @options     = options.dup
          @recall      = recall.dup
          @set         = set

505
          normalize_recall!
506 507 508
          normalize_options!
          normalize_controller_action_id!
          use_relative_controller!
509
          normalize_controller!
510
          normalize_action!
511
        end
512

513
        def controller
514
          @options[:controller]
515 516
        end

517 518 519
        def current_controller
          @recall[:controller]
        end
520

521 522
        def use_recall_for(key)
          if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
B
Bogdan Gusiev 已提交
523
            if !named_route_exists? || segment_keys.include?(key)
524
              @options[key] = @recall.delete(key)
525
            end
526 527
          end
        end
528

529 530 531 532 533
        # Set 'index' as default action for recall
        def normalize_recall!
          @recall[:action] ||= 'index'
        end

534 535 536 537
        def normalize_options!
          # If an explicit :controller was given, always make :action explicit
          # too, so that action expiry works as expected for things like
          #
A
AvnerCohen 已提交
538
          #   generate({controller: 'content'}, {controller: 'content', action: 'show'})
539 540 541 542
          #
          # (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".
543

544 545
          if options[:controller]
            options[:action]     ||= 'index'
546
            options[:controller]   = options[:controller].to_s
547
          end
548

549 550
          if options.key?(:action)
            options[:action] = (options[:action] || 'index').to_s
551 552
          end
        end
553

554 555 556 557 558 559 560 561 562 563
        # 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!
          use_recall_for(:controller) or return
          use_recall_for(:action) or return
          use_recall_for(:id)
        end
564

A
AvnerCohen 已提交
565
        # if the current controller is "foo/bar/baz" and controller: "baz/bat"
566 567
        # is specified, the controller becomes "foo/baz/bat"
        def use_relative_controller!
568
          if !named_route && different_controller? && !controller.start_with?("/")
569 570 571
            old_parts = current_controller.split('/')
            size = controller.count("/") + 1
            parts = old_parts[0...-size] << controller
572
            @options[:controller] = parts.join("/")
573 574
          end
        end
575

576 577 578 579 580
        # Remove leading slashes from controllers
        def normalize_controller!
          @options[:controller] = controller.sub(%r{^/}, '') if controller
        end

581 582 583 584
        # Move 'index' action from options to recall
        def normalize_action!
          if @options[:action] == 'index'
            @recall[:action] = @options.delete(:action)
585
          end
586
        end
587

588 589
        # Generates a path from routes, returns [path, params].
        # If no route is generated the formatter will raise ActionController::UrlGenerationError
590
        def generate
591
          @set.formatter.generate(named_route, options, recall, PARAMETERIZE)
592
        end
593

594 595 596
        def different_controller?
          return false unless current_controller
          controller.to_param != current_controller.to_param
597
        end
598 599 600 601 602 603 604

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

          def segment_keys
605
            set.named_routes[named_route].segment_keys
606
          end
607 608 609 610 611 612 613 614 615
      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={})
616 617
        path, params = generate(options, recall)
        return path, params.keys
618 619
      end

620 621
      def generate(options, recall = {})
        Generator.new(options, recall, self).generate
622 623
      end

624
      RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
625 626
                          :trailing_slash, :anchor, :params, :only_path, :script_name,
                          :original_script_name]
627

J
José Valim 已提交
628 629 630 631
      def mounted?
        false
      end

632 633 634 635
      def optimize_routes_generation?
        !mounted? && default_url_options.empty?
      end

636 637
      def find_script_name(options)
        options.delete :script_name
638
      end
639

640
      # The +options+ argument must be a hash whose keys are *symbols*.
J
Joshua Peek 已提交
641
      def url_for(options)
642
        options = default_url_options.merge options
643

644 645 646 647 648 649 650
        user = password = nil

        if options[:user] && options[:password]
          user     = options.delete :user
          password = options.delete :password
        end

651
        recall  = options.delete(:_recall) { {} }
652

653
        original_script_name = options.delete(:original_script_name)
654
        script_name = find_script_name options
655 656 657 658

        if script_name && original_script_name
          script_name = original_script_name + script_name
        end
659

660 661
        path_options = options.dup
        RESERVED_OPTIONS.each { |ro| path_options.delete ro }
662

663
        path, params = generate(path_options, recall)
664 665 666 667 668 669 670 671 672 673 674 675

        if options.key? :params
          params.merge! options[:params]
        end

        options[:path]        = path
        options[:script_name] = script_name
        options[:params]      = params
        options[:user]        = user
        options[:password]    = password

        ActionDispatch::Http::URL.url_for(options)
676 677
      end

678
      def call(env)
679 680 681
        req = request_class.new(env)
        req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
        @router.serve(req)
682 683
      end

684
      def recognize_path(path, environment = {})
685
        method = (environment[:method] || "GET").to_s.upcase
686
        path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://}
687
        extras = environment[:extras] || {}
688

689 690 691
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
692
          raise ActionController::RoutingError, e.message
693 694
        end

695
        req = request_class.new(env)
696
        @router.recognize(req) do |route, params|
697
          params.merge!(extras)
698 699
          params.each do |key, value|
            if value.is_a?(String)
700
              value = value.dup.force_encoding(Encoding::BINARY)
701
              params[key] = URI.parser.unescape(value)
702 703
            end
          end
704 705
          old_params = req.path_parameters
          req.path_parameters = old_params.merge params
706
          dispatcher = route.app
707
          if dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env)
708 709
            dispatcher = dispatcher.app
          end
710

711 712 713 714 715 716 717
          if dispatcher.is_a?(Dispatcher)
            if dispatcher.controller(params, false)
              dispatcher.prepare_params!(params)
              return params
            else
              raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
            end
718 719 720
          end
        end

J
Joshua Peek 已提交
721
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
722
      end
723 724
    end
  end
J
Joshua Peek 已提交
725
end