route_set.rb 19.1 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
          params.reject! {|k,v| !v }
446

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

449
          path << "?#{params.to_query}" unless params.empty?
450
          path
451
        rescue Rack::Mount::RoutingError
452
          raise_routing_error
453 454
        end

455 456 457
        def raise_routing_error
          raise ActionController::RoutingError.new("No route matches #{options.inspect}")
        end
458

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

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

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

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

      def _generate_prefix(options = {})
        nil
      end
494

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

499 500 501 502 503 504 505 506 507
        handle_positional_args(options)

        rewritten_url = ""

        path_segments = options.delete(:_path_segments)
        unless options[:only_path]
          rewritten_url << (options[:protocol] || "http")
          rewritten_url << "://" unless rewritten_url.match("://")
          rewritten_url << rewrite_authentication(options)
508 509
          rewritten_url << host_from_options(options)
          rewritten_url << ":#{options.delete(:port)}" if options[:port]
510 511
        end

512
        script_name = options.delete(:script_name)
P
Piotr Sarnacki 已提交
513
        path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s
514

515 516
        path_options = options.except(*RESERVED_OPTIONS)
        path_options = yield(path_options) if block_given?
517
        path << generate(path_options, path_segments || {})
518

519
        # ROUTES TODO: This can be called directly, so script_name should probably be set in the routes
520
        rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
521
        rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor]
522 523 524 525

        rewritten_url
      end

526
      def call(env)
527
        finalize!
528
        @set.call(env)
529 530
      end

531
      def recognize_path(path, environment = {})
532
        method = (environment[:method] || "GET").to_s.upcase
533
        path = Rack::Mount::Utils.normalize_path(path) unless path =~ %r{://}
534

535 536 537
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
538
          raise ActionController::RoutingError, e.message
539 540
        end

541
        req = @request_class.new(env)
J
Joshua Peek 已提交
542
        @set.recognize(req) do |route, matches, params|
543 544 545
          params.each do |key, value|
            if value.is_a?(String)
              value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware?
546
              params[key] = URI.parser.unescape(value)
547 548 549
            end
          end

550
          dispatcher = route.app
551 552
          dispatcher = dispatcher.app while dispatcher.is_a?(Mapper::Constraints)

J
José Valim 已提交
553
          if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
554 555 556 557 558
            dispatcher.prepare_params!(params)
            return params
          end
        end

J
Joshua Peek 已提交
559
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
560
      end
561 562

      private
563 564 565 566 567 568 569 570 571 572

        def host_from_options(options)
          computed_host = subdomain_and_domain(options) || options[:host]
          unless computed_host
            raise ArgumentError, "Missing host to link to! Please provide :host parameter or set default_url_options[:host]"
          end
          computed_host
        end

        def subdomain_and_domain(options)
573
          return nil unless options[:subdomain] || options[:domain]
574 575
          tld_length = options[:tld_length] || ActionDispatch::Http::URL.tld_length

576 577 578 579 580
          host = ""
          host << (options[:subdomain] || ActionDispatch::Http::URL.extract_subdomain(options[:host], tld_length))
          host << "."
          host << (options[:domain] || ActionDispatch::Http::URL.extract_domain(options[:host], tld_length))
          host
581 582
        end

583 584 585 586 587 588 589
        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 已提交
590
          options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
591 592 593 594 595 596 597 598 599
        end

        def rewrite_authentication(options)
          if options[:user] && options[:password]
            "#{Rack::Utils.escape(options.delete(:user))}:#{Rack::Utils.escape(options.delete(:password))}@"
          else
            ""
          end
        end
600 601
    end
  end
J
Joshua Peek 已提交
602
end