route_set.rb 17.3 KB
Newer Older
1
require 'forwardable'
2
require 'active_support/core_ext/object/to_query'
3
require 'action_dispatch/routing/deprecated_mapper'
4

5 6 7
$: << File.expand_path('../../vendor/rack-mount-0.6.6.pre', __FILE__)
require 'rack/mount'

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

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

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

          # 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

          controller.action(params[:action]).call(env)
30 31 32
        end

        def prepare_params!(params)
33 34
          merge_default_action!(params)
          split_glob_param!(params) if @glob_param
35
        end
36

J
José Valim 已提交
37
        def controller(params, raise_error=true)
W
wycats 已提交
38 39 40 41 42 43 44 45 46
          if params && params.key?(:controller)
            controller_param = params[:controller]
            unless controller = @controllers[controller_param]
              controller_name = "#{controller_param.camelize}Controller"
              controller = @controllers[controller_param] =
                ActiveSupport::Dependencies.ref(controller_name)
            end

            controller.get
47
          end
48
        rescue NameError => e
J
José Valim 已提交
49
          raise ActionController::RoutingError, e.message, e.backtrace if raise_error
50 51 52 53 54 55 56 57 58 59 60 61
        end

        private
          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
      end

62 63 64 65 66
      # 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
67
        attr_reader :routes, :helpers, :module
68 69 70 71 72

        def initialize
          clear!
        end

73 74 75 76
        def helper_names
          self.module.instance_methods.map(&:to_s)
        end

77 78 79 80
        def clear!
          @routes = {}
          @helpers = []

81 82
          @module ||= Module.new do
            instance_methods.each { |selector| remove_method(selector) }
83 84 85 86 87 88 89 90 91 92 93 94 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
          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|
123
            dest.__send__(:include, @module)
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
          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 已提交
146 147 148

            # We use module_eval to avoid leaks
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
149 150 151 152
              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 已提交
153
            END_EVAL
154 155 156
            helpers << selector
          end

J
José Valim 已提交
157 158 159 160 161 162 163 164 165 166 167 168 169
          # 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')
          #
170 171 172 173
          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 已提交
174
            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
175
              def #{selector}(*args)
176 177 178 179 180
                options =  #{hash_access_method}(args.extract_options!)

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

                url_for(options)
              end
J
José Valim 已提交
185
            END_EVAL
186 187 188 189
            helpers << selector
          end
      end

190
      attr_accessor :set, :routes, :named_routes
191
      attr_accessor :disable_clear_and_finalize, :resources_path_names
192
      attr_accessor :default_url_options, :request_class, :valid_conditions
193

194 195 196 197
      def self.default_resources_path_names
        { :new => 'new', :edit => 'edit' }
      end

198
      def initialize(request_class = ActionDispatch::Request)
199 200
        self.routes = []
        self.named_routes = NamedRouteCollection.new
201 202
        self.resources_path_names = self.class.default_resources_path_names.dup
        self.controller_namespaces = Set.new
203
        self.default_url_options = {}
204
        self.request_class = request_class
205
        self.valid_conditions = request_class.public_instance_methods.select{ |m| m != "id" }.map{ |m| m.to_sym }
206
        self.valid_conditions += [:controller, :action]
207

J
Joshua Peek 已提交
208
        @disable_clear_and_finalize = false
209
        clear!
210 211
      end

212
      def draw(&block)
J
Joshua Peek 已提交
213
        clear! unless @disable_clear_and_finalize
214 215 216 217 218 219 220 221

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

J
Joshua Peek 已提交
222
        finalize! unless @disable_clear_and_finalize
223 224

        nil
225 226
      end

J
Joshua Peek 已提交
227
      def finalize!
228 229
        return if @finalized
        @finalized = true
J
Joshua Peek 已提交
230 231 232
        @set.freeze
      end

233
      def clear!
J
Joshua Peek 已提交
234 235
        # Clear the controller cache so we may discover new ones
        @controller_constraints = nil
236
        @finalized = false
237 238
        routes.clear
        named_routes.clear
239 240 241 242
        @set = ::Rack::Mount::RouteSet.new(
          :parameters_key => PARAMETERS_KEY,
          :request_class  => request_class
        )
243 244 245 246 247 248 249
      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

250 251
      def url_helpers
        @url_helpers ||= begin
252
          routes = self
C
Carlhuda 已提交
253

254
          helpers = Module.new do
C
Carlhuda 已提交
255
            extend ActiveSupport::Concern
256
            include UrlFor
C
Carlhuda 已提交
257

258 259 260 261 262 263
            @routes = routes
            class << self
              delegate :url_for, :to => '@routes'
            end
            extend routes.named_routes.module

C
Carlhuda 已提交
264 265
            # ROUTES TODO: install_helpers isn't great... can we make a module with the stuff that
            # we can include?
266
            # Yes plz - JP
C
Carlhuda 已提交
267
            included do
268
              routes.install_helpers(self)
269
              singleton_class.send(:define_method, :_router) { routes }
C
Carlhuda 已提交
270
            end
271

272
            define_method(:_router) { routes }
C
Carlhuda 已提交
273
          end
274 275

          helpers
C
Carlhuda 已提交
276 277 278
        end
      end

279 280 281 282
      def empty?
        routes.empty?
      end

283
      def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
284
        route = Route.new(self, app, conditions, requirements, defaults, name, anchor)
285
        @set.add_route(*route)
286
        named_routes[name] = route if name
287 288 289 290
        routes << route
        route
      end

291
      class Generator #:nodoc:
292 293 294 295 296 297 298 299 300 301 302
        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!
303
          normalize_recall!
304 305 306 307 308
          normalize_controller_action_id!
          use_relative_controller!
          controller.sub!(%r{^/}, '') if controller
          handle_nil_action!
        end
309

310 311
        def controller
          @controller ||= @options[:controller]
312 313
        end

314 315 316
        def current_controller
          @recall[:controller]
        end
317

318 319 320 321 322
        def use_recall_for(key)
          if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
            @options[key] = @recall.delete(key)
          end
        end
323

324 325 326 327 328 329 330 331 332
        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".
333

334 335 336 337
          if options[:controller]
            options[:action]     ||= 'index'
            options[:controller]   = options[:controller].to_s
          end
338

339 340 341 342
          if options[:action]
            options[:action] = options[:action].to_s
          end
        end
343

344 345 346 347 348 349 350 351 352
        def normalize_recall!
          # If the target route is not a standard route then remove controller and action
          # from the options otherwise they will appear in the url parameters
          if block_or_proc_route_target?
            recall.delete(:controller) unless segment_keys.include?(:controller)
            recall.delete(:action) unless segment_keys.include?(:action)
          end
        end

353 354 355 356 357 358 359 360 361 362 363 364
        # 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
365

366 367 368 369 370 371 372 373 374 375
        # 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
376

377 378 379 380 381
        # 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'
382
          end
383
          recall[:action] = options.delete(:action) if options[:action] == 'index'
384
        end
385

386 387
        def generate
          error = ActionController::RoutingError.new("No route matches #{options.inspect}")
388
          path, params = @set.set.generate(:path_info, named_route, options, recall, opts)
389

390
          raise error unless path
391

392
          params.reject! {|k,v| !v }
393

394
          return [path, params.keys] if @extras
395

396 397 398 399
          path << "?#{params.to_query}" if params.any?
          "#{script_name}#{path}"
        rescue Rack::Mount::RoutingError
          raise error
400 401
        end

402 403 404 405 406 407 408 409 410
        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
              Rack::Mount::Utils.escape_uri(value.to_param)
            end
411
          end
412
          {:parameterize => parameterize}
413 414
        end

415 416 417
        def different_controller?
          return false unless current_controller
          controller.to_param != current_controller.to_param
418
        end
419 420 421 422 423 424 425 426 427 428 429 430 431

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

          def block_or_proc_route_target?
            named_route_exists? && !set.named_routes[named_route].app.is_a?(Dispatcher)
          end

          def segment_keys
            named_route_exists? ? set.named_routes[named_route].segment_keys : []
          end
432 433 434 435 436 437 438 439 440 441 442 443 444
      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)
445
        Generator.new(options, recall, self, extras).generate
446 447
      end

448
      RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash]
449

J
Joshua Peek 已提交
450
      def url_for(options)
451
        finalize!
452 453
        options = default_url_options.merge(options || {})

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
        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 || {})

        # ROUTES TODO: This can be called directly, so script_name should probably be set in the router
        rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
477
        rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor]
478 479 480 481

        rewritten_url
      end

482
      def call(env)
483
        finalize!
484
        @set.call(env)
485 486
      end

487
      def recognize_path(path, environment = {})
488
        method = (environment[:method] || "GET").to_s.upcase
489
        path = Rack::Mount::Utils.normalize_path(path)
490

491 492 493
        begin
          env = Rack::MockRequest.env_for(path, {:method => method})
        rescue URI::InvalidURIError => e
J
Joshua Peek 已提交
494
          raise ActionController::RoutingError, e.message
495 496
        end

497
        req = Rack::Request.new(env)
J
Joshua Peek 已提交
498
        @set.recognize(req) do |route, matches, params|
499 500 501 502 503 504 505
          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

506
          dispatcher = route.app
J
José Valim 已提交
507
          if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
508 509 510 511 512
            dispatcher.prepare_params!(params)
            return params
          end
        end

J
Joshua Peek 已提交
513
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
514
      end
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538

      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
539 540
    end
  end
J
Joshua Peek 已提交
541
end