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
        def controller_reference(controller_param)
53 54
          controller_name = "#{controller_param.camelize}Controller"

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

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

        def initialize
          clear!
        end

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

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

94 95
          @module ||= Module.new do
            instance_methods.each { |selector| remove_method(selector) }
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 135
          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|
136
            dest.__send__(:include, @module)
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
          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 已提交
159 160 161

            # We use module_eval to avoid leaks
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
162
              remove_method :#{selector} if method_defined?(:#{selector})
163 164 165 166 167 168 169 170 171 172 173
              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 已提交
174
            END_EVAL
175 176 177
            helpers << selector
          end

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

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

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

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

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

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

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

        nil
      end

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

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

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

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

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

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

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

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

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

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

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

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

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

          helpers
C
Carlhuda 已提交
325 326 327
        end
      end

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

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

340
      class Generator #:nodoc:
341 342 343 344 345 346 347 348 349 350 351 352 353
        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
        }

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

        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
369

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

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

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

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

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

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

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

421 422 423 424 425 426 427 428 429 430
        # 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
431

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

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

444
          raise_routing_error unless path
445

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

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

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

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

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

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

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

      def _generate_prefix(options = {})
        nil
      end
492

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

497 498
        handle_positional_args(options)

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

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

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

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

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

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

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

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

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

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

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

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

      private
558

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

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

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