mapper.rb 50.1 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/core_ext/object/inclusion'
5
require 'active_support/inflector'
6
require 'action_dispatch/routing/redirection'
7

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

20 21
        attr_reader :app

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

26
        def matches?(env)
27
          req = @request.new(env)
28 29 30

          @constraints.each { |constraint|
            if constraint.respond_to?(:matches?) && !constraint.matches?(req)
31
              return false
32
            elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req))
33
              return false
34 35 36
            end
          }

37 38 39 40 41
          return true
        end

        def call(env)
          matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
42
        end
43 44 45 46 47

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

50
      class Mapping #:nodoc:
51
        IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
52
        ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
53 54
        SHORTHAND_REGEX = %r{^/[\w/]+$}
        WILDCARD_PATH = %r{\*([^/]+)$}
55

56
        def initialize(set, scope, path, options)
57 58
          @set, @scope = set, scope
          @options = (@scope[:options] || {}).merge(options)
59
          @path = normalize_path(path)
60
          normalize_options!
61
        end
J
Joshua Peek 已提交
62

63
        def to_route
64
          [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
65
        end
J
Joshua Peek 已提交
66

67
        private
68 69 70

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

72 73 74
            if using_match_shorthand?(path_without_format, @options)
              to_shorthand    = @options[:to].blank?
              @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1')
75 76
            end

77
            @options.merge!(default_controller_and_action(to_shorthand))
78 79 80 81 82

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

83
              if requirement.source =~ ANCHOR_CHARACTERS_REGEX
84 85 86 87 88 89
                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
90
          end
91

92
          # match "account/overview"
93
          def using_match_shorthand?(path, options)
94
            path && options.except(:via, :anchor, :to, :as).empty? && path =~ SHORTHAND_REGEX
95
          end
96

97
          def normalize_path(path)
98 99
            raise ArgumentError, "path is required" if path.blank?
            path = Mapper.normalize_path(path)
100 101 102 103 104 105 106 107 108 109 110

            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

111 112
            # Add a constraint for wildcard route to make it non-greedy and match the
            # optional format part of the route by default
113
            if path.match(WILDCARD_PATH) && @options[:format] != false
114 115 116
              @options.reverse_merge!(:"#{$1}" => /.+?/)
            end

117 118 119
            if @options[:format] == false
              @options.delete(:format)
              path
120
            elsif path.include?(":format") || path.end_with?('/')
121 122 123 124
              path
            else
              "#{path}(.:format)"
            end
125
          end
126

127 128
          def app
            Constraints.new(
129
              to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
130 131
              blocks,
              @set.request_class
132
            )
133 134
          end

135 136 137
          def conditions
            { :path_info => @path }.merge(constraints).merge(request_method_condition)
          end
J
Joshua Peek 已提交
138

139
          def requirements
140
            @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
141 142 143 144
              requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
              @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
            end
          end
145

146
          def defaults
147 148 149 150 151 152
            @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

153
          def default_controller_and_action(to_shorthand=nil)
154
            if to.respond_to?(:call)
155 156
              { }
            else
157
              if to.is_a?(String)
158
                controller, action = to.split('#')
159 160
              elsif to.is_a?(Symbol)
                action = to.to_s
161
              end
J
Joshua Peek 已提交
162

163 164
              controller ||= default_controller
              action     ||= default_action
165

166 167 168
              unless controller.is_a?(Regexp) || to_shorthand
                controller = [@scope[:module], controller].compact.join("/").presence
              end
169

170 171 172 173
              if controller.is_a?(String) && controller =~ %r{\A/}
                raise ArgumentError, "controller name should not start with a slash"
              end

174 175
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
176

177
              if controller.blank? && segment_keys.exclude?("controller")
178 179
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
180

181
              if action.blank? && segment_keys.exclude?("action")
182 183
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
184

A
Aaron Patterson 已提交
185
              hash = {}
A
Aaron Patterson 已提交
186 187
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
188
              hash
189 190
            end
          end
191

192
          def blocks
193 194 195 196 197
            constraints = @options[:constraints]
            if constraints.present? && !constraints.is_a?(Hash)
              [constraints]
            else
              @scope[:blocks] || []
198 199
            end
          end
J
Joshua Peek 已提交
200

201 202 203
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
204

205 206
          def request_method_condition
            if via = @options[:via]
207 208
              list = Array(via).map { |m| m.to_s.dasherize.upcase }
              { :request_method => list }
209 210
            else
              { }
211
            end
212
          end
J
Joshua Peek 已提交
213

214 215
          def segment_keys
            @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
216 217
              Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
            ).names
218
          end
219

220 221 222
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
223

224
          def default_controller
225
            if @options[:controller]
226
              @options[:controller]
227
            elsif @scope[:controller]
228
              @scope[:controller]
229
            end
230
          end
231 232 233

          def default_action
            if @options[:action]
234
              @options[:action]
235 236
            elsif @scope[:action]
              @scope[:action]
237 238
            end
          end
239
      end
240

241
      # Invokes Rack::Mount::Utils.normalize path and ensure that
242 243
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
244 245
      def self.normalize_path(path)
        path = Rack::Mount::Utils.normalize_path(path)
246
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$}
247 248 249
        path
      end

250 251 252 253
      def self.normalize_name(name)
        normalize_path(name)[1..-1].gsub("/", "_")
      end

254
      module Base
255 256 257 258
        # You can specify what Rails should route "/" to with the root method:
        #
        #   root :to => 'pages#main'
        #
259
        # For options, see +match+, as +root+ uses it internally.
260
        #
261 262 263
        # 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.
264 265 266
        def root(options = {})
          match '/', options.reverse_merge(:as => :root)
        end
267

268 269 270
        # Matches a url pattern to one or more routes. Any symbols in a pattern
        # are interpreted as url query parameters and thus available as +params+
        # in an action:
271
        #
272
        #   # sets :controller, :action and :id in params
273
        #   match ':controller/:action/:id'
274
        #
275 276 277 278 279 280 281 282 283 284 285 286
        # Two of these symbols are special, +:controller+ maps to the controller
        # and +:action+ to the controller's action. A pattern can also map
        # wildcard segments (globs) to params:
        #
        #   match 'songs/*category/:title' => 'songs#show'
        #
        #   # 'songs/rock/classic/stairway-to-heaven' sets
        #   #  params[:category] = 'rock/classic'
        #   #  params[:title] = 'stairway-to-heaven'
        #
        # When a pattern points to an internal route, the route's +:action+ and
        # +:controller+ should be set in options or hash shorthand. Examples:
287 288 289 290
        #
        #   match 'photos/:id' => 'photos#show'
        #   match 'photos/:id', :to => 'photos#show'
        #   match 'photos/:id', :controller => 'photos', :action => 'show'
291
        #
292 293 294 295 296 297 298 299
        # A pattern can also point to a +Rack+ endpoint i.e. anything that
        # responds to +call+:
        #
        #   match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon" }
        #   match 'photos/:id' => PhotoRackApp
        #   # Yes, controller actions are just rack endpoints
        #   match 'photos/:id' => PhotosController.action(:show)
        #
300
        # === Options
301
        #
302
        # Any options not seen here are passed on as params with the url.
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
        #
        # [: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]
331 332
        #   Points to a +Rack+ endpoint. Can be an object that responds to
        #   +call+ or a string representing a controller's action.
333
        #
334 335 336
        #      match 'path', :to => 'controller#action'
        #      match 'path', :to => lambda { [200, {}, "Success!"] }
        #      match 'path', :to => RackApp
337 338 339
        #
        # [:on]
        #   Shorthand for wrapping routes in a specific RESTful context. Valid
340
        #   values are +:member+, +:collection+, and +:new+. Only use within
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
        #   <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
357
        #   object that responds to <tt>matches?</tt>
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
        #
        #     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.
376 377
        #
        # [:anchor]
378
        #   Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
379 380 381 382
        #   false, the pattern matches any request prefixed with the given path.
        #
        #     # Matches any request starting with 'path'
        #     match 'path' => 'c#a', :anchor => false
383
        def match(path, options=nil)
384 385 386
          mapping = Mapping.new(@set, @scope, path, options || {})
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
387 388
          self
        end
389

390 391
        # Mount a Rack-based application to be used within the application.
        #
R
Ryan Bigg 已提交
392
        #   mount SomeRackApp, :at => "some_route"
393 394 395
        #
        # Alternatively:
        #
R
Ryan Bigg 已提交
396
        #   mount(SomeRackApp => "some_route")
397
        #
398 399
        # For options, see +match+, as +mount+ uses it internally.
        #
400 401 402 403 404
        # 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 已提交
405
        #   mount(SomeRackApp => "some_route", :as => "exciting")
406 407 408
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
409 410 411 412 413 414 415 416 417 418 419
        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

420 421
          options[:as] ||= app_name(app)

422
          match(path, options.merge(:to => app, :anchor => false, :format => false))
423 424

          define_generate_prefix(app, options[:as])
425 426 427
          self
        end

428 429 430 431
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
432

433 434 435 436 437 438
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

439 440 441
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
442 443 444 445 446 447 448

            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
449 450 451
          end

          def define_generate_prefix(app, name)
452
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
453 454

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
455 456
            _routes = @set
            app.routes.define_mounted_helper(name)
457 458
            app.routes.class_eval do
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
459
                prefix_options = options.slice(*_route.segment_keys)
460 461
                # 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 已提交
462
                _routes.url_helpers.send("#{name}_path", prefix_options)
463 464 465
              end
            end
          end
466 467 468
      end

      module HttpHelpers
469
        # Define a route that only recognizes HTTP GET.
470
        # For supported arguments, see <tt>Base#match</tt>.
471 472 473 474
        #
        # Example:
        #
        # get 'bacon', :to => 'food#bacon'
475 476 477 478
        def get(*args, &block)
          map_method(:get, *args, &block)
        end

479
        # Define a route that only recognizes HTTP POST.
480
        # For supported arguments, see <tt>Base#match</tt>.
481 482 483 484
        #
        # Example:
        #
        # post 'bacon', :to => 'food#bacon'
485 486 487 488
        def post(*args, &block)
          map_method(:post, *args, &block)
        end

489
        # Define a route that only recognizes HTTP PUT.
490
        # For supported arguments, see <tt>Base#match</tt>.
491 492 493 494
        #
        # Example:
        #
        # put 'bacon', :to => 'food#bacon'
495 496 497 498
        def put(*args, &block)
          map_method(:put, *args, &block)
        end

499
        # Define a route that only recognizes HTTP PUT.
500
        # For supported arguments, see <tt>Base#match</tt>.
501 502 503 504
        #
        # Example:
        #
        # delete 'broccoli', :to => 'food#broccoli'
505 506 507 508 509 510 511 512 513 514 515 516 517 518
        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

519 520 521
      # 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
S
Sebastian Martinez 已提交
522 523
      # the <tt>app/controllers/admin</tt> directory, and you can group them
      # together in your router:
524 525 526 527
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
528
      #
529
      # This will create a number of routes for each of the posts and comments
S
Sebastian Martinez 已提交
530
      # controller. For <tt>Admin::PostsController</tt>, Rails will create:
531
      #
532 533 534 535 536 537 538
      #   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
539
      #
540
      # If you want to route /posts (without the prefix /admin) to
S
Sebastian Martinez 已提交
541
      # <tt>Admin::PostsController</tt>, you could use
542
      #
543
      #   scope :module => "admin" do
544
      #     resources :posts
545 546 547
      #   end
      #
      # or, for a single case
548
      #
549
      #   resources :posts, :module => "admin"
550
      #
S
Sebastian Martinez 已提交
551
      # If you want to route /admin/posts to +PostsController+
552
      # (without the Admin:: module prefix), you could use
553
      #
554
      #   scope "/admin" do
555
      #     resources :posts
556 557 558
      #   end
      #
      # or, for a single case
559
      #
560
      #   resources :posts, :path => "/admin/posts"
561 562 563
      #
      # 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
S
Sebastian Martinez 已提交
564
      # +PostsController+:
565
      #
566 567 568 569 570 571 572
      #   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
573
      module Scoping
574
        # Scopes a set of routes to the given default options.
575 576 577 578 579 580 581 582
        #
        # 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.
583 584
        # The difference here being that the routes generated are like /:account_id/projects,
        # rather than /accounts/:account_id/projects.
585
        #
586
        # === Options
587
        #
588
        # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
589
        #
590
        # === Examples
591
        #
S
Sebastian Martinez 已提交
592
        #   # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
593 594 595
        #   scope :module => "admin" do
        #     resources :posts
        #   end
596
        #
597 598 599 600
        #   # prefix the posts resource's requests with '/admin'
        #   scope :path => "/admin" do
        #     resources :posts
        #   end
601
        #
S
Sebastian Martinez 已提交
602
        #   # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
603 604 605
        #   scope :as => "sekret" do
        #     resources :posts
        #   end
606 607
        def scope(*args)
          options = args.extract_options!
608
          options = options.dup
609

610
          options[:path] = args.first if args.first.is_a?(String)
611
          recover = {}
612

613 614 615
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
            block, options[:constraints] = options[:constraints], {}
616
          end
617

618 619 620 621 622
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
623 624
          end

625 626
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
627

628 629
          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)
630 631 632 633

          yield
          self
        ensure
634 635 636 637 638 639
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
640 641
        end

642 643 644 645 646 647
        # Scopes routes to a specific controller
        #
        # Example:
        #   controller "food" do
        #     match "bacon", :action => "bacon"
        #   end
648 649 650
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
651 652
        end

653 654 655 656 657 658 659 660
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
661 662 663 664 665 666 667
        #       admin_posts GET    /admin/posts(.:format)          admin/posts#index
        #       admin_posts POST   /admin/posts(.:format)          admin/posts#create
        #    new_admin_post GET    /admin/posts/new(.:format)      admin/posts#new
        #   edit_admin_post GET    /admin/posts/:id/edit(.:format) admin/posts#edit
        #        admin_post GET    /admin/posts/:id(.:format)      admin/posts#show
        #        admin_post PUT    /admin/posts/:id(.:format)      admin/posts#update
        #        admin_post DELETE /admin/posts/:id(.:format)      admin/posts#destroy
668
        #
669
        # === Options
670
        #
671 672
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
        # options all default to the name of the namespace.
673
        #
674 675
        # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
        # <tt>Resources#resources</tt>.
676
        #
677
        # === Examples
678
        #
679 680 681 682
        #   # accessible through /sekret/posts rather than /admin/posts
        #   namespace :admin, :path => "sekret" do
        #     resources :posts
        #   end
683
        #
S
Sebastian Martinez 已提交
684
        #   # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
685 686 687
        #   namespace :admin, :module => "sekret" do
        #     resources :posts
        #   end
688
        #
S
Sebastian Martinez 已提交
689
        #   # generates +sekret_posts_path+ rather than +admin_posts_path+
690 691 692
        #   namespace :admin, :as => "sekret" do
        #     resources :posts
        #   end
693
        def namespace(path, options = {})
694
          path = path.to_s
695 696 697
          options = { :path => path, :as => path, :module => path,
                      :shallow_path => path, :shallow_prefix => path }.merge!(options)
          scope(options) { yield }
698
        end
699

R
Ryan Bigg 已提交
700 701 702 703 704 705 706 707 708 709
        # === 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.
710
        #
R
R.T. Lechow 已提交
711
        # You may use this to also restrict other parameters:
R
Ryan Bigg 已提交
712 713 714 715 716
        #
        #   resources :posts do
        #     constraints(:post_id => /\d+\.\d+) do
        #       resources :comments
        #     end
J
James Miller 已提交
717
        #   end
R
Ryan Bigg 已提交
718 719 720 721 722 723 724 725 726 727 728 729 730 731
        #
        # === 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
        #
R
R.T. Lechow 已提交
732
        # Requests to routes can be constrained based on specific criteria:
R
Ryan Bigg 已提交
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754
        #
        #    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
755 756 757 758
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
759
        # Allows you to set default parameters for a route, such as this:
760 761 762
        #   defaults :id => 'home' do
        #     match 'scoped_pages/(:id)', :to => 'pages#show'
        #   end
R
Ryan Bigg 已提交
763
        # Using this, the +:id+ parameter here will default to 'home'.
764 765 766 767
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

768
        private
J
José Valim 已提交
769
          def scope_options #:nodoc:
770 771 772
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

J
José Valim 已提交
773
          def merge_path_scope(parent, child) #:nodoc:
774
            Mapper.normalize_path("#{parent}/#{child}")
775 776
          end

J
José Valim 已提交
777
          def merge_shallow_path_scope(parent, child) #:nodoc:
778 779 780
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
781
          def merge_as_scope(parent, child) #:nodoc:
782
            parent ? "#{parent}_#{child}" : child
783 784
          end

J
José Valim 已提交
785
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
786 787 788
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
789
          def merge_module_scope(parent, child) #:nodoc:
790 791 792
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
793
          def merge_controller_scope(parent, child) #:nodoc:
794
            child
795 796
          end

J
José Valim 已提交
797
          def merge_path_names_scope(parent, child) #:nodoc:
798 799 800
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
801
          def merge_constraints_scope(parent, child) #:nodoc:
802 803 804
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
805
          def merge_defaults_scope(parent, child) #:nodoc:
806 807 808
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
809
          def merge_blocks_scope(parent, child) #:nodoc:
810 811 812
            merged = parent ? parent.dup : []
            merged << child if child
            merged
813 814
          end

J
José Valim 已提交
815
          def merge_options_scope(parent, child) #:nodoc:
816
            (parent || {}).except(*override_keys(child)).merge(child)
817
          end
818

J
José Valim 已提交
819
          def merge_shallow_scope(parent, child) #:nodoc:
820 821
            child ? true : false
          end
822

J
José Valim 已提交
823
          def override_keys(child) #:nodoc:
824 825
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
826 827
      end

828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851
      # 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
S
Sebastian Martinez 已提交
852 853
      # <tt>app/controllers/admin</tt> directory, and you can group them together
      # in your router:
854 855 856 857 858
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
S
Sebastian Martinez 已提交
859 860
      # By default the +:id+ parameter doesn't accept dots. If you need to
      # use dots as part of the +:id+ parameter add a constraint which
861 862 863 864
      # overrides this restriction, e.g:
      #
      #   resources :articles, :id => /[^\/]+/
      #
S
Sebastian Martinez 已提交
865
      # This allows any character other than a slash as part of your +:id+.
866
      #
J
Joshua Peek 已提交
867
      module Resources
868 869
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
870 871 872
        VALID_ON_OPTIONS  = [:new, :collection, :member]
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except]
        CANONICAL_ACTIONS = %w(index create new show update destroy)
873

874
        class Resource #:nodoc:
875
          DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit]
876

877
          attr_reader :controller, :path, :options
878 879

          def initialize(entities, options = {})
880
            @name       = entities.to_s
881
            @path       = (options.delete(:path) || @name).to_s
882
            @controller = (options.delete(:controller) || @name).to_s
883
            @as         = options.delete(:as)
884
            @options    = options
885 886
          end

887
          def default_actions
888
            self.class::DEFAULT_ACTIONS
889 890
          end

891
          def actions
892
            if only = @options[:only]
893
              Array(only).map(&:to_sym)
894
            elsif except = @options[:except]
895 896 897 898 899 900
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

901
          def name
902
            @as || @name
903 904
          end

905
          def plural
906
            @plural ||= name.to_s
907 908 909
          end

          def singular
910
            @singular ||= name.to_s.singularize
911 912
          end

913
          alias :member_name :singular
914

915
          # Checks for uncountable plurals, and appends "_index" if the plural
916
          # and singular form are the same.
917
          def collection_name
918
            singular == plural ? "#{plural}_index" : plural
919 920
          end

921
          def resource_scope
922
            { :controller => controller }
923 924
          end

925
          alias :collection_scope :path
926 927

          def member_scope
928
            "#{path}/:id"
929 930
          end

931
          def new_scope(new_path)
932
            "#{path}/#{new_path}"
933 934 935
          end

          def nested_scope
936
            "#{path}/:#{singular}_id"
937
          end
938

939 940 941
        end

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

944
          def initialize(entities, options)
945
            @as         = nil
946
            @name       = entities.to_s
947
            @path       = (options.delete(:path) || @name).to_s
948
            @controller = (options.delete(:controller) || plural).to_s
949 950 951 952
            @as         = options.delete(:as)
            @options    = options
          end

953 954
          def plural
            @plural ||= name.to_s.pluralize
955 956
          end

957 958
          def singular
            @singular ||= name.to_s
959
          end
960 961 962 963 964 965

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
966 967
        end

968 969 970 971
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

972 973 974 975 976 977 978 979 980
        # 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
S
Sebastian Martinez 已提交
981
        # the +GeoCoders+ controller (note that the controller is named after
982 983 984 985 986 987 988 989
        # the plural):
        #
        #   GET     /geocoder/new
        #   POST    /geocoder
        #   GET     /geocoder
        #   GET     /geocoder/edit
        #   PUT     /geocoder
        #   DELETE  /geocoder
990
        #
991
        # === Options
992
        # Takes same options as +resources+.
J
Joshua Peek 已提交
993
        def resource(*resources, &block)
J
Joshua Peek 已提交
994
          options = resources.extract_options!
J
Joshua Peek 已提交
995

996
          if apply_common_behavior_for(:resource, resources, options, &block)
997 998 999
            return self
          end

1000 1001
          resource_scope(SingletonResource.new(resources.pop, options)) do
            yield if block_given?
1002

1003
            collection do
1004
              post :create
1005
            end if parent_resource.actions.include?(:create)
1006

1007
            new do
1008
              get :new
1009
            end if parent_resource.actions.include?(:new)
1010

1011
            member do
1012
              get    :edit if parent_resource.actions.include?(:edit)
1013 1014 1015
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
1016 1017 1018
            end
          end

J
Joshua Peek 已提交
1019
          self
1020 1021
        end

1022 1023 1024 1025 1026 1027 1028 1029
        # 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
S
Sebastian Martinez 已提交
1030
        # the +Photos+ controller:
1031 1032 1033 1034 1035 1036 1037
        #
        #   GET     /photos/new
        #   POST    /photos
        #   GET     /photos/:id
        #   GET     /photos/:id/edit
        #   PUT     /photos/:id
        #   DELETE  /photos/:id
1038
        #
1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053
        # 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
        #
1054
        # === Options
1055 1056
        # Takes same options as <tt>Base#match</tt> as well as:
        #
1057 1058 1059 1060 1061 1062 1063
        # [: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
1064
        #
1065 1066
        # [:only]
        #   Only generate routes for the given actions.
1067
        #
1068 1069
        #     resources :cows, :only => :show
        #     resources :cows, :only => [:show, :index]
1070
        #
1071 1072
        # [:except]
        #   Generate all routes except for the given actions.
1073
        #
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087
        #     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
1088
        #       resources :comments, :except => [:show, :edit, :update, :destroy]
1089
        #     end
1090 1091 1092 1093 1094
        #     resources :comments, :only => [:show, :edit, :update, :destroy]
        #
        #   This allows URLs for resources that otherwise would be deeply nested such
        #   as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
        #   to be shortened to just <tt>/comments/1234</tt>.
1095 1096 1097 1098
        #
        # [:shallow_path]
        #   Prefixes nested shallow routes with the specified path.
        #
1099 1100 1101 1102
        #     scope :shallow_path => "sekret" do
        #       resources :posts do
        #         resources :comments, :shallow => true
        #       end
1103 1104 1105 1106
        #     end
        #
        #   The +comments+ resource here will have the following routes generated for it:
        #
G
ganesh 已提交
1107 1108 1109
        #     post_comments    GET    /posts/:post_id/comments(.:format)
        #     post_comments    POST   /posts/:post_id/comments(.:format)
        #     new_post_comment GET    /posts/:post_id/comments/new(.:format)
1110 1111 1112 1113 1114 1115
        #     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
1116
        #
S
Sebastian Martinez 已提交
1117
        #   # routes call <tt>Admin::PostsController</tt>
1118
        #   resources :posts, :module => "admin"
1119
        #
1120
        #   # resource actions are at /admin/posts.
1121
        #   resources :posts, :path => "admin/posts"
J
Joshua Peek 已提交
1122
        def resources(*resources, &block)
J
Joshua Peek 已提交
1123
          options = resources.extract_options!
1124

1125
          if apply_common_behavior_for(:resources, resources, options, &block)
1126 1127 1128
            return self
          end

1129
          resource_scope(Resource.new(resources.pop, options)) do
1130
            yield if block_given?
J
Joshua Peek 已提交
1131

1132
            collection do
1133 1134
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1135
            end
1136

1137
            new do
1138
              get :new
1139
            end if parent_resource.actions.include?(:new)
1140

1141
            member do
1142
              get    :edit if parent_resource.actions.include?(:edit)
1143 1144 1145
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
1146 1147 1148
            end
          end

J
Joshua Peek 已提交
1149
          self
1150 1151
        end

1152 1153 1154 1155 1156 1157 1158 1159 1160
        # 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>
S
Sebastian Martinez 已提交
1161
        # with GET, and route to the search action of +PhotosController+. It will also
1162 1163
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1164
        def collection
1165 1166
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1167 1168
          end

1169 1170 1171 1172
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1173
          end
1174
        end
J
Joshua Peek 已提交
1175

1176 1177 1178 1179 1180 1181 1182 1183 1184
        # 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
S
Sebastian Martinez 已提交
1185
        # preview action of +PhotosController+. It will also create the
1186
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1187
        def member
1188 1189
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1190
          end
J
Joshua Peek 已提交
1191

1192 1193 1194 1195
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1196 1197 1198 1199 1200 1201 1202
          end
        end

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

1204 1205 1206 1207
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1208
          end
J
Joshua Peek 已提交
1209 1210
        end

1211
        def nested
1212 1213
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1214 1215 1216
          end

          with_scope_level(:nested) do
1217
            if shallow?
1218
              with_exclusive_scope do
1219
                if @scope[:shallow_path].blank?
1220
                  scope(parent_resource.nested_scope, nested_options) { yield }
1221
                else
1222
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1223
                    scope(parent_resource.nested_scope, nested_options) { yield }
1224 1225 1226 1227
                  end
                end
              end
            else
1228
              scope(parent_resource.nested_scope, nested_options) { yield }
1229 1230 1231 1232
            end
          end
        end

1233
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1234
        def namespace(path, options = {})
1235
          if resource_scope?
1236 1237 1238 1239 1240 1241
            nested { super }
          else
            super
          end
        end

1242
        def shallow
1243
          scope(:shallow => true, :shallow_path => @scope[:path]) do
1244 1245 1246 1247
            yield
          end
        end

1248 1249 1250 1251
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

J
Joshua Peek 已提交
1252
        def match(*args)
1253
          options = args.extract_options!.dup
1254 1255
          options[:anchor] = true unless options.key?(:anchor)

1256
          if args.length > 1
1257
            args.each { |path| match(path, options.dup) }
1258 1259 1260
            return self
          end

1261 1262
          on = options.delete(:on)
          if VALID_ON_OPTIONS.include?(on)
1263
            args.push(options)
1264 1265 1266
            return send(on){ match(*args) }
          elsif on
            raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1267 1268
          end

1269 1270 1271 1272
          if @scope[:scope_level] == :resources
            args.push(options)
            return nested { match(*args) }
          elsif @scope[:scope_level] == :resource
1273
            args.push(options)
J
Joshua Peek 已提交
1274 1275
            return member { match(*args) }
          end
J
Joshua Peek 已提交
1276

1277
          action = args.first
1278
          path = path_for_action(action, options.delete(:path))
1279

1280 1281 1282
          if action.to_s =~ /^[\w\/]+$/
            options[:action] ||= action unless action.to_s.include?("/")
          else
1283 1284 1285 1286 1287 1288 1289
            action = nil
          end

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

1292
          super(path, options)
J
Joshua Peek 已提交
1293 1294
        end

1295
        def root(options={})
1296
          if @scope[:scope_level] == :resources
1297 1298
            with_scope_level(:root) do
              scope(parent_resource.path) do
1299 1300 1301 1302 1303 1304
                super(options)
              end
            end
          else
            super(options)
          end
1305 1306
        end

1307
        protected
1308

1309
          def parent_resource #:nodoc:
1310 1311 1312
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1313
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1314 1315 1316 1317 1318
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1319 1320 1321 1322 1323
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1324
            options.keys.each do |k|
1325 1326 1327
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1328 1329 1330
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1331 1332 1333 1334 1335
                send(method, resources.pop, options, &block)
              end
              return true
            end

1336 1337 1338 1339
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1340 1341 1342
            false
          end

J
José Valim 已提交
1343
          def action_options?(options) #:nodoc:
1344 1345 1346
            options[:only] || options[:except]
          end

J
José Valim 已提交
1347
          def scope_action_options? #:nodoc:
1348 1349 1350
            @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except])
          end

J
José Valim 已提交
1351
          def scope_action_options #:nodoc:
1352 1353 1354
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1355
          def resource_scope? #:nodoc:
1356
            @scope[:scope_level].in?([:resource, :resources])
1357 1358
          end

J
José Valim 已提交
1359
          def resource_method_scope? #:nodoc:
1360
            @scope[:scope_level].in?([:collection, :member, :new])
1361 1362
          end

1363
          def with_exclusive_scope
1364
            begin
1365 1366
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1367

1368 1369 1370
              with_scope_level(:exclusive) do
                yield
              end
1371
            ensure
1372
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1373 1374 1375
            end
          end

1376
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1377
            old, @scope[:scope_level] = @scope[:scope_level], kind
1378
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1379 1380 1381
            yield
          ensure
            @scope[:scope_level] = old
1382
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1383
          end
1384

J
José Valim 已提交
1385
          def resource_scope(resource) #:nodoc:
1386
            with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do
1387
              scope(parent_resource.resource_scope) do
1388 1389 1390 1391 1392
                yield
              end
            end
          end

J
José Valim 已提交
1393
          def nested_options #:nodoc:
1394 1395 1396 1397 1398 1399
            {}.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 已提交
1400
          def id_constraint? #:nodoc:
1401
            @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
1402 1403
          end

J
José Valim 已提交
1404
          def id_constraint #:nodoc:
1405
            @scope[:constraints][:id]
1406 1407
          end

J
José Valim 已提交
1408
          def canonical_action?(action, flag) #:nodoc:
1409
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1410 1411
          end

J
José Valim 已提交
1412
          def shallow_scoping? #:nodoc:
1413
            shallow? && @scope[:scope_level] == :member
1414 1415
          end

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

1420 1421
            path = if canonical_action?(action, path.blank?)
              prefix.to_s
1422
            else
1423
              "#{prefix}/#{action_path(action, path)}"
1424 1425 1426
            end
          end

J
José Valim 已提交
1427
          def action_path(name, path = nil) #:nodoc:
1428 1429 1430
            # Ruby 1.8 can't transform empty strings to symbols
            name = name.to_sym if name.is_a?(String) && !name.empty?
            path || @scope[:path_names][name] || name.to_s
1431 1432
          end

J
José Valim 已提交
1433
          def prefix_name_for_action(as, action) #:nodoc:
1434
            if as
1435
              as.to_s
1436
            elsif !canonical_action?(action, @scope[:scope_level])
1437
              action.to_s
1438
            end
1439 1440
          end

J
José Valim 已提交
1441
          def name_for_action(as, action) #:nodoc:
1442
            prefix = prefix_name_for_action(as, action)
1443
            prefix = Mapper.normalize_name(prefix) if prefix
1444 1445 1446
            name_prefix = @scope[:as]

            if parent_resource
1447 1448
              return nil if as.nil? && action.nil?

1449 1450
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1451
            end
1452

1453
            name = case @scope[:scope_level]
1454
            when :nested
1455
              [name_prefix, prefix]
1456
            when :collection
1457
              [prefix, name_prefix, collection_name]
1458
            when :new
1459 1460 1461 1462 1463
              [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]
1464
            else
1465
              [name_prefix, member_name, prefix]
1466
            end
1467

1468
            candidate = name.select(&:present?).join("_").presence
1469
            candidate unless as.nil? && @set.routes.find { |r| r.name == candidate }
1470
          end
J
Joshua Peek 已提交
1471
      end
J
Joshua Peek 已提交
1472

J
José Valim 已提交
1473
      module Shorthand #:nodoc:
1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485
        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

1486 1487 1488 1489 1490
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
      end

1491 1492
      include Base
      include HttpHelpers
1493
      include Redirection
1494 1495
      include Scoping
      include Resources
1496
      include Shorthand
J
Joshua Peek 已提交
1497 1498
    end
  end
J
Joshua Peek 已提交
1499
end