route_set.rb 17.8 KB
Newer Older
1
require 'rack/mount'
2
require 'forwardable'
3
require 'active_support/core_ext/object/to_query'
4
require 'active_support/core_ext/hash/slice'
5

J
Joshua Peek 已提交
6
module ActionDispatch
7
  module Routing
J
Joshua Peek 已提交
8
    class RouteSet #:nodoc:
9 10
      PARAMETERS_KEY = 'action_dispatch.request.path_parameters'

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

        def call(env)
          params = env[PARAMETERS_KEY]
20
          prepare_params!(params)
J
José Valim 已提交
21 22 23 24 25 26

          # 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

27
          dispatch(controller, params[:action], env)
28 29 30
        end

        def prepare_params!(params)
31 32
          merge_default_action!(params)
          split_glob_param!(params) if @glob_param
33
        end
34

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

50
      private
51

52 53 54 55 56
        def controller_reference(controller_param)
          unless controller = @controllers[controller_param]
            controller_name = "#{controller_param.camelize}Controller"
            controller = @controllers[controller_param] =
              ActiveSupport::Dependencies.ref(controller_name)
57
          end
58 59 60 61 62 63 64 65 66 67 68 69
          controller.get
        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)
70
          params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
71
        end
72 73
      end

74 75 76 77 78
      # 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
79
        attr_reader :routes, :helpers, :module
80 81 82 83 84

        def initialize
          clear!
        end

85 86 87 88
        def helper_names
          self.module.instance_methods.map(&:to_s)
        end

89 90 91 92
        def clear!
          @routes = {}
          @helpers = []

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

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

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

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

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

        def names
          routes.keys
        end

        def length
          routes.length
        end

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

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

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

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

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

          def define_hash_access(route, name, kind, options)
            selector = hash_access_name(name, kind)
J
José Valim 已提交
158 159 160

            # We use module_eval to avoid leaks
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
161
              remove_method :#{selector} if method_defined?(:#{selector})
162 163 164 165 166 167 168 169 170 171 172
              def #{selector}(*args)
                options = args.extract_options!

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

                options ? #{options.inspect}.merge(options) : #{options.inspect}
              end
              protected :#{selector}
J
José Valim 已提交
173
            END_EVAL
174 175 176
            helpers << selector
          end

J
José Valim 已提交
177 178 179 180 181 182 183 184 185 186 187 188 189
          # 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')
          #
190 191 192 193
          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 已提交
194
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
195
              remove_method :#{selector} if method_defined?(:#{selector})
196
              def #{selector}(*args)
197
                url_for(#{hash_access_method}(*args))
198
              end
J
José Valim 已提交
199
            END_EVAL
200 201 202 203
            helpers << selector
          end
      end

204
      attr_accessor :set, :routes, :named_routes, :default_scope
205
      attr_accessor :disable_clear_and_finalize, :resources_path_names
206
      attr_accessor :default_url_options, :request_class, :valid_conditions
207

208 209 210 211
      def self.default_resources_path_names
        { :new => 'new', :edit => 'edit' }
      end

212
      def initialize(request_class = ActionDispatch::Request)
213 214
        self.routes = []
        self.named_routes = NamedRouteCollection.new
215
        self.resources_path_names = self.class.default_resources_path_names.dup
216
        self.default_url_options = {}
217

218
        self.request_class = request_class
219 220 221
        self.valid_conditions = request_class.public_instance_methods.map { |m| m.to_sym }
        self.valid_conditions.delete(:id)
        self.valid_conditions.push(:controller, :action)
222

C
Carl Lerche 已提交
223
        @append = []
J
Joshua Peek 已提交
224
        @disable_clear_and_finalize = false
225
        clear!
226 227
      end

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

        nil
      end

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

C
Carl Lerche 已提交
240
      def eval_block(block)
241
        mapper = Mapper.new(self)
242 243
        if default_scope
          mapper.with_default_scope(default_scope, &block)
244
        else
245
          mapper.instance_exec(&block)
246
        end
247 248
      end

J
Joshua Peek 已提交
249
      def finalize!
250
        return if @finalized
C
Carl Lerche 已提交
251
        @append.each { |blk| eval_block(blk) }
252
        @finalized = true
J
Joshua Peek 已提交
253 254 255
        @set.freeze
      end

256
      def clear!
J
Joshua Peek 已提交
257 258
        # Clear the controller cache so we may discover new ones
        @controller_constraints = nil
259
        @finalized = false
260 261
        routes.clear
        named_routes.clear
262 263 264 265
        @set = ::Rack::Mount::RouteSet.new(
          :parameters_key => PARAMETERS_KEY,
          :request_class  => request_class
        )
266 267 268 269 270 271 272
      end

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

P
Piotr Sarnacki 已提交
273 274 275
      module MountedHelpers
      end

276
      def mounted_helpers(name = :main_app)
P
Piotr Sarnacki 已提交
277 278 279 280
        define_mounted_helper(name) if name
        MountedHelpers
      end

281 282 283
      def define_mounted_helper(name)
        return if MountedHelpers.method_defined?(name)

P
Piotr Sarnacki 已提交
284 285 286
        routes = self
        MountedHelpers.class_eval do
          define_method "_#{name}" do
287
            RoutesProxy.new(routes, self._routes_context)
P
Piotr Sarnacki 已提交
288 289 290 291 292 293 294 295 296 297
          end
        end

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

298 299
      def url_helpers
        @url_helpers ||= begin
300
          routes = self
C
Carlhuda 已提交
301

302
          helpers = Module.new do
C
Carlhuda 已提交
303
            extend ActiveSupport::Concern
304
            include UrlFor
C
Carlhuda 已提交
305

306
            @_routes = routes
307
            class << self
308
              delegate :url_for, :to => '@_routes'
309 310 311
            end
            extend routes.named_routes.module

C
Carlhuda 已提交
312 313
            # ROUTES TODO: install_helpers isn't great... can we make a module with the stuff that
            # we can include?
314
            # Yes plz - JP
C
Carlhuda 已提交
315
            included do
316
              routes.install_helpers(self)
317
              singleton_class.send(:redefine_method, :_routes) { routes }
318 319
            end

320
            define_method(:_routes) { @_routes || routes }
C
Carlhuda 已提交
321
          end
322 323

          helpers
C
Carlhuda 已提交
324 325 326
        end
      end

327 328 329 330
      def empty?
        routes.empty?
      end

331
      def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
332
        route = Route.new(self, app, conditions, requirements, defaults, name, anchor)
333
        @set.add_route(*route)
334
        named_routes[name] = route if name
335 336 337 338
        routes << route
        route
      end

339
      class Generator #:nodoc:
340 341 342 343 344 345 346 347 348 349 350 351 352
        PARAMETERIZE = {
          :parameterize => lambda do |name, value|
            if name == :controller
              value
            elsif value.is_a?(Array)
              value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/')
            else
              return nil unless param = value.to_param
              param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/")
            end
          end
        }

353
        attr_reader :options, :recall, :set, :named_route
354 355 356 357 358 359 360 361 362 363 364 365 366 367

        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
368

369 370
        def controller
          @controller ||= @options[:controller]
371 372
        end

373 374 375
        def current_controller
          @recall[:controller]
        end
376

377 378
        def use_recall_for(key)
          if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
379 380 381 382 383
            if named_route_exists?
              @options[key] = @recall.delete(key) if segment_keys.include?(key)
            else
              @options[key] = @recall.delete(key)
            end
384 385
          end
        end
386

387 388 389 390 391 392 393 394 395
        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".
396

397 398 399 400
          if options[:controller]
            options[:action]     ||= 'index'
            options[:controller]   = options[:controller].to_s
          end
401

402 403 404 405
          if options[:action]
            options[:action] = options[:action].to_s
          end
        end
406

407 408 409 410 411 412 413 414 415 416 417 418
        # 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
419

420 421 422 423 424 425 426 427 428 429
        # if the current controller is "foo/bar/baz" and :controller => "baz/bat"
        # is specified, the controller becomes "foo/baz/bat"
        def use_relative_controller!
          if !named_route && different_controller?
            old_parts = current_controller.split('/')
            size = controller.count("/") + 1
            parts = old_parts[0...-size] << controller
            @controller = @options[:controller] = parts.join("/")
          end
        end
430

431 432 433 434 435
        # 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'
436
          end
437
          recall[:action] = options.delete(:action) if options[:action] == 'index'
438
        end
439

440
        def generate
441
          path, params = @set.set.generate(:path_info, named_route, options, recall, PARAMETERIZE)
442

443
          raise_routing_error unless path
444

445
          return [path, params.keys] if @extras
446

447
          [path, params]
448
        rescue Rack::Mount::RoutingError
449
          raise_routing_error
450 451
        end

452
        def raise_routing_error
453
          raise ActionController::RoutingError, "No route matches #{options.inspect}"
454
        end
455

456 457 458
        def different_controller?
          return false unless current_controller
          controller.to_param != current_controller.to_param
459
        end
460 461 462 463 464 465 466

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

          def segment_keys
467
            set.named_routes[named_route].segment_keys
468
          end
469 470 471 472 473 474 475 476 477 478 479 480 481
      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)
482
        Generator.new(options, recall, self, extras).generate
483 484
      end

485
      RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
486
                          :trailing_slash, :anchor, :params, :only_path, :script_name]
487 488 489 490

      def _generate_prefix(options = {})
        nil
      end
491

J
Joshua Peek 已提交
492
      def url_for(options)
493
        finalize!
494
        options = (options || {}).reverse_merge!(default_url_options)
495

496 497
        handle_positional_args(options)

498 499 500
        user, password = extract_authentication(options)
        path_segments  = options.delete(:_path_segments)
        script_name    = options.delete(:script_name)
501

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

504 505 506
        path_options = options.except(*RESERVED_OPTIONS)
        path_options = yield(path_options) if block_given?

507 508
        path_addition, params = generate(path_options, path_segments || {})
        path << path_addition
509

510 511 512 513 514 515
        ActionDispatch::Http::URL.url_for(options.merge({
          :path => path,
          :params => params,
          :user => user,
          :password => password
        }))
516 517
      end

518
      def call(env)
519
        finalize!
520
        @set.call(env)
521 522
      end

523
      def recognize_path(path, environment = {})
524
        method = (environment[:method] || "GET").to_s.upcase
525
        path = Rack::Mount::Utils.normalize_path(path) unless path =~ %r{://}
526

527 528 529
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
530
          raise ActionController::RoutingError, e.message
531 532
        end

533
        req = @request_class.new(env)
J
Joshua Peek 已提交
534
        @set.recognize(req) do |route, matches, params|
535 536 537
          params.each do |key, value|
            if value.is_a?(String)
              value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware?
538
              params[key] = URI.parser.unescape(value)
539 540 541
            end
          end

542
          dispatcher = route.app
543 544 545
          while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do
            dispatcher = dispatcher.app
          end
546

J
José Valim 已提交
547
          if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
548 549 550 551 552
            dispatcher.prepare_params!(params)
            return params
          end
        end

J
Joshua Peek 已提交
553
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
554
      end
555 556

      private
557

558 559 560 561 562
        def extract_authentication(options)
          if options[:user] && options[:password]
            [options.delete(:user), options.delete(:password)]
          else
            nil
563 564 565
          end
        end

566 567 568 569 570 571 572
        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 已提交
573
          options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
574 575
        end

576 577
    end
  end
J
Joshua Peek 已提交
578
end