mapper.rb 47.7 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
require 'action_dispatch/routing/redirection'
6

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

19 20
        attr_reader :app

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

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

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

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

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

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

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

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

59
        private
60 61 62

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

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

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

            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
82
          end
83

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

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

            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

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

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

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

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

132
          def defaults
133 134 135 136 137 138
            @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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

246 247 248 249
        # You can specify what Rails should route "/" to with the root method:
        #
        #   root :to => 'pages#main'
        #
250 251
        # For options, see the +match+ method's documentation, as +root+ uses it internally.
        #
252 253 254
        # You should put the root route at the top of <tt>config/routes.rb</tt>,
        # because this means it will be matched first. As this is the most popular route
        # of most Rails applications, this is beneficial.
255 256 257
        def root(options = {})
          match '/', options.reverse_merge(:as => :root)
        end
258

259 260
        # Matches a pattern to one or more urls. Any symbols in a pattern are
        # interpreted as url parameters:
261
        #
262 263
        #   # sets parameters :controller, :action and :id
        #   match ':controller/:action/:id'
264
        #
265 266 267 268 269 270 271 272 273 274
        # Two of these symbols are special: <tt>:controller</tt> maps to the
        # controller name and <tt>:action</tt> to the action name within that
        # controller. Anything other than <tt>:controller</tt> or
        # <tt>:action</tt> will be available to the action as part of +params+.
        # If a pattern does not have :controller and :action symbols, then they
        # must be set in options or shorthand. For example:
        #
        #   match 'photos/:id' => 'photos#show'
        #   match 'photos/:id', :to => 'photos#show'
        #   match 'photos/:id', :controller => 'photos', :action => 'show'
275
        #
276
        # === Options
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
        #
        # [:controller]
        #   The route's controller.
        #
        # [:action]
        #   The route's action.
        #
        # [:path]
        #   The path prefix for the routes.
        #
        # [:module]
        #   The namespace for :controller.
        #
        #     match 'path' => 'c#a', :module => 'sekret', :controller => 'posts'
        #     #=> Sekret::PostsController
        #
        #   See <tt>Scoping#namespace</tt> for its scope equivalent.
        #
        # [:as]
        #   The name used to generate routing helpers.
        #
        # [:via]
        #   Allowed HTTP verb(s) for route.
        #
        #      match 'path' => 'c#a', :via => :get
        #      match 'path' => 'c#a', :via => [:get, :post]
        #
        # [:to]
        #   Shorthand for specifying :controller and :action.
        #
        #      match 'path' => 'c#a', :to => 'controller#action'
        #
        # [:on]
        #   Shorthand for wrapping routes in a specific RESTful context. Valid
        #   values are :member, :collection, and :new.  Only use within
        #   <tt>resource(s)</tt> block. For example:
        #
        #      resource :bar do
        #        match 'foo' => 'c#a', :on => :member, :via => [:get, :post]
        #      end
        #
        #   Is equivalent to:
        #
        #      resource :bar do
        #        member do
        #          match 'foo' => 'c#a', :via => [:get, :post]
        #        end
        #      end
        #
        # [:constraints]
        #   Constrains parameters with a hash of regular expressions or an
        #   object that responds to #matches?
        #
        #     match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ }
        #
        #     class Blacklist
        #       def matches?(request) request.remote_ip == '1.2.3.4' end
        #     end
        #     match 'path' => 'c#a', :constraints => Blacklist.new
        #
        #   See <tt>Scoping#constraints</tt> for more examples with its scope
        #   equivalent.
        #
        # [:defaults]
        #   Sets defaults for parameters
        #
        #     # Sets params[:format] to 'jpg' by default
        #     match 'path' => 'c#a', :defaults => { :format => 'jpg' }
        #
        #   See <tt>Scoping#defaults</tt> for its scope equivalent.
347 348
        def match(path, options=nil)
          mapping = Mapping.new(@set, @scope, path, options || {}).to_route
349
          @set.add_route(*mapping)
350 351
          self
        end
352

353 354
        # Mount a Rack-based application to be used within the application.
        #
R
Ryan Bigg 已提交
355
        #   mount SomeRackApp, :at => "some_route"
356 357 358
        #
        # Alternatively:
        #
R
Ryan Bigg 已提交
359
        #   mount(SomeRackApp => "some_route")
360 361 362 363 364 365
        #
        # 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:
        #
R
Ryan Bigg 已提交
366
        #   mount(SomeRackApp => "some_route", :as => "exciting")
367 368 369
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
370 371 372 373 374 375 376 377 378 379 380
        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

381 382
          options[:as] ||= app_name(app)

383
          match(path, options.merge(:to => app, :anchor => false, :format => false))
384 385

          define_generate_prefix(app, options[:as])
386 387 388
          self
        end

389 390 391 392
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
393

394 395 396 397 398 399
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

400 401 402
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
403 404 405 406 407 408 409

            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
410 411 412
          end

          def define_generate_prefix(app, name)
413
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
414 415

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
416 417
            _routes = @set
            app.routes.define_mounted_helper(name)
418 419
            app.routes.class_eval do
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
420
                prefix_options = options.slice(*_route.segment_keys)
421 422
                # 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 已提交
423
                _routes.url_helpers.send("#{name}_path", prefix_options)
424 425 426
              end
            end
          end
427 428 429
      end

      module HttpHelpers
430
        # Define a route that only recognizes HTTP GET.
431
        # For supported arguments, see <tt>Base#match</tt>.
432 433 434 435
        #
        # Example:
        #
        # get 'bacon', :to => 'food#bacon'
436 437 438 439
        def get(*args, &block)
          map_method(:get, *args, &block)
        end

440
        # Define a route that only recognizes HTTP POST.
441
        # For supported arguments, see <tt>Base#match</tt>.
442 443 444 445
        #
        # Example:
        #
        # post 'bacon', :to => 'food#bacon'
446 447 448 449
        def post(*args, &block)
          map_method(:post, *args, &block)
        end

450
        # Define a route that only recognizes HTTP PUT.
451
        # For supported arguments, see <tt>Base#match</tt>.
452 453 454 455
        #
        # Example:
        #
        # put 'bacon', :to => 'food#bacon'
456 457 458 459
        def put(*args, &block)
          map_method(:put, *args, &block)
        end

460
        # Define a route that only recognizes HTTP PUT.
461
        # For supported arguments, see <tt>Base#match</tt>.
462 463 464 465
        #
        # Example:
        #
        # delete 'broccoli', :to => 'food#broccoli'
466 467 468 469 470 471 472 473 474 475 476 477 478 479
        def delete(*args, &block)
          map_method(:delete, *args, &block)
        end

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

480 481 482 483 484 485 486 487 488
      # 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
489
      #
490 491
      # This will create a number of routes for each of the posts and comments
      # controller. For Admin::PostsController, Rails will create:
492
      #
493 494 495 496 497 498 499
      #   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
500
      #
501
      # If you want to route /posts (without the prefix /admin) to
502
      # Admin::PostsController, you could use
503
      #
504
      #   scope :module => "admin" do
505
      #     resources :posts
506 507 508
      #   end
      #
      # or, for a single case
509
      #
510
      #   resources :posts, :module => "admin"
511
      #
512
      # If you want to route /admin/posts to PostsController
513
      # (without the Admin:: module prefix), you could use
514
      #
515
      #   scope "/admin" do
516
      #     resources :posts
517 518 519
      #   end
      #
      # or, for a single case
520
      #
521 522 523 524 525
      #   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:
526
      #
527 528 529 530 531 532 533
      #   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
534
      module Scoping
535
        def initialize(*args) #:nodoc:
536 537 538 539
          @scope = {}
          super
        end

540
        # Scopes a set of routes to the given default options.
541 542 543 544 545 546 547 548 549 550 551
        #
        # Take the following route definition as an example:
        #
        #   scope :path => ":account_id", :as => "account" do
        #     resources :projects
        #   end
        #
        # This generates helpers such as +account_projects_path+, just like +resources+ does.
        # The difference here being that the routes generated are like /rails/projects/2,
        # rather than /accounts/rails/projects/2.
        #
552
        # === Options
553
        #
554
        # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
555
        #
556
        # === Examples
557
        #
558 559 560 561
        #   # route /posts (without the prefix /admin) to Admin::PostsController
        #   scope :module => "admin" do
        #     resources :posts
        #   end
562
        #
563 564 565 566
        #   # prefix the posts resource's requests with '/admin'
        #   scope :path => "/admin" do
        #     resources :posts
        #   end
567
        #
568 569 570 571
        #   # prefix the routing helper name: sekret_posts_path instead of posts_path
        #   scope :as => "sekret" do
        #     resources :posts
        #   end
572 573
        def scope(*args)
          options = args.extract_options!
574
          options = options.dup
575

576
          options[:path] = args.first if args.first.is_a?(String)
577
          recover = {}
578

579 580 581
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
            block, options[:constraints] = options[:constraints], {}
582
          end
583

584 585 586 587 588
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
589 590
          end

591 592
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
593

594 595
          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)
596 597 598 599

          yield
          self
        ensure
600 601 602 603 604 605
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
606 607
        end

608 609 610 611 612 613
        # Scopes routes to a specific controller
        #
        # Example:
        #   controller "food" do
        #     match "bacon", :action => "bacon"
        #   end
614 615 616
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
617 618
        end

619 620 621 622 623 624 625 626
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
627 628 629 630 631 632 633
        #       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"}
634
        #
635
        # === Options
636
        #
637 638
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
        # options all default to the name of the namespace.
639
        #
640 641
        # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
        # <tt>Resources#resources</tt>.
642
        #
643
        # === Examples
644
        #
645 646 647 648
        #   # accessible through /sekret/posts rather than /admin/posts
        #   namespace :admin, :path => "sekret" do
        #     resources :posts
        #   end
649
        #
650 651 652 653
        #   # maps to Sekret::PostsController rather than Admin::PostsController
        #   namespace :admin, :module => "sekret" do
        #     resources :posts
        #   end
654
        #
655 656 657 658
        #   # generates sekret_posts_path rather than admin_posts_path
        #   namespace :admin, :as => "sekret" do
        #     resources :posts
        #   end
659
        def namespace(path, options = {})
660
          path = path.to_s
661 662 663
          options = { :path => path, :as => path, :module => path,
                      :shallow_path => path, :shallow_prefix => path }.merge!(options)
          scope(options) { yield }
664
        end
665

R
Ryan Bigg 已提交
666 667 668 669 670 671 672 673 674 675
        # === 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.
676
        #
R
Ryan Bigg 已提交
677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
        # 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
720 721 722 723
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
724
        # Allows you to set default parameters for a route, such as this:
725 726 727
        #   defaults :id => 'home' do
        #     match 'scoped_pages/(:id)', :to => 'pages#show'
        #   end
R
Ryan Bigg 已提交
728
        # Using this, the +:id+ parameter here will default to 'home'.
729 730 731 732
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

733
        private
J
José Valim 已提交
734
          def scope_options #:nodoc:
735 736 737
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

J
José Valim 已提交
738
          def merge_path_scope(parent, child) #:nodoc:
739
            Mapper.normalize_path("#{parent}/#{child}")
740 741
          end

J
José Valim 已提交
742
          def merge_shallow_path_scope(parent, child) #:nodoc:
743 744 745
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
746
          def merge_as_scope(parent, child) #:nodoc:
747
            parent ? "#{parent}_#{child}" : child
748 749
          end

J
José Valim 已提交
750
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
751 752 753
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
754
          def merge_module_scope(parent, child) #:nodoc:
755 756 757
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
758
          def merge_controller_scope(parent, child) #:nodoc:
759
            child
760 761
          end

J
José Valim 已提交
762
          def merge_path_names_scope(parent, child) #:nodoc:
763 764 765
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
766
          def merge_constraints_scope(parent, child) #:nodoc:
767 768 769
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
770
          def merge_defaults_scope(parent, child) #:nodoc:
771 772 773
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
774
          def merge_blocks_scope(parent, child) #:nodoc:
775 776 777
            merged = parent ? parent.dup : []
            merged << child if child
            merged
778 779
          end

J
José Valim 已提交
780
          def merge_options_scope(parent, child) #:nodoc:
781
            (parent || {}).except(*override_keys(child)).merge(child)
782
          end
783

J
José Valim 已提交
784
          def merge_shallow_scope(parent, child) #:nodoc:
785 786
            child ? true : false
          end
787

J
José Valim 已提交
788
          def override_keys(child) #:nodoc:
789 790
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
791 792
      end

793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823
      # 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 已提交
824
      module Resources
825 826
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
827 828 829
        VALID_ON_OPTIONS  = [:new, :collection, :member]
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except]
        CANONICAL_ACTIONS = %w(index create new show update destroy)
830

831
        class Resource #:nodoc:
832
          DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit]
833

834
          attr_reader :controller, :path, :options
835 836

          def initialize(entities, options = {})
837
            @name       = entities.to_s
838
            @path       = (options.delete(:path) || @name).to_s
839
            @controller = (options.delete(:controller) || @name).to_s
840
            @as         = options.delete(:as)
841
            @options    = options
842 843
          end

844
          def default_actions
845
            self.class::DEFAULT_ACTIONS
846 847
          end

848
          def actions
849
            if only = @options[:only]
850
              Array(only).map(&:to_sym)
851
            elsif except = @options[:except]
852 853 854 855 856 857
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

858
          def name
859
            @as || @name
860 861
          end

862
          def plural
863
            @plural ||= name.to_s
864 865 866
          end

          def singular
867
            @singular ||= name.to_s.singularize
868 869
          end

870
          alias :member_name :singular
871

872 873
          # Checks for uncountable plurals, and appends "_index" if the plural 
          # and singular form are the same.
874
          def collection_name
875
            singular == plural ? "#{plural}_index" : plural
876 877
          end

878
          def resource_scope
879
            { :controller => controller }
880 881
          end

882
          alias :collection_scope :path
883 884

          def member_scope
885
            "#{path}/:id"
886 887
          end

888
          def new_scope(new_path)
889
            "#{path}/#{new_path}"
890 891 892
          end

          def nested_scope
893
            "#{path}/:#{singular}_id"
894
          end
895

896 897 898
        end

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

901
          def initialize(entities, options)
902
            @as         = nil
903
            @name       = entities.to_s
904
            @path       = (options.delete(:path) || @name).to_s
905
            @controller = (options.delete(:controller) || plural).to_s
906 907 908 909
            @as         = options.delete(:as)
            @options    = options
          end

910 911
          def plural
            @plural ||= name.to_s.pluralize
912 913
          end

914 915
          def singular
            @singular ||= name.to_s
916
          end
917 918 919 920 921 922

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
923 924
        end

925
        def initialize(*args) #:nodoc:
926
          super
927
          @scope[:path_names] = @set.resources_path_names
928 929
        end

930 931 932 933
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
        # 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
952
        #
953
        # === Options
954
        # Takes same options as +resources+.
J
Joshua Peek 已提交
955
        def resource(*resources, &block)
J
Joshua Peek 已提交
956
          options = resources.extract_options!
J
Joshua Peek 已提交
957

958
          if apply_common_behavior_for(:resource, resources, options, &block)
959 960 961
            return self
          end

962 963
          resource_scope(SingletonResource.new(resources.pop, options)) do
            yield if block_given?
964

965
            collection do
966
              post :create
967
            end if parent_resource.actions.include?(:create)
968

969
            new do
970
              get :new
971
            end if parent_resource.actions.include?(:new)
972

973
            member do
974
              get    :edit if parent_resource.actions.include?(:edit)
975 976 977
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
978 979 980
            end
          end

J
Joshua Peek 已提交
981
          self
982 983
        end

984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999
        # 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
1000
        #
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
        # Resources can also be nested infinitely by using this block syntax:
        #
        #   resources :photos do
        #     resources :comments
        #   end
        #
        # This generates the following comments routes:
        #
        #   GET     /photos/:id/comments/new
        #   POST    /photos/:id/comments
        #   GET     /photos/:id/comments/:id
        #   GET     /photos/:id/comments/:id/edit
        #   PUT     /photos/:id/comments/:id
        #   DELETE  /photos/:id/comments/:id
        #
1016
        # === Options
1017 1018
        # Takes same options as <tt>Base#match</tt> as well as:
        #
1019 1020 1021 1022 1023 1024 1025
        # [: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
1026
        #
1027 1028
        # [:only]
        #   Only generate routes for the given actions.
1029
        #
1030 1031
        #     resources :cows, :only => :show
        #     resources :cows, :only => [:show, :index]
1032
        #
1033 1034
        # [:except]
        #   Generate all routes except for the given actions.
1035
        #
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073
        #     resources :cows, :except => :show
        #     resources :cows, :except => [:show, :index]
        #
        # [:shallow]
        #   Generates shallow routes for nested resource(s). When placed on a parent resource,
        #   generates shallow routes for all nested resources.
        #
        #     resources :posts, :shallow => true do
        #       resources :comments
        #     end
        #
        #   Is the same as:
        #
        #     resources :posts do
        #       resources :comments
        #     end
        #     resources :comments
        #
        # [:shallow_path]
        #   Prefixes nested shallow routes with the specified path.
        #
        #   scope :shallow_path => "sekret" do
        #     resources :posts do
        #       resources :comments, :shallow => true
        #     end
        #   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)
        #
        # === Examples
1074
        #
1075 1076
        #   # routes call Admin::PostsController
        #   resources :posts, :module => "admin"
1077
        #
1078 1079
        #   # resource actions are at /admin/posts.
        #   resources :posts, :path => "admin"
1080
        #
J
Joshua Peek 已提交
1081
        def resources(*resources, &block)
J
Joshua Peek 已提交
1082
          options = resources.extract_options!
1083

1084
          if apply_common_behavior_for(:resources, resources, options, &block)
1085 1086 1087
            return self
          end

1088
          resource_scope(Resource.new(resources.pop, options)) do
1089
            yield if block_given?
J
Joshua Peek 已提交
1090

1091
            collection do
1092 1093
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1094
            end
1095

1096
            new do
1097
              get :new
1098
            end if parent_resource.actions.include?(:new)
1099

1100
            member do
1101
              get    :edit if parent_resource.actions.include?(:edit)
1102 1103 1104
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
1105 1106 1107
            end
          end

J
Joshua Peek 已提交
1108
          self
1109 1110
        end

1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122
        # 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 已提交
1123
        def collection
1124 1125
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1126 1127
          end

1128 1129 1130 1131
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1132
          end
1133
        end
J
Joshua Peek 已提交
1134

1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
        # 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 已提交
1146
        def member
1147 1148
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1149
          end
J
Joshua Peek 已提交
1150

1151 1152 1153 1154
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1155 1156 1157 1158 1159 1160 1161
          end
        end

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

1163 1164 1165 1166
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1167
          end
J
Joshua Peek 已提交
1168 1169
        end

1170
        def nested
1171 1172
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1173 1174 1175
          end

          with_scope_level(:nested) do
1176
            if shallow?
1177
              with_exclusive_scope do
1178
                if @scope[:shallow_path].blank?
1179
                  scope(parent_resource.nested_scope, nested_options) { yield }
1180
                else
1181
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1182
                    scope(parent_resource.nested_scope, nested_options) { yield }
1183 1184 1185 1186
                  end
                end
              end
            else
1187
              scope(parent_resource.nested_scope, nested_options) { yield }
1188 1189 1190 1191
            end
          end
        end

1192
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1193
        def namespace(path, options = {})
1194
          if resource_scope?
1195 1196 1197 1198 1199 1200
            nested { super }
          else
            super
          end
        end

1201 1202 1203 1204 1205 1206
        def shallow
          scope(:shallow => true) do
            yield
          end
        end

1207 1208 1209 1210
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

J
Joshua Peek 已提交
1211
        def match(*args)
1212
          options = args.extract_options!.dup
1213 1214
          options[:anchor] = true unless options.key?(:anchor)

1215
          if args.length > 1
1216
            args.each { |path| match(path, options.dup) }
1217 1218 1219
            return self
          end

1220 1221
          on = options.delete(:on)
          if VALID_ON_OPTIONS.include?(on)
1222
            args.push(options)
1223 1224 1225
            return send(on){ match(*args) }
          elsif on
            raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1226 1227
          end

1228 1229 1230 1231
          if @scope[:scope_level] == :resources
            args.push(options)
            return nested { match(*args) }
          elsif @scope[:scope_level] == :resource
1232
            args.push(options)
J
Joshua Peek 已提交
1233 1234
            return member { match(*args) }
          end
J
Joshua Peek 已提交
1235

1236
          action = args.first
1237
          path = path_for_action(action, options.delete(:path))
1238

1239 1240 1241
          if action.to_s =~ /^[\w\/]+$/
            options[:action] ||= action unless action.to_s.include?("/")
          else
1242 1243 1244 1245 1246 1247 1248
            action = nil
          end

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

1251
          super(path, options)
J
Joshua Peek 已提交
1252 1253
        end

1254
        def root(options={})
1255
          if @scope[:scope_level] == :resources
1256 1257
            with_scope_level(:root) do
              scope(parent_resource.path) do
1258 1259 1260 1261 1262 1263
                super(options)
              end
            end
          else
            super(options)
          end
1264 1265
        end

1266
        protected
1267

1268
          def parent_resource #:nodoc:
1269 1270 1271
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1272
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1273 1274 1275 1276 1277
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1278 1279 1280 1281 1282
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1283
            options.keys.each do |k|
1284 1285 1286
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1287 1288 1289
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1290 1291 1292 1293 1294
                send(method, resources.pop, options, &block)
              end
              return true
            end

1295 1296 1297 1298
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1299 1300 1301
            false
          end

J
José Valim 已提交
1302
          def action_options?(options) #:nodoc:
1303 1304 1305
            options[:only] || options[:except]
          end

J
José Valim 已提交
1306
          def scope_action_options? #:nodoc:
1307 1308 1309
            @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except])
          end

J
José Valim 已提交
1310
          def scope_action_options #:nodoc:
1311 1312 1313
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1314
          def resource_scope? #:nodoc:
1315 1316 1317
            [:resource, :resources].include?(@scope[:scope_level])
          end

J
José Valim 已提交
1318
          def resource_method_scope? #:nodoc:
1319 1320 1321
            [:collection, :member, :new].include?(@scope[:scope_level])
          end

1322
          def with_exclusive_scope
1323
            begin
1324 1325
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1326

1327 1328 1329
              with_scope_level(:exclusive) do
                yield
              end
1330
            ensure
1331
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1332 1333 1334
            end
          end

1335
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1336
            old, @scope[:scope_level] = @scope[:scope_level], kind
1337
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1338 1339 1340
            yield
          ensure
            @scope[:scope_level] = old
1341
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1342
          end
1343

J
José Valim 已提交
1344
          def resource_scope(resource) #:nodoc:
1345
            with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do
1346
              scope(parent_resource.resource_scope) do
1347 1348 1349 1350 1351
                yield
              end
            end
          end

J
José Valim 已提交
1352
          def nested_options #:nodoc:
1353 1354 1355 1356 1357 1358
            {}.tap do |options|
              options[:as] = parent_resource.member_name
              options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint?
            end
          end

J
José Valim 已提交
1359
          def id_constraint? #:nodoc:
1360
            @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
1361 1362
          end

J
José Valim 已提交
1363
          def id_constraint #:nodoc:
1364
            @scope[:constraints][:id]
1365 1366
          end

J
José Valim 已提交
1367
          def canonical_action?(action, flag) #:nodoc:
1368
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1369 1370
          end

J
José Valim 已提交
1371
          def shallow_scoping? #:nodoc:
1372
            shallow? && @scope[:scope_level] == :member
1373 1374
          end

J
José Valim 已提交
1375
          def path_for_action(action, path) #:nodoc:
1376
            prefix = shallow_scoping? ?
1377 1378
              "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]

1379 1380
            path = if canonical_action?(action, path.blank?)
              prefix.to_s
1381
            else
1382
              "#{prefix}/#{action_path(action, path)}"
1383 1384 1385
            end
          end

J
José Valim 已提交
1386
          def action_path(name, path = nil) #:nodoc:
1387
            path || @scope[:path_names][name.to_sym] || name.to_s
1388 1389
          end

J
José Valim 已提交
1390
          def prefix_name_for_action(as, action) #:nodoc:
1391
            if as
1392
              as.to_s
1393
            elsif !canonical_action?(action, @scope[:scope_level])
1394
              action.to_s
1395
            end
1396 1397
          end

J
José Valim 已提交
1398
          def name_for_action(as, action) #:nodoc:
1399
            prefix = prefix_name_for_action(as, action)
1400
            prefix = Mapper.normalize_name(prefix) if prefix
1401 1402 1403
            name_prefix = @scope[:as]

            if parent_resource
1404 1405
              return nil if as.nil? && action.nil?

1406 1407
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1408
            end
1409

1410
            name = case @scope[:scope_level]
1411 1412
            when :nested
              [member_name, prefix]
1413
            when :collection
1414
              [prefix, name_prefix, collection_name]
1415
            when :new
1416 1417 1418 1419 1420
              [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]
1421
            else
1422
              [name_prefix, member_name, prefix]
1423
            end
1424

1425
            candidate = name.select(&:present?).join("_").presence
1426
            candidate unless as.nil? && @set.routes.find { |r| r.name == candidate }
1427
          end
J
Joshua Peek 已提交
1428
      end
J
Joshua Peek 已提交
1429

J
José Valim 已提交
1430
      module Shorthand #:nodoc:
1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442
        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

1443 1444
      include Base
      include HttpHelpers
1445
      include Redirection
1446 1447
      include Scoping
      include Resources
1448
      include Shorthand
J
Joshua Peek 已提交
1449 1450
    end
  end
J
Joshua Peek 已提交
1451
end