mapper.rb 43.9 KB
Newer Older
1
require 'erb'
2
require 'active_support/core_ext/hash/except'
3
require 'active_support/core_ext/object/blank'
4
require 'active_support/inflector'
5

J
Joshua Peek 已提交
6 7
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
8
    class Mapper
9
      class Constraints #:nodoc:
10
        def self.new(app, constraints, request = Rack::Request)
11
          if constraints.any?
12
            super(app, constraints, request)
13 14 15 16 17
          else
            app
          end
        end

18 19
        attr_reader :app

20 21
        def initialize(app, constraints, request)
          @app, @constraints, @request = app, constraints, request
22 23 24
        end

        def call(env)
25
          req = @request.new(env)
26 27 28

          @constraints.each { |constraint|
            if constraint.respond_to?(:matches?) && !constraint.matches?(req)
J
Joshua Peek 已提交
29
              return [ 404, {'X-Cascade' => 'pass'}, [] ]
30
            elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req))
J
Joshua Peek 已提交
31
              return [ 404, {'X-Cascade' => 'pass'}, [] ]
32 33 34 35 36
            end
          }

          @app.call(env)
        end
37 38 39 40 41

        private
          def constraint_args(constraint, request)
            constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
          end
42 43
      end

44
      class Mapping #:nodoc:
45
        IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
46

47
        def initialize(set, scope, path, options)
48 49
          @set, @scope = set, scope
          @options = (@scope[:options] || {}).merge(options)
50
          @path = normalize_path(path)
51
          normalize_options!
52
        end
J
Joshua Peek 已提交
53

54
        def to_route
55
          [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
56
        end
J
Joshua Peek 已提交
57

58
        private
59 60 61

          def normalize_options!
            path_without_format = @path.sub(/\(\.:format\)$/, '')
62

63 64 65
            if using_match_shorthand?(path_without_format, @options)
              to_shorthand    = @options[:to].blank?
              @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1')
66 67
            end

68
            @options.merge!(default_controller_and_action(to_shorthand))
69 70 71 72 73 74 75 76 77 78 79 80

            requirements.each do |name, requirement|
              # segment_keys.include?(k.to_s) || k == :controller
              next unless Regexp === requirement && !constraints[name]

              if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
                raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
              end
              if requirement.multiline?
                raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
              end
            end
81
          end
82

83
          # match "account/overview"
84
          def using_match_shorthand?(path, options)
85
            path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w\/]+$}
86
          end
87

88
          def normalize_path(path)
89 90
            raise ArgumentError, "path is required" if path.blank?
            path = Mapper.normalize_path(path)
91 92 93 94 95 96 97 98 99 100 101

            if path.match(':controller')
              raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]

              # Add a default constraint for :controller path segments that matches namespaced
              # controllers with default routes like :controller/:action/:id(.:format), e.g:
              # GET /admin/products/show/1
              # => { :controller => 'admin/products', :action => 'show', :id => '1' }
              @options.reverse_merge!(:controller => /.+?/)
            end

102 103 104 105
            if @options[:format] == false
              @options.delete(:format)
              path
            elsif path.include?(":format")
106 107 108 109
              path
            else
              "#{path}(.:format)"
            end
110
          end
111

112 113
          def app
            Constraints.new(
114
              to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
115 116
              blocks,
              @set.request_class
117
            )
118 119
          end

120 121 122
          def conditions
            { :path_info => @path }.merge(constraints).merge(request_method_condition)
          end
J
Joshua Peek 已提交
123

124
          def requirements
125
            @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
126 127 128 129
              requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
              @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
            end
          end
130

131
          def defaults
132 133 134 135 136 137
            @defaults ||= (@options[:defaults] || {}).tap do |defaults|
              defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults]
              @options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) }
            end
          end

138
          def default_controller_and_action(to_shorthand=nil)
139
            if to.respond_to?(:call)
140 141
              { }
            else
142
              if to.is_a?(String)
143
                controller, action = to.split('#')
144 145
              elsif to.is_a?(Symbol)
                action = to.to_s
146
              end
J
Joshua Peek 已提交
147

148 149
              controller ||= default_controller
              action     ||= default_action
150

151 152 153
              unless controller.is_a?(Regexp) || to_shorthand
                controller = [@scope[:module], controller].compact.join("/").presence
              end
154

155 156 157 158
              if controller.is_a?(String) && controller =~ %r{\A/}
                raise ArgumentError, "controller name should not start with a slash"
              end

159 160
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
161

162
              if controller.blank? && segment_keys.exclude?("controller")
163 164
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
165

166
              if action.blank? && segment_keys.exclude?("action")
167 168
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
169

A
Aaron Patterson 已提交
170
              hash = {}
A
Aaron Patterson 已提交
171 172
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
173
              hash
174 175
            end
          end
176

177
          def blocks
A
Aaron Patterson 已提交
178 179
            block = @scope[:blocks] || []

180
            if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
A
Aaron Patterson 已提交
181
              block << @options[:constraints]
182
            end
J
Joshua Peek 已提交
183

A
Aaron Patterson 已提交
184
            block
185
          end
J
Joshua Peek 已提交
186

187 188 189
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
190

191 192
          def request_method_condition
            if via = @options[:via]
193 194
              via = Array(via).map { |m| m.to_s.dasherize.upcase }
              { :request_method => %r[^#{via.join('|')}$] }
195 196
            else
              { }
197
            end
198
          end
J
Joshua Peek 已提交
199

200 201
          def segment_keys
            @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
202 203
              Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
            ).names
204
          end
205

206 207 208
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
209

210
          def default_controller
211
            if @options[:controller]
212
              @options[:controller]
213
            elsif @scope[:controller]
214
              @scope[:controller]
215
            end
216
          end
217 218 219

          def default_action
            if @options[:action]
220
              @options[:action]
221 222
            elsif @scope[:action]
              @scope[:action]
223 224
            end
          end
225
      end
226

227
      # Invokes Rack::Mount::Utils.normalize path and ensure that
228 229
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
230 231
      def self.normalize_path(path)
        path = Rack::Mount::Utils.normalize_path(path)
232
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$}
233 234 235
        path
      end

236 237 238 239
      def self.normalize_name(name)
        normalize_path(name)[1..-1].gsub("/", "_")
      end

240
      module Base
241
        def initialize(set) #:nodoc:
242 243
          @set = set
        end
244

245 246 247 248 249
        # You can specify what Rails should route "/" to with the root method:
        #
        #   root :to => 'pages#main'
        #
        # You should put the root route at the end of <tt>config/routes.rb</tt>.
250 251 252
        def root(options = {})
          match '/', options.reverse_merge(:as => :root)
        end
253

254 255 256 257 258 259 260 261 262
        # When you set up a regular route, you supply a series of symbols that
        # Rails maps to parts of an incoming HTTP request.
        #
        #   match ':controller/:action/:id/:user_id'
        #
        # Two of these symbols are special: :controller maps to the name of a
        # controller in your application, and :action maps to the name of an
        # action within that controller. Anything other than :controller or
        # :action will be available to the action as part of params.
263 264
        def match(path, options=nil)
          mapping = Mapping.new(@set, @scope, path, options || {}).to_route
265
          @set.add_route(*mapping)
266 267
          self
        end
268

269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        # Mount a Rack-based application to be used within the application.
        #
        # mount SomeRackApp, :at => "some_route"
        #
        # Alternatively:
        #
        # mount(SomeRackApp => "some_route")
        #
        # All mounted applications come with routing helpers to access them.
        # These are named after the class specified, so for the above example
        # the helper is either +some_rack_app_path+ or +some_rack_app_url+.
        # To customize this helper's name, use the +:as+ option:
        #
        # mount(SomeRackApp => "some_route", :as => "exciting")
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
286 287 288 289 290 291 292 293 294 295 296
        def mount(app, options = nil)
          if options
            path = options.delete(:at)
          else
            options = app
            app, path = options.find { |k, v| k.respond_to?(:call) }
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

297 298
          options[:as] ||= app_name(app)

299
          match(path, options.merge(:to => app, :anchor => false, :format => false))
300 301

          define_generate_prefix(app, options[:as])
302 303 304
          self
        end

305 306 307 308
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
309

310 311 312 313 314 315
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

316 317 318
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
319 320 321 322 323 324 325

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
              ActiveSupport::Inflector.underscore(class_name).gsub("/", "_")
            end
326 327 328 329 330 331
          end

          def define_generate_prefix(app, name)
            return unless app.respond_to?(:routes)

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
332 333
            _routes = @set
            app.routes.define_mounted_helper(name)
334 335
            app.routes.class_eval do
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
336
                prefix_options = options.slice(*_route.segment_keys)
337 338
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
P
Piotr Sarnacki 已提交
339
                _routes.url_helpers.send("#{name}_path", prefix_options)
340 341 342
              end
            end
          end
343 344 345
      end

      module HttpHelpers
346
        # Define a route that only recognizes HTTP GET.
347 348 349 350 351
        # For supported arguments, see +match+.
        #
        # Example:
        #
        # get 'bacon', :to => 'food#bacon'
352 353 354 355
        def get(*args, &block)
          map_method(:get, *args, &block)
        end

356
        # Define a route that only recognizes HTTP POST.
357 358 359 360 361
        # For supported arguments, see +match+.
        #
        # Example:
        #
        # post 'bacon', :to => 'food#bacon'
362 363 364 365
        def post(*args, &block)
          map_method(:post, *args, &block)
        end

366
        # Define a route that only recognizes HTTP PUT.
367 368 369 370 371
        # For supported arguments, see +match+.
        #
        # Example:
        #
        # put 'bacon', :to => 'food#bacon'
372 373 374 375
        def put(*args, &block)
          map_method(:put, *args, &block)
        end

376 377 378 379 380 381
        # Define a route that only recognizes HTTP PUT.
        # For supported arguments, see +match+.
        #
        # Example:
        #
        # delete 'broccoli', :to => 'food#broccoli'
382 383 384 385
        def delete(*args, &block)
          map_method(:delete, *args, &block)
        end

386 387 388
        # Redirect any path to another path:
        #
        #   match "/stories" => redirect("/posts")
389
        def redirect(*args)
390 391
          options = args.last.is_a?(Hash) ? args.pop : {}

392
          path      = args.shift || Proc.new
A
Aaron Patterson 已提交
393
          path_proc = path.is_a?(Proc) ? path : proc { |params| (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) }
394
          status    = options[:status] || 301
395 396

          lambda do |env|
397
            req = Request.new(env)
398 399 400 401 402

            params = [req.symbolized_path_parameters]
            params << req if path_proc.arity > 1

            uri = URI.parse(path_proc.call(*params))
403 404
            uri.scheme ||= req.scheme
            uri.host   ||= req.host
405
            uri.port   ||= req.port unless req.standard_port?
406

407 408
            body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)

409 410 411 412 413
            headers = {
              'Location' => uri.to_s,
              'Content-Type' => 'text/html',
              'Content-Length' => body.length.to_s
            }
414

415
            [ status, headers, [body] ]
416
          end
417 418 419 420 421 422 423 424 425 426 427 428
        end

        private
          def map_method(method, *args, &block)
            options = args.extract_options!
            options[:via] = method
            args.push(options)
            match(*args, &block)
            self
          end
      end

429 430 431 432 433 434 435 436 437
      # You may wish to organize groups of controllers under a namespace.
      # Most commonly, you might group a number of administrative controllers
      # under an +admin+ namespace. You would place these controllers under
      # the app/controllers/admin directory, and you can group them together
      # in your router:
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
438
      #
439 440
      # This will create a number of routes for each of the posts and comments
      # controller. For Admin::PostsController, Rails will create:
441
      #
442 443 444 445 446 447 448
      #   GET	    /admin/posts
      #   GET	    /admin/posts/new
      #   POST	  /admin/posts
      #   GET	    /admin/posts/1
      #   GET	    /admin/posts/1/edit
      #   PUT	    /admin/posts/1
      #   DELETE  /admin/posts/1
449
      #
450
      # If you want to route /posts (without the prefix /admin) to
451
      # Admin::PostsController, you could use
452
      #
453
      #   scope :module => "admin" do
454
      #     resources :posts
455 456 457
      #   end
      #
      # or, for a single case
458
      #
459
      #   resources :posts, :module => "admin"
460
      #
461
      # If you want to route /admin/posts to PostsController
462
      # (without the Admin:: module prefix), you could use
463
      #
464
      #   scope "/admin" do
465
      #     resources :posts
466 467 468
      #   end
      #
      # or, for a single case
469
      #
470 471 472 473 474
      #   resources :posts, :path => "/admin"
      #
      # In each of these cases, the named routes remain the same as if you did
      # not use scope. In the last case, the following paths map to
      # PostsController:
475
      #
476 477 478 479 480 481 482
      #   GET	    /admin/posts
      #   GET	    /admin/posts/new
      #   POST	  /admin/posts
      #   GET	    /admin/posts/1
      #   GET	    /admin/posts/1/edit
      #   PUT	    /admin/posts/1
      #   DELETE  /admin/posts/1
483
      module Scoping
484
        def initialize(*args) #:nodoc:
485 486 487 488
          @scope = {}
          super
        end

489 490 491 492
        # === Supported options
        # [:module]
        #   If you want to route /posts (without the prefix /admin) to
        #   Admin::PostsController, you could use
493
        #
494 495 496
        #     scope :module => "admin" do
        #       resources :posts
        #     end
497
        #
498
        # [:path]
499
        #   If you want to prefix the route, you could use
500
        #
501 502 503
        #     scope :path => "/admin" do
        #       resources :posts
        #     end
504
        #
R
Ryan Bigg 已提交
505
        #   This will prefix all of the +posts+ resource's requests with '/admin'
506 507 508 509
        #
        # [:as]
        #  Prefixes the routing helpers in this scope with the specified label.
        #
R
Ryan Bigg 已提交
510 511 512
        #    scope :as => "sekret" do
        #      resources :posts
        #    end
513 514
        #
        # Helpers such as +posts_path+ will now be +sekret_posts_path+
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
        #
        # [:shallow_path]
        #
        #   Prefixes nested shallow routes with the specified path.
        #
        #   scope :shallow_path => "sekret" do
        #     resources :posts do
        #       resources :comments, :shallow => true
        #     end
        #
        #   The +comments+ resource here will have the following routes generated for it:
        #
        #     post_comments    GET    /sekret/posts/:post_id/comments(.:format)
        #     post_comments    POST   /sekret/posts/:post_id/comments(.:format)
        #     new_post_comment GET    /sekret/posts/:post_id/comments/new(.:format)
        #     edit_comment     GET    /sekret/comments/:id/edit(.:format)
        #     comment          GET    /sekret/comments/:id(.:format)
        #     comment          PUT    /sekret/comments/:id(.:format)
        #     comment          DELETE /sekret/comments/:id(.:format)
534 535
        def scope(*args)
          options = args.extract_options!
536
          options = options.dup
537

538
          options[:path] = args.first if args.first.is_a?(String)
539
          recover = {}
540

541 542 543
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
            block, options[:constraints] = options[:constraints], {}
544
          end
545

546 547 548 549 550
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
551 552
          end

553 554
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
555

556 557
          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)
558 559 560 561

          yield
          self
        ensure
562 563 564 565 566 567
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
568 569
        end

570 571 572 573 574 575
        # Scopes routes to a specific controller
        #
        # Example:
        #   controller "food" do
        #     match "bacon", :action => "bacon"
        #   end
576 577 578
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
579 580
        end

581 582 583 584 585 586 587 588
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
589 590 591 592 593 594 595
        #      admin_posts GET    /admin/posts(.:format)          {:action=>"index", :controller=>"admin/posts"}
        #      admin_posts POST   /admin/posts(.:format)          {:action=>"create", :controller=>"admin/posts"}
        #   new_admin_post GET    /admin/posts/new(.:format)      {:action=>"new", :controller=>"admin/posts"}
        #  edit_admin_post GET    /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"}
        #       admin_post GET    /admin/posts/:id(.:format)      {:action=>"show", :controller=>"admin/posts"}
        #       admin_post PUT    /admin/posts/:id(.:format)      {:action=>"update", :controller=>"admin/posts"}
        #       admin_post DELETE /admin/posts/:id(.:format)      {:action=>"destroy", :controller=>"admin/posts"}
596 597 598 599 600 601 602 603 604 605 606
        # === Supported options
        #
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ all default to the name of the namespace.
        #
        # [:path]
        #   The path prefix for the routes.
        #
        #   namespace :admin, :path => "sekret" do
        #     resources :posts
        #   end
        #
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621
        #   All routes for the above +resources+ will be accessible through +/sekret/posts+, rather than +/admin/posts+
        #
        # [:module]
        #   The namespace for the controllers.
        #
        #   namespace :admin, :module => "sekret" do
        #     resources :posts
        #   end
        #
        #   The +PostsController+ here should go in the +Sekret+ namespace and so it should be defined like this:
        #
        #   class Sekret::PostsController < ApplicationController
        #     # code go here
        #   end
        #
622
        # [:as]
623
        #   Changes the name used in routing helpers for this namespace.
624
        #
625 626 627
        #     namespace :admin, :as => "sekret" do
        #       resources :posts
        #     end
628 629
        #
        # Routing helpers such as +admin_posts_path+ will now be +sekret_posts_path+.
630 631 632
        #
        # [:shallow_path]
        #   See the +scope+ method.
633
        def namespace(path, options = {})
634
          path = path.to_s
635 636 637
          options = { :path => path, :as => path, :module => path,
                      :shallow_path => path, :shallow_prefix => path }.merge!(options)
          scope(options) { yield }
638
        end
R
Ryan Bigg 已提交
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
        
        # === Parameter Restriction
        # Allows you to constrain the nested routes based on a set of rules.
        # For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
        #
        #   constraints(:id => /\d+\.\d+) do
        #     resources :posts
        #   end
        #
        # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
        # The +id+ parameter must match the constraint passed in for this example.
        # 
        # You may use this to also resrict other parameters:
        #
        #   resources :posts do
        #     constraints(:post_id => /\d+\.\d+) do
        #       resources :comments
        #     end
        #
        # === Restricting based on IP
        #
        # Routes can also be constrained to an IP or a certain range of IP addresses:
        #
        #   constraints(:ip => /192.168.\d+.\d+/) do
        #     resources :posts
        #   end
        #
        # Any user connecting from the 192.168.* range will be able to see this resource,
        # where as any user connecting outside of this range will be told there is no such route.
        #
        # === Dynamic request matching
        #
        # Requests to routes can be constrained based on specific critera:
        #
        #    constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
        #      resources :iphones
        #    end
        #
        # You are able to move this logic out into a class if it is too complex for routes.
        # This class must have a +matches?+ method defined on it which either returns +true+
        # if the user should be given access to that route, or +false+ if the user should not.
        #
        #    class Iphone
        #      def self.matches(request)
        #        request.env["HTTP_USER_AGENT"] =~ /iPhone/
        #      end
        #    end
        #
        # An expected place for this code would be +lib/constraints+.
        #
        # This class is then used like this:
        #
        #    constraints(Iphone) do
        #      resources :iphones
        #    end
694 695 696 697
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
698 699 700 701 702
        # Allows you to set default parameters for a route, such as this:
        # defaults :id => 'home' do
        #   match 'scoped_pages/(:id)', :to => 'pages#show'
        # end
        # Using this, the +:id+ parameter here will default to 'home'.
703 704 705 706
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

707 708 709 710 711 712
        private
          def scope_options
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

          def merge_path_scope(parent, child)
713
            Mapper.normalize_path("#{parent}/#{child}")
714 715
          end

716 717 718 719
          def merge_shallow_path_scope(parent, child)
            Mapper.normalize_path("#{parent}/#{child}")
          end

720
          def merge_as_scope(parent, child)
721
            parent ? "#{parent}_#{child}" : child
722 723
          end

724 725 726 727
          def merge_shallow_prefix_scope(parent, child)
            parent ? "#{parent}_#{child}" : child
          end

728
          def merge_module_scope(parent, child)
729 730 731 732
            parent ? "#{parent}/#{child}" : child
          end

          def merge_controller_scope(parent, child)
733
            child
734 735
          end

736
          def merge_path_names_scope(parent, child)
737 738 739 740 741 742 743
            merge_options_scope(parent, child)
          end

          def merge_constraints_scope(parent, child)
            merge_options_scope(parent, child)
          end

744 745 746 747
          def merge_defaults_scope(parent, child)
            merge_options_scope(parent, child)
          end

748
          def merge_blocks_scope(parent, child)
749 750 751
            merged = parent ? parent.dup : []
            merged << child if child
            merged
752 753 754
          end

          def merge_options_scope(parent, child)
755
            (parent || {}).except(*override_keys(child)).merge(child)
756
          end
757 758 759 760

          def merge_shallow_scope(parent, child)
            child ? true : false
          end
761 762 763 764

          def override_keys(child)
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
765 766
      end

767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797
      # Resource routing allows you to quickly declare all of the common routes
      # for a given resourceful controller. Instead of declaring separate routes
      # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
      # actions, a resourceful route declares them in a single line of code:
      #
      #  resources :photos
      #
      # Sometimes, you have a resource that clients always look up without
      # referencing an ID. A common example, /profile always shows the profile of
      # the currently logged in user. In this case, you can use a singular resource
      # to map /profile (rather than /profile/:id) to the show action.
      #
      #  resource :profile
      #
      # It's common to have resources that are logically children of other
      # resources:
      #
      #   resources :magazines do
      #     resources :ads
      #   end
      #
      # You may wish to organize groups of controllers under a namespace. Most
      # commonly, you might group a number of administrative controllers under
      # an +admin+ namespace. You would place these controllers under the
      # app/controllers/admin directory, and you can group them together in your
      # router:
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
J
Joshua Peek 已提交
798
      module Resources
799 800
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
801 802 803
        VALID_ON_OPTIONS  = [:new, :collection, :member]
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except]
        CANONICAL_ACTIONS = %w(index create new show update destroy)
804

805
        class Resource #:nodoc:
806
          DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit]
807

808
          attr_reader :controller, :path, :options
809 810

          def initialize(entities, options = {})
811
            @name       = entities.to_s
812
            @path       = (options.delete(:path) || @name).to_s
813
            @controller = (options.delete(:controller) || @name).to_s
814
            @as         = options.delete(:as)
815
            @options    = options
816 817
          end

818
          def default_actions
819
            self.class::DEFAULT_ACTIONS
820 821
          end

822
          def actions
823
            if only = @options[:only]
824
              Array(only).map(&:to_sym)
825
            elsif except = @options[:except]
826 827 828 829 830 831
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

832
          def name
833
            @as || @name
834 835
          end

836
          def plural
837
            @plural ||= name.to_s
838 839 840
          end

          def singular
841
            @singular ||= name.to_s.singularize
842 843
          end

844
          alias :member_name :singular
845

846
          # Checks for uncountable plurals, and appends "_index" if they're.
847
          def collection_name
848
            singular == plural ? "#{plural}_index" : plural
849 850
          end

851
          def resource_scope
852
            { :controller => controller }
853 854
          end

855
          alias :collection_scope :path
856 857

          def member_scope
858
            "#{path}/:id"
859 860
          end

861
          def new_scope(new_path)
862
            "#{path}/#{new_path}"
863 864 865
          end

          def nested_scope
866
            "#{path}/:#{singular}_id"
867
          end
868

869 870 871
        end

        class SingletonResource < Resource #:nodoc:
872
          DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit]
873

874
          def initialize(entities, options)
875
            @as         = nil
876
            @name       = entities.to_s
877
            @path       = (options.delete(:path) || @name).to_s
878
            @controller = (options.delete(:controller) || plural).to_s
879 880 881 882
            @as         = options.delete(:as)
            @options    = options
          end

883 884
          def plural
            @plural ||= name.to_s.pluralize
885 886
          end

887 888
          def singular
            @singular ||= name.to_s
889
          end
890 891 892 893 894 895

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
896 897
        end

898
        def initialize(*args) #:nodoc:
899
          super
900
          @scope[:path_names] = @set.resources_path_names
901 902
        end

903 904 905 906
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
        # Sometimes, you have a resource that clients always look up without
        # referencing an ID. A common example, /profile always shows the
        # profile of the currently logged in user. In this case, you can use
        # a singular resource to map /profile (rather than /profile/:id) to
        # the show action:
        #
        #   resource :geocoder
        #
        # creates six different routes in your application, all mapping to
        # the GeoCoders controller (note that the controller is named after
        # the plural):
        #
        #   GET     /geocoder/new
        #   POST    /geocoder
        #   GET     /geocoder
        #   GET     /geocoder/edit
        #   PUT     /geocoder
        #   DELETE  /geocoder
J
Joshua Peek 已提交
925
        def resource(*resources, &block)
J
Joshua Peek 已提交
926
          options = resources.extract_options!
J
Joshua Peek 已提交
927

928
          if apply_common_behavior_for(:resource, resources, options, &block)
929 930 931
            return self
          end

932 933
          resource_scope(SingletonResource.new(resources.pop, options)) do
            yield if block_given?
934

935
            collection do
936
              post :create
937
            end if parent_resource.actions.include?(:create)
938

939
            new do
940
              get :new
941
            end if parent_resource.actions.include?(:new)
942

943
            member do
944
              get    :edit if parent_resource.actions.include?(:edit)
945 946 947
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
948 949 950
            end
          end

J
Joshua Peek 已提交
951
          self
952 953
        end

954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969
        # In Rails, a resourceful route provides a mapping between HTTP verbs
        # and URLs and controller actions. By convention, each action also maps
        # to particular CRUD operations in a database. A single entry in the
        # routing file, such as
        #
        #   resources :photos
        #
        # creates seven different routes in your application, all mapping to
        # the Photos controller:
        #
        #   GET     /photos/new
        #   POST    /photos
        #   GET     /photos/:id
        #   GET     /photos/:id/edit
        #   PUT     /photos/:id
        #   DELETE  /photos/:id
970
        #
971 972 973 974 975 976 977 978
        # === Supported options
        # [:path_names]
        #   Allows you to change the paths of the seven default actions.
        #   Paths not specified are not changed.
        #
        #     resources :posts, :path_names => { :new => "brand_new" }
        #
        #   The above example will now change /posts/new to /posts/brand_new
979 980 981 982 983 984 985
        #
        # [:module]
        #   Set the module where the controller can be found. Defaults to nothing.
        #
        #     resources :posts, :module => "admin"
        #
        #   All requests to the posts resources will now go to +Admin::PostsController+.
J
Joshua Peek 已提交
986
        def resources(*resources, &block)
J
Joshua Peek 已提交
987
          options = resources.extract_options!
988

989
          if apply_common_behavior_for(:resources, resources, options, &block)
990 991 992
            return self
          end

993
          resource_scope(Resource.new(resources.pop, options)) do
994
            yield if block_given?
J
Joshua Peek 已提交
995

996
            collection do
997 998
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
999
            end
1000

1001
            new do
1002
              get :new
1003
            end if parent_resource.actions.include?(:new)
1004

1005
            member do
1006
              get    :edit if parent_resource.actions.include?(:edit)
1007 1008 1009
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
1010 1011 1012
            end
          end

J
Joshua Peek 已提交
1013
          self
1014 1015
        end

1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
        # To add a route to the collection:
        #
        #   resources :photos do
        #     collection do
        #       get 'search'
        #     end
        #   end
        #
        # This will enable Rails to recognize paths such as <tt>/photos/search</tt>
        # with GET, and route to the search action of PhotosController. It will also
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1028
        def collection
1029 1030
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1031 1032
          end

1033 1034 1035 1036
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1037
          end
1038
        end
J
Joshua Peek 已提交
1039

1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050
        # To add a member route, add a member block into the resource block:
        #
        #   resources :photos do
        #     member do
        #       get 'preview'
        #     end
        #   end
        #
        # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
        # preview action of PhotosController. It will also create the
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1051
        def member
1052 1053
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1054
          end
J
Joshua Peek 已提交
1055

1056 1057 1058 1059
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1060 1061 1062 1063 1064 1065 1066
          end
        end

        def new
          unless resource_scope?
            raise ArgumentError, "can't use new outside resource(s) scope"
          end
1067

1068 1069 1070 1071
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1072
          end
J
Joshua Peek 已提交
1073 1074
        end

1075
        def nested
1076 1077
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1078 1079 1080
          end

          with_scope_level(:nested) do
1081
            if shallow?
1082
              with_exclusive_scope do
1083
                if @scope[:shallow_path].blank?
1084
                  scope(parent_resource.nested_scope, nested_options) { yield }
1085
                else
1086
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1087
                    scope(parent_resource.nested_scope, nested_options) { yield }
1088 1089 1090 1091
                  end
                end
              end
            else
1092
              scope(parent_resource.nested_scope, nested_options) { yield }
1093 1094 1095 1096
            end
          end
        end

1097
        def namespace(path, options = {})
1098
          if resource_scope?
1099 1100 1101 1102 1103 1104
            nested { super }
          else
            super
          end
        end

1105 1106 1107 1108 1109 1110
        def shallow
          scope(:shallow => true) do
            yield
          end
        end

1111 1112 1113 1114
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

J
Joshua Peek 已提交
1115
        def match(*args)
1116
          options = args.extract_options!.dup
1117 1118
          options[:anchor] = true unless options.key?(:anchor)

1119
          if args.length > 1
1120
            args.each { |path| match(path, options.dup) }
1121 1122 1123
            return self
          end

1124 1125
          on = options.delete(:on)
          if VALID_ON_OPTIONS.include?(on)
1126
            args.push(options)
1127 1128 1129
            return send(on){ match(*args) }
          elsif on
            raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1130 1131
          end

1132 1133 1134 1135
          if @scope[:scope_level] == :resources
            args.push(options)
            return nested { match(*args) }
          elsif @scope[:scope_level] == :resource
1136
            args.push(options)
J
Joshua Peek 已提交
1137 1138
            return member { match(*args) }
          end
J
Joshua Peek 已提交
1139

1140
          action = args.first
1141
          path = path_for_action(action, options.delete(:path))
1142

1143 1144 1145
          if action.to_s =~ /^[\w\/]+$/
            options[:action] ||= action unless action.to_s.include?("/")
          else
1146 1147 1148 1149 1150 1151 1152
            action = nil
          end

          if options.key?(:as) && !options[:as]
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1153
          end
J
Joshua Peek 已提交
1154

1155
          super(path, options)
J
Joshua Peek 已提交
1156 1157
        end

1158
        def root(options={})
1159
          if @scope[:scope_level] == :resources
1160 1161
            with_scope_level(:root) do
              scope(parent_resource.path) do
1162 1163 1164 1165 1166 1167
                super(options)
              end
            end
          else
            super(options)
          end
1168 1169
        end

1170
        protected
1171

1172
          def parent_resource #:nodoc:
1173 1174 1175
            @scope[:scope_level_resource]
          end

1176
          def apply_common_behavior_for(method, resources, options, &block)
1177 1178 1179 1180 1181
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1182 1183 1184 1185 1186
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1187
            options.keys.each do |k|
1188 1189 1190
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1191 1192 1193
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1194 1195 1196 1197 1198
                send(method, resources.pop, options, &block)
              end
              return true
            end

1199 1200 1201 1202
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1203 1204 1205
            false
          end

1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217
          def action_options?(options)
            options[:only] || options[:except]
          end

          def scope_action_options?
            @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except])
          end

          def scope_action_options
            @scope[:options].slice(:only, :except)
          end

1218 1219 1220 1221
          def resource_scope?
            [:resource, :resources].include?(@scope[:scope_level])
          end

1222 1223 1224 1225
          def resource_method_scope?
            [:collection, :member, :new].include?(@scope[:scope_level])
          end

1226
          def with_exclusive_scope
1227
            begin
1228 1229
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1230

1231 1232 1233
              with_scope_level(:exclusive) do
                yield
              end
1234
            ensure
1235
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1236 1237 1238
            end
          end

1239
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1240
            old, @scope[:scope_level] = @scope[:scope_level], kind
1241
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1242 1243 1244
            yield
          ensure
            @scope[:scope_level] = old
1245
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1246
          end
1247 1248 1249

          def resource_scope(resource)
            with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do
1250
              scope(parent_resource.resource_scope) do
1251 1252 1253 1254 1255
                yield
              end
            end
          end

1256 1257 1258 1259 1260 1261 1262 1263
          def nested_options
            {}.tap do |options|
              options[:as] = parent_resource.member_name
              options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint?
            end
          end

          def id_constraint?
1264
            @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
1265 1266 1267
          end

          def id_constraint
1268
            @scope[:constraints][:id]
1269 1270
          end

1271
          def canonical_action?(action, flag)
1272
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1273 1274 1275
          end

          def shallow_scoping?
1276
            shallow? && @scope[:scope_level] == :member
1277 1278
          end

1279
          def path_for_action(action, path)
1280
            prefix = shallow_scoping? ?
1281 1282
              "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]

1283 1284
            path = if canonical_action?(action, path.blank?)
              prefix.to_s
1285
            else
1286
              "#{prefix}/#{action_path(action, path)}"
1287 1288 1289
            end
          end

1290 1291
          def action_path(name, path = nil)
            path || @scope[:path_names][name.to_sym] || name.to_s
1292 1293
          end

1294 1295
          def prefix_name_for_action(as, action)
            if as
1296
              as.to_s
1297
            elsif !canonical_action?(action, @scope[:scope_level])
1298
              action.to_s
1299
            end
1300 1301
          end

1302 1303
          def name_for_action(as, action)
            prefix = prefix_name_for_action(as, action)
1304
            prefix = Mapper.normalize_name(prefix) if prefix
1305 1306 1307 1308 1309
            name_prefix = @scope[:as]

            if parent_resource
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1310
            end
1311

1312
            name = case @scope[:scope_level]
1313 1314
            when :nested
              [member_name, prefix]
1315
            when :collection
1316
              [prefix, name_prefix, collection_name]
1317
            when :new
1318 1319 1320 1321 1322
              [prefix, :new, name_prefix, member_name]
            when :member
              [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
            when :root
              [name_prefix, collection_name, prefix]
1323
            else
1324
              [name_prefix, member_name, prefix]
1325
            end
1326

1327
            candidate = name.select(&:present?).join("_").presence
1328
            candidate unless as.nil? && @set.routes.find { |r| r.name == candidate }
1329
          end
J
Joshua Peek 已提交
1330
      end
J
Joshua Peek 已提交
1331

1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344
      module Shorthand
        def match(*args)
          if args.size == 1 && args.last.is_a?(Hash)
            options  = args.pop
            path, to = options.find { |name, value| name.is_a?(String) }
            options.merge!(:to => to).delete(path)
            super(path, options)
          else
            super
          end
        end
      end

1345 1346 1347 1348
      include Base
      include HttpHelpers
      include Scoping
      include Resources
1349
      include Shorthand
J
Joshua Peek 已提交
1350 1351
    end
  end
J
Joshua Peek 已提交
1352
end