route_set.rb 17.7 KB
Newer Older
1
require 'rack/mount'
2
require 'forwardable'
3
require 'active_support/core_ext/object/to_query'
4
require 'action_dispatch/routing/deprecated_mapper'
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 70 71
          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)
          params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) }
        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 162 163 164
              def #{selector}(options = nil)                                      # def hash_for_users_url(options = nil)
                options ? #{options.inspect}.merge(options) : #{options.inspect}  #   options ? {:only_path=>false}.merge(options) : {:only_path=>false}
              end                                                                 # end
              protected :#{selector}                                              # protected :hash_for_users_url
J
José Valim 已提交
165
            END_EVAL
166 167 168
            helpers << selector
          end

J
José Valim 已提交
169 170 171 172 173 174 175 176 177 178 179 180 181
          # 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')
          #
182 183 184 185
          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 已提交
186
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
187
              def #{selector}(*args)
188 189 190 191 192
                options =  #{hash_access_method}(args.extract_options!)

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

                url_for(options)
              end
J
José Valim 已提交
197
            END_EVAL
198 199 200 201
            helpers << selector
          end
      end

202
      attr_accessor :set, :routes, :named_routes
203
      attr_accessor :disable_clear_and_finalize, :resources_path_names
204
      attr_accessor :default_url_options, :request_class, :valid_conditions
205

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

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

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

J
Joshua Peek 已提交
222
        @disable_clear_and_finalize = false
223
        clear!
224 225
      end

226
      def draw(&block)
J
Joshua Peek 已提交
227
        clear! unless @disable_clear_and_finalize
228 229 230 231 232 233 234 235

        mapper = Mapper.new(self)
        if block.arity == 1
          mapper.instance_exec(DeprecatedMapper.new(self), &block)
        else
          mapper.instance_exec(&block)
        end

J
Joshua Peek 已提交
236
        finalize! unless @disable_clear_and_finalize
237 238

        nil
239 240
      end

J
Joshua Peek 已提交
241
      def finalize!
242 243
        return if @finalized
        @finalized = true
J
Joshua Peek 已提交
244 245 246
        @set.freeze
      end

247
      def clear!
J
Joshua Peek 已提交
248 249
        # Clear the controller cache so we may discover new ones
        @controller_constraints = nil
250
        @finalized = false
251 252
        routes.clear
        named_routes.clear
253 254 255 256
        @set = ::Rack::Mount::RouteSet.new(
          :parameters_key => PARAMETERS_KEY,
          :request_class  => request_class
        )
257 258 259 260 261 262 263
      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

264 265
      def url_helpers
        @url_helpers ||= begin
266
          routes = self
C
Carlhuda 已提交
267

268
          helpers = Module.new do
C
Carlhuda 已提交
269
            extend ActiveSupport::Concern
270
            include UrlFor
C
Carlhuda 已提交
271

272 273 274 275 276 277
            @routes = routes
            class << self
              delegate :url_for, :to => '@routes'
            end
            extend routes.named_routes.module

C
Carlhuda 已提交
278 279
            # ROUTES TODO: install_helpers isn't great... can we make a module with the stuff that
            # we can include?
280
            # Yes plz - JP
C
Carlhuda 已提交
281
            included do
282
              routes.install_helpers(self)
283
              singleton_class.send(:define_method, :_routes) { routes }
C
Carlhuda 已提交
284
            end
285

286
            define_method(:_routes) { routes }
C
Carlhuda 已提交
287
          end
288 289

          helpers
C
Carlhuda 已提交
290 291 292
        end
      end

293 294 295 296
      def empty?
        routes.empty?
      end

297
      def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
298
        route = Route.new(self, app, conditions, requirements, defaults, name, anchor)
299
        @set.add_route(*route)
300
        named_routes[name] = route if name
301 302 303 304
        routes << route
        route
      end

305
      class Generator #:nodoc:
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
        attr_reader :options, :recall, :set, :script_name, :named_route

        def initialize(options, recall, set, extras = false)
          @script_name = options.delete(:script_name)
          @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
322

323 324
        def controller
          @controller ||= @options[:controller]
325 326
        end

327 328 329
        def current_controller
          @recall[:controller]
        end
330

331 332
        def use_recall_for(key)
          if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
333 334 335 336 337
            if named_route_exists?
              @options[key] = @recall.delete(key) if segment_keys.include?(key)
            else
              @options[key] = @recall.delete(key)
            end
338 339
          end
        end
340

341 342 343 344 345 346 347 348 349
        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".
350

351 352 353 354
          if options[:controller]
            options[:action]     ||= 'index'
            options[:controller]   = options[:controller].to_s
          end
355

356 357 358 359
          if options[:action]
            options[:action] = options[:action].to_s
          end
        end
360

361 362 363 364 365 366 367 368 369 370 371 372
        # 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
373

374 375 376 377 378 379 380 381 382 383
        # 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
384

385 386 387 388 389
        # 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'
390
          end
391
          recall[:action] = options.delete(:action) if options[:action] == 'index'
392
        end
393

394
        def generate
395
          path, params = @set.set.generate(:path_info, named_route, options, recall, opts)
396

397
          raise_routing_error unless path
398

399
          params.reject! {|k,v| !v }
400

401
          return [path, params.keys] if @extras
402

403 404 405
          path << "?#{params.to_query}" if params.any?
          "#{script_name}#{path}"
        rescue Rack::Mount::RoutingError
406
          raise_routing_error
407 408
        end

409 410 411 412 413 414 415
        def opts
          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
416 417
              return nil unless param = value.to_param
              param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/")
418
            end
419
          end
420
          {:parameterize => parameterize}
421
        end
422 423 424 425

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

427 428 429
        def different_controller?
          return false unless current_controller
          controller.to_param != current_controller.to_param
430
        end
431 432 433 434 435 436 437

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

          def segment_keys
438
            set.named_routes[named_route].segment_keys
439
          end
440 441 442 443 444 445 446 447 448 449 450 451 452
      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)
453
        Generator.new(options, recall, self, extras).generate
454 455
      end

456
      RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash]
457

J
Joshua Peek 已提交
458
      def url_for(options)
459
        finalize!
460
        options = (options || {}).reverse_merge!(default_url_options)
461

462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
        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)

          raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host]

          rewritten_url << options[:host]
          rewritten_url << ":#{options.delete(:port)}" if options.key?(:port)
        end

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

483
        # ROUTES TODO: This can be called directly, so script_name should probably be set in the routes
484
        rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
485
        rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor]
486 487 488 489

        rewritten_url
      end

490
      def call(env)
491
        finalize!
492
        @set.call(env)
493 494
      end

495
      def recognize_path(path, environment = {})
496
        method = (environment[:method] || "GET").to_s.upcase
497
        path = Rack::Mount::Utils.normalize_path(path)
498

499 500 501
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
502
          raise ActionController::RoutingError, e.message
503 504
        end

505
        req = Rack::Request.new(env)
J
Joshua Peek 已提交
506
        @set.recognize(req) do |route, matches, params|
507 508 509 510 511 512 513
          params.each do |key, value|
            if value.is_a?(String)
              value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware?
              params[key] = URI.unescape(value)
            end
          end

514
          dispatcher = route.app
515 516
          dispatcher = dispatcher.app while dispatcher.is_a?(Mapper::Constraints)

J
José Valim 已提交
517
          if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
518 519 520 521 522
            dispatcher.prepare_params!(params)
            return params
          end
        end

J
Joshua Peek 已提交
523
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
524
      end
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548

      private
        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

          args = args.zip(keys).inject({}) do |h, (v, k)|
            h[k] = v
            h
          end

          # Tell url_for to skip default_url_options
          options.merge!(args)
        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
549 550
    end
  end
J
Joshua Peek 已提交
551
end