route_set.rb 24.1 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
require 'action_dispatch/routing/endpoint'
12

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

23
      class Dispatcher < Routing::Endpoint
24 25
        def initialize(raise_on_name_error)
          @raise_on_name_error = raise_on_name_error
26
          @controller_class_names = ThreadSafe::Cache.new
27 28
        end

29 30 31
        def dispatcher?; true; end

        def serve(req)
32
          req.check_path_parameters!
33
          params = req.path_parameters
34

35
          prepare_params!(params)
J
José Valim 已提交
36

37
          controller = controller(params, @raise_on_name_error) do
J
José Valim 已提交
38 39 40
            return [404, {'X-Cascade' => 'pass'}, []]
          end

41
          dispatch(controller, params[:action], req)
42 43 44
        end

        def prepare_params!(params)
45
          normalize_controller!(params)
46
          merge_default_action!(params)
47
        end
48

49 50
        # 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
51
        # a user error. However, if the controller was retrieved through a dynamic
52
        # segment, as in :controller(/:action), we should simply return nil and
53
        # delegate the control back to Rack cascade. Besides, if this is not a default
54
        # controller, it means we should respect the @scope[:module] parameter.
55
        def controller(params, raise_on_name_error=true)
56
          controller_reference params.fetch(:controller) { yield }
57
        rescue NameError => e
58
          raise ActionController::RoutingError, e.message, e.backtrace if raise_on_name_error
59
          yield
60 61
        end

62 63 64
      protected

        attr_reader :controller_class_names
65

66
        def controller_reference(controller_param)
67
          const_name = controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
68
          ActiveSupport::Dependencies.constantize(const_name)
69 70
        end

71 72
      private

73 74
        def dispatch(controller, action, req)
          controller.action(action).call(req.env)
75 76
        end

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

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

86 87 88
      # 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.
89
      class NamedRouteCollection
90
        include Enumerable
91
        attr_reader :routes, :url_helpers_module, :path_helpers_module
92 93

        def initialize
94
          @routes  = {}
95 96
          @path_helpers = Set.new
          @url_helpers = Set.new
97 98
          @url_helpers_module  = Module.new
          @path_helpers_module = Module.new
99 100
        end

101
        def route_defined?(name)
102 103
          key = name.to_sym
          @path_helpers.include?(key) || @url_helpers.include?(key)
104 105
        end

106
        def helper_names
107
          @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s)
108 109
        end

110
        def clear!
111 112 113 114 115 116
          @path_helpers.each do |helper|
            @path_helpers_module.send :undef_method, helper
          end

          @url_helpers.each do |helper|
            @url_helpers_module.send  :undef_method, helper
117 118
          end

119
          @routes.clear
120 121
          @path_helpers.clear
          @url_helpers.clear
122 123 124
        end

        def add(name, route)
125 126 127 128
          key       = name.to_sym
          path_name = :"#{name}_path"
          url_name  = :"#{name}_url"

129
          if routes.key? key
130 131
            @path_helpers_module.send :undef_method, path_name
            @url_helpers_module.send  :undef_method, url_name
132 133
          end
          routes[key] = route
134
          define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH
135
          define_url_helper @url_helpers_module,  route, url_name,  route.defaults, name, UNKNOWN
136 137 138

          @path_helpers << path_name
          @url_helpers << url_name
139 140 141 142 143 144
        end

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

145
        def key?(name)
146
          return unless name
147 148 149
          routes.key? name.to_sym
        end

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
        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

167
        class UrlHelper
168
          def self.create(route, options, route_name, url_strategy)
169
            if optimize_helper?(route)
170
              OptimizedUrlHelper.new(route, options, route_name, url_strategy)
171
            else
172
              new route, options, route_name, url_strategy
173 174
            end
          end
175

176
          def self.optimize_helper?(route)
177
            !route.glob? && route.path.requirements.empty?
178 179
          end

180
          attr_reader :url_strategy, :route_name
181

182
          class OptimizedUrlHelper < UrlHelper
183
            attr_reader :arg_size
184

185
            def initialize(route, options, route_name, url_strategy)
186
              super
187 188
              @required_parts = @route.required_parts
              @arg_size       = @required_parts.size
189 190
            end

191 192
            def call(t, args, inner_options)
              if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
193
                options = t.url_options.merge @options
194
                options[:path] = optimized_helper(args)
195
                url_strategy.call options
196
              else
197 198
                super
              end
199
            end
200

201
            private
202

203
            def optimized_helper(args)
204 205 206
              params = parameterize_args(args) { |k|
                raise_generation_error(args)
              }
207

208
              @route.format params
209 210
            end

211 212
            def optimize_routes_generation?(t)
              t.send(:optimize_routes_generation?)
213
            end
214

215
            def parameterize_args(args)
A
Aaron Patterson 已提交
216
              params = {}
217 218 219 220 221 222
              @arg_size.times { |i|
                key = @required_parts[i]
                value = args[i].to_param
                yield key if value.nil? || value.empty?
                params[key] = value
              }
A
Aaron Patterson 已提交
223
              params
224 225
            end

226 227 228 229 230
            def raise_generation_error(args)
              missing_keys = []
              params = parameterize_args(args) { |missing_key|
                missing_keys << missing_key
              }
Y
Yang Bo 已提交
231
              constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}]
232 233
              message = "No route matches #{constraints.inspect}"
              message << " missing required keys: #{missing_keys.sort.inspect}"
234 235 236

              raise ActionController::UrlGenerationError, message
            end
237
          end
238

239
          def initialize(route, options, route_name, url_strategy)
240
            @options      = options
241
            @segment_keys = route.segment_keys.uniq
242
            @route        = route
243
            @url_strategy = url_strategy
244
            @route_name   = route_name
245 246
          end

247
          def call(t, args, inner_options)
248 249
            controller_options = t.url_options
            options = controller_options.merge @options
250
            hash = handle_positional_args(controller_options,
251
                                          inner_options || {},
252 253 254 255
                                          args,
                                          options,
                                          @segment_keys)

256
            t._routes.url_for(hash, route_name, url_strategy)
257
          end
258

259
          def handle_positional_args(controller_options, inner_options, args, result, path_params)
260
            if args.size > 0
261 262 263 264 265 266 267 268
              # take format into account
              if path_params.include?(:format)
                path_params_size = path_params.size - 1
              else
                path_params_size = path_params.size
              end

              if args.size < path_params_size
269 270
                path_params -= controller_options.keys
                path_params -= result.keys
B
Bogdan Gusiev 已提交
271
              end
272
              inner_options.each_key do |key|
S
schneems 已提交
273 274 275 276 277 278
                path_params.delete(key)
              end

              args.each_with_index do |arg, index|
                param = path_params[index]
                result[param] = arg if param
279
              end
280
            end
J
José Valim 已提交
281

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
            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')
        #
300
        def define_url_helper(mod, route, name, opts, route_key, url_strategy)
301
          helper = UrlHelper.create(route, opts, route_key, url_strategy)
302
          mod.module_eval do
303
            define_method(name) do |*args|
304 305 306
              options = nil
              options = args.pop if args.last.is_a? Hash
              helper.call self, args, options
307
            end
308
          end
309
        end
310 311
      end

312 313 314 315
      # strategy for building urls to send to the client
      PATH    = ->(options) { ActionDispatch::Http::URL.path_for(options) }
      UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }

316
      attr_accessor :formatter, :set, :named_routes, :default_scope, :router
317
      attr_accessor :disable_clear_and_finalize, :resources_path_names
318
      attr_accessor :default_url_options, :dispatcher_class
319
      attr_reader :env_key
320

321 322
      alias :routes :set

323 324 325 326
      def self.default_resources_path_names
        { :new => 'new', :edit => 'edit' }
      end

A
Aaron Patterson 已提交
327
      def self.new_with_config(config)
328 329 330
        route_set_config = DEFAULT_CONFIG

        # engines apparently don't have this set
A
Aaron Patterson 已提交
331
        if config.respond_to? :relative_url_root
332 333 334 335 336
          route_set_config.relative_url_root = config.relative_url_root
        end

        if config.respond_to? :api_only
          route_set_config.api_only = config.api_only
A
Aaron Patterson 已提交
337
        end
338 339

        new route_set_config
A
Aaron Patterson 已提交
340 341
      end

342
      Config = Struct.new :relative_url_root, :api_only
A
Aaron Patterson 已提交
343

344
      DEFAULT_CONFIG = Config.new(nil, false)
A
Aaron Patterson 已提交
345 346

      def initialize(config = DEFAULT_CONFIG)
347
        self.named_routes = NamedRouteCollection.new
A
Aaron Patterson 已提交
348
        self.resources_path_names = self.class.default_resources_path_names
349
        self.default_url_options = {}
350

A
Aaron Patterson 已提交
351
        @config                     = config
352 353
        @append                     = []
        @prepend                    = []
J
Joshua Peek 已提交
354
        @disable_clear_and_finalize = false
355
        @finalized                  = false
356
        @env_key                    = "ROUTES_#{object_id}_SCRIPT_NAME".freeze
357 358

        @set    = Journey::Routes.new
359
        @router = Journey::Router.new @set
360
        @formatter = Journey::Formatter.new self
361
        @dispatcher_class = Routing::RouteSet::Dispatcher
362 363
      end

A
Aaron Patterson 已提交
364 365 366 367
      def relative_url_root
        @config.relative_url_root
      end

368 369 370 371
      def api_only?
        @config.api_only
      end

372 373 374 375
      def request_class
        ActionDispatch::Request
      end

376
      def draw(&block)
J
Joshua Peek 已提交
377
        clear! unless @disable_clear_and_finalize
C
Carl Lerche 已提交
378 379 380 381 382 383 384 385
        eval_block(block)
        finalize! unless @disable_clear_and_finalize
        nil
      end

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

387 388 389 390
      def prepend(&block)
        @prepend << block
      end

C
Carl Lerche 已提交
391
      def eval_block(block)
392 393
        if block.arity == 1
          raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
394
            "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
395
        end
396
        mapper = Mapper.new(self)
397 398
        if default_scope
          mapper.with_default_scope(default_scope, &block)
399
        else
400
          mapper.instance_exec(&block)
401
        end
402
      end
A
Aaron Patterson 已提交
403
      private :eval_block
404

J
Joshua Peek 已提交
405
      def finalize!
406
        return if @finalized
C
Carl Lerche 已提交
407
        @append.each { |blk| eval_block(blk) }
408
        @finalized = true
J
Joshua Peek 已提交
409 410
      end

411
      def clear!
412
        @finalized = false
413
        named_routes.clear
414 415
        set.clear
        formatter.clear
416
        @prepend.each { |blk| eval_block(blk) }
417 418
      end

419 420
      def dispatcher(raise_on_name_error)
        dispatcher_class.new(raise_on_name_error)
421 422
      end

423
      module MountedHelpers
424 425
        extend ActiveSupport::Concern
        include UrlFor
P
Piotr Sarnacki 已提交
426 427
      end

A
Akshay Vishnoi 已提交
428
      # Contains all the mounted helpers across different
429 430 431
      # 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.
432
      def mounted_helpers
P
Piotr Sarnacki 已提交
433 434 435
        MountedHelpers
      end

436 437 438
      def define_mounted_helper(name)
        return if MountedHelpers.method_defined?(name)

P
Piotr Sarnacki 已提交
439
        routes = self
440 441
        helpers = routes.url_helpers

P
Piotr Sarnacki 已提交
442 443
        MountedHelpers.class_eval do
          define_method "_#{name}" do
444
            RoutesProxy.new(routes, _routes_context, helpers)
P
Piotr Sarnacki 已提交
445 446 447
          end
        end

448
        MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
P
Piotr Sarnacki 已提交
449
          def #{name}
450
            @_#{name} ||= _#{name}
P
Piotr Sarnacki 已提交
451 452 453 454
          end
        RUBY
      end

455
      def url_helpers(supports_path = true)
456 457 458 459 460 461 462 463 464 465 466 467 468
        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
            def url_for(options)
              @_routes.url_for(options)
            end
469

470 471
            def optimize_routes_generation?
              @_routes.optimize_routes_generation?
472 473
            end

474 475 476
            attr_reader :_routes
            def url_options; {}; end
          end
477

478
          url_helpers = routes.named_routes.url_helpers_module
C
Carlhuda 已提交
479

480 481 482 483
          # Make named_routes available in the module singleton
          # as well, so one can do:
          # Rails.application.routes.url_helpers.posts_path
          extend url_helpers
484

485 486 487
          # Any class that includes this module will get all
          # named routes...
          include url_helpers
488

489 490
          if supports_path
            path_helpers = routes.named_routes.path_helpers_module
491

492 493 494
            include path_helpers
            extend path_helpers
          end
495

496 497 498 499
          # plus a singleton class method called _routes ...
          included do
            singleton_class.send(:redefine_method, :_routes) { routes }
          end
500

501 502 503 504
          # And an instance method _routes. Note that
          # UrlFor (included in this module) add extra
          # conveniences for working with @_routes.
          define_method(:_routes) { @_routes || routes }
505

506 507
          define_method(:_generate_paths_by_default) do
            supports_path
508
          end
509 510

          private :_generate_paths_by_default
511
        end
C
Carlhuda 已提交
512 513
      end

514 515 516 517
      def empty?
        routes.empty?
      end

518
      def add_route(mapping, path_ast, name, anchor)
519
        raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
520

521 522
        if name && named_routes[name]
          raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \
V
Vipul A M 已提交
523
            "You may have defined two routes with the same name using the `:as` option, or " \
524 525
            "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" \
526 527 528
            "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
        end

A
Aaron Patterson 已提交
529
        route = @set.add_route(name, mapping)
530
        named_routes[name] = route if name
531 532 533
        route
      end

534
      class Generator
535 536 537
        PARAMETERIZE = lambda do |name, value|
          if name == :controller
            value
Y
yui-knk 已提交
538 539
          else
            value.to_param
540
          end
541
        end
542

543
        attr_reader :options, :recall, :set, :named_route
544

545 546
        def initialize(named_route, options, recall, set)
          @named_route = named_route
S
schneems 已提交
547
          @options     = options
548
          @recall      = recall
549 550
          @set         = set

551
          normalize_recall!
552 553 554
          normalize_options!
          normalize_controller_action_id!
          use_relative_controller!
555
          normalize_controller!
556
          normalize_action!
557
        end
558

559
        def controller
560
          @options[:controller]
561 562
        end

563 564 565
        def current_controller
          @recall[:controller]
        end
566

567 568
        def use_recall_for(key)
          if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
B
Bogdan Gusiev 已提交
569
            if !named_route_exists? || segment_keys.include?(key)
570
              @options[key] = @recall[key]
571
            end
572 573
          end
        end
574

575 576 577 578 579
        # Set 'index' as default action for recall
        def normalize_recall!
          @recall[:action] ||= 'index'
        end

580 581 582 583
        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 已提交
584
          #   generate({controller: 'content'}, {controller: 'content', action: 'show'})
585 586 587 588
          #
          # (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".
589

590 591
          if options[:controller]
            options[:action]     ||= 'index'
592
            options[:controller]   = options[:controller].to_s
593
          end
594

595 596
          if options.key?(:action)
            options[:action] = (options[:action] || 'index').to_s
597 598
          end
        end
599

600 601 602 603 604 605 606 607 608 609
        # 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
610

A
AvnerCohen 已提交
611
        # if the current controller is "foo/bar/baz" and controller: "baz/bat"
612 613
        # is specified, the controller becomes "foo/baz/bat"
        def use_relative_controller!
614
          if !named_route && different_controller? && !controller.start_with?("/")
615 616 617
            old_parts = current_controller.split('/')
            size = controller.count("/") + 1
            parts = old_parts[0...-size] << controller
618
            @options[:controller] = parts.join("/")
619 620
          end
        end
621

622 623
        # Remove leading slashes from controllers
        def normalize_controller!
S
schneems 已提交
624
          if controller
625 626
            if controller.start_with?("/".freeze)
              @options[:controller] = controller[1..-1]
S
schneems 已提交
627 628 629 630
            else
              @options[:controller] = controller
            end
          end
631 632
        end

633 634
        # Move 'index' action from options to recall
        def normalize_action!
S
schneems 已提交
635
          if @options[:action] == 'index'.freeze
636
            @recall[:action] = @options.delete(:action)
637
          end
638
        end
639

640 641
        # Generates a path from routes, returns [path, params].
        # If no route is generated the formatter will raise ActionController::UrlGenerationError
642
        def generate
643
          @set.formatter.generate(named_route, options, recall, PARAMETERIZE)
644
        end
645

646 647 648
        def different_controller?
          return false unless current_controller
          controller.to_param != current_controller.to_param
649
        end
650 651 652 653 654 655 656

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

          def segment_keys
657
            set.named_routes[named_route].segment_keys
658
          end
659 660 661 662 663 664 665 666 667
      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={})
668 669
        route_key = options.delete :use_route
        path, params = generate(route_key, options, recall)
670
        return path, params.keys
671 672
      end

673 674
      def generate(route_key, options, recall = {})
        Generator.new(route_key, options, recall, self).generate
675
      end
676
      private :generate
677

678
      RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
679 680
                          :trailing_slash, :anchor, :params, :only_path, :script_name,
                          :original_script_name]
681

682
      def optimize_routes_generation?
A
Aaron Patterson 已提交
683
        default_url_options.empty?
684 685
      end

686
      def find_script_name(options)
687
        options.delete(:script_name) || relative_url_root || ''
688
      end
689

690
      def path_for(options, route_name = nil)
A
Aaron Patterson 已提交
691 692 693
        url_for(options, route_name, PATH)
      end

694
      # The +options+ argument must be a hash whose keys are *symbols*.
695
      def url_for(options, route_name = nil, url_strategy = UNKNOWN)
696
        options = default_url_options.merge options
697

698 699 700 701 702 703 704
        user = password = nil

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

705
        recall  = options.delete(:_recall) { {} }
706

707
        original_script_name = options.delete(:original_script_name)
708
        script_name = find_script_name options
709

710
        if original_script_name
711 712
          script_name = original_script_name + script_name
        end
713

714 715
        path_options = options.dup
        RESERVED_OPTIONS.each { |ro| path_options.delete ro }
716

717
        path, params = generate(route_name, path_options, recall)
718 719 720 721 722 723 724 725 726 727 728

        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

729
        url_strategy.call options
730 731
      end

732
      def call(env)
733 734 735
        req = request_class.new(env)
        req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
        @router.serve(req)
736 737
      end

738
      def recognize_path(path, environment = {})
739
        method = (environment[:method] || "GET").to_s.upcase
740
        path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://}
741
        extras = environment[:extras] || {}
742

743 744 745
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
746
          raise ActionController::RoutingError, e.message
747 748
        end

749
        req = request_class.new(env)
750
        @router.recognize(req) do |route, params|
751
          params.merge!(extras)
752 753
          params.each do |key, value|
            if value.is_a?(String)
754
              value = value.dup.force_encoding(Encoding::BINARY)
755
              params[key] = URI.parser.unescape(value)
756 757
            end
          end
758 759
          old_params = req.path_parameters
          req.path_parameters = old_params.merge params
760
          app = route.app
761
          if app.matches?(req) && app.dispatcher?
762
            dispatcher = app.app
763

764
            dispatcher.controller(params, false) do
765 766
              raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
            end
767 768 769

            dispatcher.prepare_params!(params)
            return params
770 771 772
          end
        end

J
Joshua Peek 已提交
773
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
774
      end
775
    end
776
    # :startdoc:
777
  end
J
Joshua Peek 已提交
778
end