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

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

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

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

          # 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

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

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

34 35 36 37
        # 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
38
        # delegate the control back to Rack cascade. Besides, if this is not a default
39 40
        # controller, it means we should respect the @scope[:module] parameter.
        def controller(params, default_controller=true)
W
wycats 已提交
41
          if params && params.key?(:controller)
42
            controller_param = params[:controller]
43
            controller_reference(controller_param)
44
          end
45
        rescue NameError => e
46
          raise ActionController::RoutingError, e.message, e.backtrace if default_controller
47 48
        end

49
      private
50

51 52 53 54 55
        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)
56
          end
57 58 59 60 61 62 63 64 65 66 67 68
          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)
69
          params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
70
        end
71 72
      end

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

        def initialize
          clear!
        end

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

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

92 93
          @module ||= Module.new do
            instance_methods.each { |selector| remove_method(selector) }
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 123 124 125 126 127 128 129 130 131 132 133
          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|
134
            dest.__send__(:include, @module)
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
          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 已提交
157 158 159

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

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

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

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

211
      def initialize(request_class = ActionDispatch::Request)
212 213
        self.routes = []
        self.named_routes = NamedRouteCollection.new
214
        self.resources_path_names = self.class.default_resources_path_names.dup
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

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

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

        nil
      end

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

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

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

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

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

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

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

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

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

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

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

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

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

          helpers
C
Carlhuda 已提交
323 324 325
        end
      end

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

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

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

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

        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
367

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

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

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

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

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

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

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

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

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

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

442
          raise_routing_error unless path
443

444
          params.reject! {|k,v| !v }
445

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

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

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

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

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

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

P
Piotr Sarnacki 已提交
487
      RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :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 499 500 501 502 503 504 505 506 507 508 509 510 511 512
        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

513 514
        script_name = options.delete(:script_name)
        path = (script_name.blank? ? _generate_prefix(options) : script_name).to_s
515

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

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

        rewritten_url
      end

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

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

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

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

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

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

J
Joshua Peek 已提交
560
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
561
      end
562 563 564 565 566 567 568 569 570

      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

          # Tell url_for to skip default_url_options
E
Emilio Tagua 已提交
571
          options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
572 573 574 575 576 577 578 579 580
        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
581 582
    end
  end
J
Joshua Peek 已提交
583
end