mapper.rb 53.5 KB
Newer Older
1
require 'active_support/core_ext/hash/except'
B
Bogdan Gusiev 已提交
2
require 'active_support/core_ext/hash/reverse_merge'
3
require 'active_support/core_ext/object/blank'
S
Santiago Pastorino 已提交
4
require 'active_support/core_ext/enumerable'
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
        attr_reader :app, :constraints
21

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
          return true
38 39
        ensure
          req.reset_parameters
40 41 42 43
        end

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

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

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

58
        def initialize(set, scope, path, options)
59
          @set, @scope = set, scope
60
          @segment_keys = nil
61
          @options = (@scope[:options] || {}).merge(options)
62
          @path = normalize_path(path)
63
          normalize_options!
64 65 66 67 68 69 70 71 72 73

          via_all = @options.delete(:via) if @options[:via] == :all

          if !via_all && request_method_condition.empty?
            msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
                  "If you want to expose your action to GET, use `get` in the router:\n\n" \
                  "  Instead of: match \"controller#action\"\n" \
                  "  Do: get \"controller#action\""
            raise msg
          end
74
        end
J
Joshua Peek 已提交
75

76
        def to_route
77
          [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
78
        end
J
Joshua Peek 已提交
79

80
        private
81 82 83

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

85 86
            if using_match_shorthand?(path_without_format, @options)
              to_shorthand    = @options[:to].blank?
87
              @options[:to] ||= path_without_format.gsub(/\(.*\)/, "")[1..-1].sub(%r{/([^/]*)$}, '#\1')
88 89
            end

90
            @options.merge!(default_controller_and_action(to_shorthand))
91 92 93 94 95

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

96
              if requirement.source =~ ANCHOR_CHARACTERS_REGEX
97 98 99 100 101 102
                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
103
          end
104

105
          # match "account/overview"
106
          def using_match_shorthand?(path, options)
107
            path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX
108
          end
109

110
          def normalize_path(path)
111 112
            raise ArgumentError, "path is required" if path.blank?
            path = Mapper.normalize_path(path)
113 114 115 116 117 118 119 120

            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' }
121
              @options[:controller] ||= /.+?/
122 123
            end

124 125
            # Add a constraint for wildcard route to make it non-greedy and match the
            # optional format part of the route by default
126
            if path.match(WILDCARD_PATH) && @options[:format] != false
127
              @options[$1.to_sym] ||= /.+?/
128 129
            end

130 131 132
            if @options[:format] == false
              @options.delete(:format)
              path
133
            elsif path.include?(":format") || path.end_with?('/')
134
              path
135 136
            elsif @options[:format] == true
              "#{path}.:format"
137 138 139
            else
              "#{path}(.:format)"
            end
140
          end
141

142 143
          def app
            Constraints.new(
144
              to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
145 146
              blocks,
              @set.request_class
147
            )
148 149
          end

150 151 152
          def conditions
            { :path_info => @path }.merge(constraints).merge(request_method_condition)
          end
J
Joshua Peek 已提交
153

154
          def requirements
155
            @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
156 157 158 159
              requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
              @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
            end
          end
160

161
          def defaults
162 163 164 165 166 167
            @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

168
          def default_controller_and_action(to_shorthand=nil)
169
            if to.respond_to?(:call)
170 171
              { }
            else
172
              if to.is_a?(String)
173
                controller, action = to.split('#')
174 175
              elsif to.is_a?(Symbol)
                action = to.to_s
176
              end
J
Joshua Peek 已提交
177

178 179
              controller ||= default_controller
              action     ||= default_action
180

181 182 183
              unless controller.is_a?(Regexp) || to_shorthand
                controller = [@scope[:module], controller].compact.join("/").presence
              end
184

185 186 187 188
              if controller.is_a?(String) && controller =~ %r{\A/}
                raise ArgumentError, "controller name should not start with a slash"
              end

189 190
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
191

192
              if controller.blank? && segment_keys.exclude?("controller")
193 194
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
195

196
              if action.blank? && segment_keys.exclude?("action")
197 198
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
199

A
Aaron Patterson 已提交
200
              hash = {}
A
Aaron Patterson 已提交
201 202
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
203
              hash
204 205
            end
          end
206

207
          def blocks
208 209 210 211 212
            constraints = @options[:constraints]
            if constraints.present? && !constraints.is_a?(Hash)
              [constraints]
            else
              @scope[:blocks] || []
213 214
            end
          end
J
Joshua Peek 已提交
215

216 217 218
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
219

220 221
          def request_method_condition
            if via = @options[:via]
222 223
              list = Array(via).map { |m| m.to_s.dasherize.upcase }
              { :request_method => list }
224 225
            else
              { }
226
            end
227
          end
J
Joshua Peek 已提交
228

229
          def segment_keys
230 231 232
            return @segment_keys if @segment_keys

            @segment_keys = Journey::Path::Pattern.new(
233
              Journey::Router::Strexp.compile(@path, requirements, SEPARATORS)
234
            ).names
235
          end
236

237 238 239
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
240

241
          def default_controller
242
            @options[:controller] || @scope[:controller]
243
          end
244 245

          def default_action
246
            @options[:action] || @scope[:action]
247
          end
248
      end
249

250
      # Invokes Rack::Mount::Utils.normalize path and ensure that
251 252
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
253
      def self.normalize_path(path)
254
        path = Journey::Router::Utils.normalize_path(path)
255
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$}
256 257 258
        path
      end

259
      def self.normalize_name(name)
260
        normalize_path(name)[1..-1].tr("/", "_")
261 262
      end

263
      module Base
264 265 266 267
        # You can specify what Rails should route "/" to with the root method:
        #
        #   root :to => 'pages#main'
        #
268
        # For options, see +match+, as +root+ uses it internally.
269
        #
B
Brian Cardarella 已提交
270 271 272 273
        # You can also pass a string which will expand
        #
        #   root 'pages#main'
        #
274 275 276
        # 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.
277
        def root(options = {})
B
Brian Cardarella 已提交
278
          options = { :to => options } if options.is_a?(String)
279
          match '/', { :as => :root, :via => :get }.merge(options)
280
        end
281

282 283 284
        # 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:
285
        #
286
        #   # sets :controller, :action and :id in params
287
        #   match ':controller/:action/:id'
288
        #
289 290 291 292 293 294 295 296 297 298 299 300
        # 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:
301 302 303 304
        #
        #   match 'photos/:id' => 'photos#show'
        #   match 'photos/:id', :to => 'photos#show'
        #   match 'photos/:id', :controller => 'photos', :action => 'show'
305
        #
306 307 308
        # A pattern can also point to a +Rack+ endpoint i.e. anything that
        # responds to +call+:
        #
A
Alexey Vakhov 已提交
309
        #   match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon"] }
310 311 312 313
        #   match 'photos/:id' => PhotoRackApp
        #   # Yes, controller actions are just rack endpoints
        #   match 'photos/:id' => PhotosController.action(:show)
        #
314
        # === Options
315
        #
316
        # Any options not seen here are passed on as params with the url.
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
        #
        # [: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]
345 346
        #   Points to a +Rack+ endpoint. Can be an object that responds to
        #   +call+ or a string representing a controller's action.
347
        #
348
        #      match 'path', :to => 'controller#action'
J
Justin Woodbridge 已提交
349
        #      match 'path', :to => lambda { |env| [200, {}, "Success!"] }
350
        #      match 'path', :to => RackApp
351 352 353
        #
        # [:on]
        #   Shorthand for wrapping routes in a specific RESTful context. Valid
354
        #   values are +:member+, +:collection+, and +:new+. Only use within
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
        #   <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
371
        #   object that responds to <tt>matches?</tt>
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
        #
        #     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.
390 391
        #
        # [:anchor]
392
        #   Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
393 394 395 396
        #   false, the pattern matches any request prefixed with the given path.
        #
        #     # Matches any request starting with 'path'
        #     match 'path' => 'c#a', :anchor => false
397
        def match(path, options=nil)
398
        end
399

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

430 431
          options[:as] ||= app_name(app)

432
          match(path, options.merge(:to => app, :anchor => false, :format => false, :via => :all))
433 434

          define_generate_prefix(app, options[:as])
435 436 437
          self
        end

438 439 440 441
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
442

443 444 445 446 447 448
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

449 450 451
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
452 453 454 455 456

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
457
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
458
            end
459 460 461
          end

          def define_generate_prefix(app, name)
462
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
463 464

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
465 466
            _routes = @set
            app.routes.define_mounted_helper(name)
J
José Valim 已提交
467 468 469 470 471
            app.routes.singleton_class.class_eval do
              define_method :mounted? do
                true
              end

472
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
473
                prefix_options = options.slice(*_route.segment_keys)
474 475
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
R
rails-noob 已提交
476 477 478
                prefix = _routes.url_helpers.send("#{name}_path", prefix_options)
                prefix = '' if prefix == '/'
                prefix
479 480 481
              end
            end
          end
482 483 484
      end

      module HttpHelpers
485
        # Define a route that only recognizes HTTP GET.
486
        # For supported arguments, see <tt>Base#match</tt>.
487 488 489
        #
        # Example:
        #
490
        #   get 'bacon', :to => 'food#bacon'
491
        def get(*args, &block)
492
          map_method(:get, args, &block)
493 494
        end

495
        # Define a route that only recognizes HTTP POST.
496
        # For supported arguments, see <tt>Base#match</tt>.
497 498 499
        #
        # Example:
        #
500
        #   post 'bacon', :to => 'food#bacon'
501
        def post(*args, &block)
502
          map_method(:post, args, &block)
503 504
        end

505 506 507 508 509 510 511 512 513 514
        # Define a route that only recognizes HTTP PATCH.
        # For supported arguments, see <tt>Base#match</tt>.
        #
        # Example:
        #
        #   patch 'bacon', :to => 'food#bacon'
        def patch(*args, &block)
          map_method(:patch, args, &block)
        end

515
        # Define a route that only recognizes HTTP PUT.
516
        # For supported arguments, see <tt>Base#match</tt>.
517 518 519
        #
        # Example:
        #
520
        #   put 'bacon', :to => 'food#bacon'
521
        def put(*args, &block)
522
          map_method(:put, args, &block)
523 524
        end

525
        # Define a route that only recognizes HTTP DELETE.
526
        # For supported arguments, see <tt>Base#match</tt>.
527 528 529
        #
        # Example:
        #
530
        #   delete 'broccoli', :to => 'food#broccoli'
531
        def delete(*args, &block)
532
          map_method(:delete, args, &block)
533 534 535
        end

        private
536
          def map_method(method, args, &block)
537 538
            options = args.extract_options!
            options[:via] = method
539
            match(*args, options, &block)
540 541 542 543
            self
          end
      end

544 545 546
      # 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 已提交
547 548
      # the <tt>app/controllers/admin</tt> directory, and you can group them
      # together in your router:
549 550 551 552
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
553
      #
554
      # This will create a number of routes for each of the posts and comments
S
Sebastian Martinez 已提交
555
      # controller. For <tt>Admin::PostsController</tt>, Rails will create:
556
      #
557 558 559 560 561
      #   GET       /admin/posts
      #   GET       /admin/posts/new
      #   POST      /admin/posts
      #   GET       /admin/posts/1
      #   GET       /admin/posts/1/edit
562
      #   PATCH/PUT /admin/posts/1
563
      #   DELETE    /admin/posts/1
564
      #
565
      # If you want to route /posts (without the prefix /admin) to
S
Sebastian Martinez 已提交
566
      # <tt>Admin::PostsController</tt>, you could use
567
      #
568
      #   scope :module => "admin" do
569
      #     resources :posts
570 571 572
      #   end
      #
      # or, for a single case
573
      #
574
      #   resources :posts, :module => "admin"
575
      #
S
Sebastian Martinez 已提交
576
      # If you want to route /admin/posts to +PostsController+
577
      # (without the Admin:: module prefix), you could use
578
      #
579
      #   scope "/admin" do
580
      #     resources :posts
581 582 583
      #   end
      #
      # or, for a single case
584
      #
585
      #   resources :posts, :path => "/admin/posts"
586 587 588
      #
      # 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 已提交
589
      # +PostsController+:
590
      #
591 592 593 594 595
      #   GET       /admin/posts
      #   GET       /admin/posts/new
      #   POST      /admin/posts
      #   GET       /admin/posts/1
      #   GET       /admin/posts/1/edit
596
      #   PATCH/PUT /admin/posts/1
597
      #   DELETE    /admin/posts/1
598
      module Scoping
599
        # Scopes a set of routes to the given default options.
600 601 602 603 604 605 606 607
        #
        # 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.
608 609
        # The difference here being that the routes generated are like /:account_id/projects,
        # rather than /accounts/:account_id/projects.
610
        #
611
        # === Options
612
        #
613
        # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
614
        #
615
        # === Examples
616
        #
S
Sebastian Martinez 已提交
617
        #   # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
618 619 620
        #   scope :module => "admin" do
        #     resources :posts
        #   end
621
        #
622 623 624 625
        #   # prefix the posts resource's requests with '/admin'
        #   scope :path => "/admin" do
        #     resources :posts
        #   end
626
        #
S
Sebastian Martinez 已提交
627
        #   # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
628 629 630
        #   scope :as => "sekret" do
        #     resources :posts
        #   end
631 632
        def scope(*args)
          options = args.extract_options!
633
          options = options.dup
634

635
          options[:path] = args.first if args.first.is_a?(String)
636
          recover = {}
637

638 639
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
640
            block, options[:constraints] = options[:constraints], {}
641
          end
642

643 644 645 646 647
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
648 649
          end

650 651 652 653 654 655
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)

          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)

656 657 658
          yield
          self
        ensure
659 660 661
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end
662 663 664

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
665 666
        end

667 668 669 670 671 672
        # Scopes routes to a specific controller
        #
        # Example:
        #   controller "food" do
        #     match "bacon", :action => "bacon"
        #   end
673 674 675
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
676 677
        end

678 679 680 681 682 683 684 685
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
686 687 688 689 690
        #       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
691
        #        admin_post PATCH/PUT /admin/posts/:id(.:format)      admin/posts#update
692
        #        admin_post DELETE    /admin/posts/:id(.:format)      admin/posts#destroy
693
        #
694
        # === Options
695
        #
696 697
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
        # options all default to the name of the namespace.
698
        #
699 700
        # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
        # <tt>Resources#resources</tt>.
701
        #
702
        # === Examples
703
        #
704 705 706 707
        #   # accessible through /sekret/posts rather than /admin/posts
        #   namespace :admin, :path => "sekret" do
        #     resources :posts
        #   end
708
        #
S
Sebastian Martinez 已提交
709
        #   # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
710 711 712
        #   namespace :admin, :module => "sekret" do
        #     resources :posts
        #   end
713
        #
S
Sebastian Martinez 已提交
714
        #   # generates +sekret_posts_path+ rather than +admin_posts_path+
715 716 717
        #   namespace :admin, :as => "sekret" do
        #     resources :posts
        #   end
718
        def namespace(path, options = {})
719
          path = path.to_s
720 721 722
          options = { :path => path, :as => path, :module => path,
                      :shallow_path => path, :shallow_prefix => path }.merge!(options)
          scope(options) { yield }
723
        end
724

R
Ryan Bigg 已提交
725 726 727 728
        # === 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:
        #
M
mjy 已提交
729
        #   constraints(:id => /\d+\.\d+/) do
R
Ryan Bigg 已提交
730 731 732 733 734
        #     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.
735
        #
R
R.T. Lechow 已提交
736
        # You may use this to also restrict other parameters:
R
Ryan Bigg 已提交
737 738
        #
        #   resources :posts do
M
mjy 已提交
739
        #     constraints(:post_id => /\d+\.\d+/) do
R
Ryan Bigg 已提交
740 741
        #       resources :comments
        #     end
J
James Miller 已提交
742
        #   end
R
Ryan Bigg 已提交
743 744 745 746 747 748 749 750 751 752 753 754 755 756
        #
        # === 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 已提交
757
        # Requests to routes can be constrained based on specific criteria:
R
Ryan Bigg 已提交
758 759 760 761 762 763 764 765 766 767
        #
        #    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
768
        #      def self.matches?(request)
R
Ryan Bigg 已提交
769 770 771 772 773 774 775 776 777 778 779
        #        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
780 781 782 783
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
784
        # Allows you to set default parameters for a route, such as this:
785 786 787
        #   defaults :id => 'home' do
        #     match 'scoped_pages/(:id)', :to => 'pages#show'
        #   end
R
Ryan Bigg 已提交
788
        # Using this, the +:id+ parameter here will default to 'home'.
789 790 791 792
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

793
        private
J
José Valim 已提交
794
          def scope_options #:nodoc:
795 796 797
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

J
José Valim 已提交
798
          def merge_path_scope(parent, child) #:nodoc:
799
            Mapper.normalize_path("#{parent}/#{child}")
800 801
          end

J
José Valim 已提交
802
          def merge_shallow_path_scope(parent, child) #:nodoc:
803 804 805
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
806
          def merge_as_scope(parent, child) #:nodoc:
807
            parent ? "#{parent}_#{child}" : child
808 809
          end

J
José Valim 已提交
810
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
811 812 813
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
814
          def merge_module_scope(parent, child) #:nodoc:
815 816 817
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
818
          def merge_controller_scope(parent, child) #:nodoc:
819
            child
820 821
          end

J
José Valim 已提交
822
          def merge_path_names_scope(parent, child) #:nodoc:
823 824 825
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
826
          def merge_constraints_scope(parent, child) #:nodoc:
827 828 829
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
830
          def merge_defaults_scope(parent, child) #:nodoc:
831 832 833
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
834
          def merge_blocks_scope(parent, child) #:nodoc:
835 836 837
            merged = parent ? parent.dup : []
            merged << child if child
            merged
838 839
          end

J
José Valim 已提交
840
          def merge_options_scope(parent, child) #:nodoc:
841
            (parent || {}).except(*override_keys(child)).merge(child)
842
          end
843

J
José Valim 已提交
844
          def merge_shallow_scope(parent, child) #:nodoc:
845 846
            child ? true : false
          end
847

J
José Valim 已提交
848
          def override_keys(child) #:nodoc:
849 850
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
851 852
      end

853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876
      # 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 已提交
877 878
      # <tt>app/controllers/admin</tt> directory, and you can group them together
      # in your router:
879 880 881 882 883
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
S
Sebastian Martinez 已提交
884 885
      # 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
886 887 888 889
      # overrides this restriction, e.g:
      #
      #   resources :articles, :id => /[^\/]+/
      #
S
Sebastian Martinez 已提交
890
      # This allows any character other than a slash as part of your +:id+.
891
      #
J
Joshua Peek 已提交
892
      module Resources
893 894
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
895
        VALID_ON_OPTIONS  = [:new, :collection, :member]
896
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except, :param]
897
        CANONICAL_ACTIONS = %w(index create new show update destroy)
898

899
        class Resource #:nodoc:
900
          attr_reader :controller, :path, :options, :param
901 902

          def initialize(entities, options = {})
903
            @name       = entities.to_s
904 905 906
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
907
            @param      = options[:param] || :id
908
            @options    = options
909 910
          end

911
          def default_actions
912
            [:index, :create, :new, :show, :update, :destroy, :edit]
913 914
          end

915
          def actions
916
            if only = @options[:only]
917
              Array(only).map(&:to_sym)
918
            elsif except = @options[:except]
919 920 921 922 923 924
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

925
          def name
926
            @as || @name
927 928
          end

929
          def plural
930
            @plural ||= name.to_s
931 932 933
          end

          def singular
934
            @singular ||= name.to_s.singularize
935 936
          end

937
          alias :member_name :singular
938

939
          # Checks for uncountable plurals, and appends "_index" if the plural
940
          # and singular form are the same.
941
          def collection_name
942
            singular == plural ? "#{plural}_index" : plural
943 944
          end

945
          def resource_scope
946
            { :controller => controller }
947 948
          end

949
          alias :collection_scope :path
950 951

          def member_scope
952
            "#{path}/:#{param}"
953 954
          end

955
          def new_scope(new_path)
956
            "#{path}/#{new_path}"
957 958 959
          end

          def nested_scope
960
            "#{path}/:#{singular}_#{param}"
961
          end
962

963 964 965
        end

        class SingletonResource < Resource #:nodoc:
966
          def initialize(entities, options)
967
            super
968
            @as         = nil
969 970
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
971 972
          end

973 974 975 976
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

977 978
          def plural
            @plural ||= name.to_s.pluralize
979 980
          end

981 982
          def singular
            @singular ||= name.to_s
983
          end
984 985 986 987 988 989

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
990 991
        end

992 993 994 995
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

996 997 998 999 1000 1001 1002 1003 1004
        # 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 已提交
1005
        # the +GeoCoders+ controller (note that the controller is named after
1006 1007
        # the plural):
        #
1008 1009 1010 1011
        #   GET       /geocoder/new
        #   POST      /geocoder
        #   GET       /geocoder
        #   GET       /geocoder/edit
1012
        #   PATCH/PUT /geocoder
1013
        #   DELETE    /geocoder
1014
        #
1015
        # === Options
1016
        # Takes same options as +resources+.
J
Joshua Peek 已提交
1017
        def resource(*resources, &block)
J
Joshua Peek 已提交
1018
          options = resources.extract_options!
J
Joshua Peek 已提交
1019

1020
          if apply_common_behavior_for(:resource, resources, options, &block)
1021 1022 1023
            return self
          end

1024
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1025
            yield if block_given?
1026

1027
            collection do
1028
              post :create
1029
            end if parent_resource.actions.include?(:create)
1030

1031
            new do
1032
              get :new
1033
            end if parent_resource.actions.include?(:new)
1034

1035
            member do
1036 1037
              get :edit if parent_resource.actions.include?(:edit)
              get :show if parent_resource.actions.include?(:show)
1038
              if parent_resource.actions.include?(:update)
1039 1040
                patch :update
                put   :update
1041
              end
1042
              delete :destroy if parent_resource.actions.include?(:destroy)
1043 1044 1045
            end
          end

J
Joshua Peek 已提交
1046
          self
1047 1048
        end

1049 1050 1051 1052 1053 1054 1055 1056
        # 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 已提交
1057
        # the +Photos+ controller:
1058
        #
1059 1060 1061 1062 1063
        #   GET       /photos
        #   GET       /photos/new
        #   POST      /photos
        #   GET       /photos/:id
        #   GET       /photos/:id/edit
1064
        #   PATCH/PUT /photos/:id
1065
        #   DELETE    /photos/:id
1066
        #
1067 1068 1069 1070 1071 1072 1073 1074
        # Resources can also be nested infinitely by using this block syntax:
        #
        #   resources :photos do
        #     resources :comments
        #   end
        #
        # This generates the following comments routes:
        #
1075 1076 1077 1078 1079
        #   GET       /photos/:photo_id/comments
        #   GET       /photos/:photo_id/comments/new
        #   POST      /photos/:photo_id/comments
        #   GET       /photos/:photo_id/comments/:id
        #   GET       /photos/:photo_id/comments/:id/edit
1080
        #   PATCH/PUT /photos/:photo_id/comments/:id
1081
        #   DELETE    /photos/:photo_id/comments/:id
1082
        #
1083
        # === Options
1084 1085
        # Takes same options as <tt>Base#match</tt> as well as:
        #
1086
        # [:path_names]
A
Aviv Ben-Yosef 已提交
1087 1088
        #   Allows you to change the segment component of the +edit+ and +new+ actions.
        #   Actions not specified are not changed.
1089 1090 1091 1092
        #
        #     resources :posts, :path_names => { :new => "brand_new" }
        #
        #   The above example will now change /posts/new to /posts/brand_new
1093
        #
1094 1095 1096 1097 1098 1099 1100
        # [:path]
        #   Allows you to change the path prefix for the resource.
        #
        #     resources :posts, :path => 'postings'
        #
        #   The resource and all segments will now route to /postings instead of /posts
        #
1101 1102
        # [:only]
        #   Only generate routes for the given actions.
1103
        #
1104 1105
        #     resources :cows, :only => :show
        #     resources :cows, :only => [:show, :index]
1106
        #
1107 1108
        # [:except]
        #   Generate all routes except for the given actions.
1109
        #
1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123
        #     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
1124
        #       resources :comments, :except => [:show, :edit, :update, :destroy]
1125
        #     end
1126 1127 1128 1129 1130
        #     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>.
1131 1132 1133 1134
        #
        # [:shallow_path]
        #   Prefixes nested shallow routes with the specified path.
        #
1135 1136 1137 1138
        #     scope :shallow_path => "sekret" do
        #       resources :posts do
        #         resources :comments, :shallow => true
        #       end
1139 1140 1141 1142
        #     end
        #
        #   The +comments+ resource here will have the following routes generated for it:
        #
1143 1144 1145 1146 1147
        #     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)
        #     edit_comment     GET       /sekret/comments/:id/edit(.:format)
        #     comment          GET       /sekret/comments/:id(.:format)
1148
        #     comment          PATCH/PUT /sekret/comments/:id(.:format)
1149
        #     comment          DELETE    /sekret/comments/:id(.:format)
1150
        #
1151 1152 1153
        # [:shallow_prefix]
        #   Prefixes nested shallow route names with specified prefix.
        #
V
Vijay Dev 已提交
1154
        #     scope :shallow_prefix => "sekret" do
1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169
        #       resources :posts do
        #         resources :comments, :shallow => true
        #       end
        #     end
        #
        #   The +comments+ resource here will have the following routes generated for it:
        #
        #     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)
        #     edit_sekret_comment     GET       /comments/:id/edit(.:format)
        #     sekret_comment          GET       /comments/:id(.:format)
        #     sekret_comment          PATCH/PUT /comments/:id(.:format)
        #     sekret_comment          DELETE    /comments/:id(.:format)
        #
1170
        # === Examples
1171
        #
S
Sebastian Martinez 已提交
1172
        #   # routes call <tt>Admin::PostsController</tt>
1173
        #   resources :posts, :module => "admin"
1174
        #
1175
        #   # resource actions are at /admin/posts.
1176
        #   resources :posts, :path => "admin/posts"
J
Joshua Peek 已提交
1177
        def resources(*resources, &block)
J
Joshua Peek 已提交
1178
          options = resources.extract_options!
1179

1180
          if apply_common_behavior_for(:resources, resources, options, &block)
1181 1182 1183
            return self
          end

1184
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1185
            yield if block_given?
J
Joshua Peek 已提交
1186

1187
            collection do
1188 1189
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1190
            end
1191

1192
            new do
1193
              get :new
1194
            end if parent_resource.actions.include?(:new)
1195

1196
            member do
1197 1198
              get :edit if parent_resource.actions.include?(:edit)
              get :show if parent_resource.actions.include?(:show)
1199
              if parent_resource.actions.include?(:update)
1200 1201
                patch :update
                put   :update
1202
              end
1203
              delete :destroy if parent_resource.actions.include?(:destroy)
1204 1205 1206
            end
          end

J
Joshua Peek 已提交
1207
          self
1208 1209
        end

1210 1211 1212 1213 1214 1215 1216 1217 1218
        # 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 已提交
1219
        # with GET, and route to the search action of +PhotosController+. It will also
1220 1221
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1222
        def collection
1223 1224
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1225 1226
          end

1227 1228 1229 1230
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1231
          end
1232
        end
J
Joshua Peek 已提交
1233

1234 1235 1236 1237 1238 1239 1240 1241 1242
        # 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 已提交
1243
        # preview action of +PhotosController+. It will also create the
1244
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1245
        def member
1246 1247
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1248
          end
J
Joshua Peek 已提交
1249

1250 1251 1252 1253
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1254 1255 1256 1257 1258 1259 1260
          end
        end

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

1262 1263 1264 1265
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1266
          end
J
Joshua Peek 已提交
1267 1268
        end

1269
        def nested
1270 1271
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1272 1273 1274
          end

          with_scope_level(:nested) do
1275
            if shallow?
1276
              with_exclusive_scope do
1277
                if @scope[:shallow_path].blank?
1278
                  scope(parent_resource.nested_scope, nested_options) { yield }
1279
                else
1280
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1281
                    scope(parent_resource.nested_scope, nested_options) { yield }
1282 1283 1284 1285
                  end
                end
              end
            else
1286
              scope(parent_resource.nested_scope, nested_options) { yield }
1287 1288 1289 1290
            end
          end
        end

1291
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1292
        def namespace(path, options = {})
1293
          if resource_scope?
1294 1295 1296 1297 1298 1299
            nested { super }
          else
            super
          end
        end

1300
        def shallow
1301
          scope(:shallow => true, :shallow_path => @scope[:path]) do
1302 1303 1304 1305
            yield
          end
        end

1306 1307 1308 1309
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

1310
        def draw(name)
1311 1312
          path = @draw_paths.find do |_path|
            _path.join("#{name}.rb").file?
1313 1314 1315 1316 1317
          end

          unless path
            msg  = "Your router tried to #draw the external file #{name}.rb,\n" \
                   "but the file was not found in:\n\n"
1318
            msg += @draw_paths.map { |_path| " * #{_path}" }.join("\n")
1319 1320 1321 1322 1323 1324
            raise msg
          end

          instance_eval(path.join("#{name}.rb").read)
        end

1325
        # match 'path' => 'controller#action'
R
Rafael Mendonça França 已提交
1326
        # match 'path', to: 'controller#action'
1327
        # match 'path', 'otherpath', on: :member, via: :get
1328 1329 1330 1331
        def match(path, *rest)
          if rest.empty? && Hash === path
            options  = path
            path, to = options.find { |name, value| name.is_a?(String) }
1332 1333
            options[:to] = to
            options.delete(path)
1334 1335 1336 1337 1338 1339
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1340 1341
          options[:anchor] = true unless options.key?(:anchor)

A
Aaron Patterson 已提交
1342 1343 1344 1345
          if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
            raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
          end

1346
          paths.each { |_path| decomposed_match(_path, options.dup) }
1347 1348
          self
        end
1349

1350
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1351 1352
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1353
          else
A
Aaron Patterson 已提交
1354 1355 1356 1357 1358 1359 1360 1361
            case @scope[:scope_level]
            when :resources
              nested { decomposed_match(path, options) }
            when :resource
              member { decomposed_match(path, options) }
            else
              add_route(path, options)
            end
J
Joshua Peek 已提交
1362
          end
1363
        end
J
Joshua Peek 已提交
1364

1365
        def add_route(action, options) # :nodoc:
1366
          path = path_for_action(action, options.delete(:path))
1367

1368 1369 1370
          if action.to_s =~ /^[\w\/]+$/
            options[:action] ||= action unless action.to_s.include?("/")
          else
1371 1372 1373
            action = nil
          end

1374
          if !options.fetch(:as, true)
1375 1376 1377
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1378
          end
J
Joshua Peek 已提交
1379

1380 1381 1382
          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)
J
Joshua Peek 已提交
1383 1384
        end

1385
        def root(options={})
1386
          if @scope[:scope_level] == :resources
1387 1388
            with_scope_level(:root) do
              scope(parent_resource.path) do
1389 1390 1391 1392 1393 1394
                super(options)
              end
            end
          else
            super(options)
          end
1395 1396
        end

1397
        protected
1398

1399
          def parent_resource #:nodoc:
1400 1401 1402
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1403
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1404 1405 1406 1407 1408
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1409 1410 1411 1412 1413
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1414
            options.keys.each do |k|
1415 1416 1417
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1418 1419 1420
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1421 1422 1423 1424 1425
                send(method, resources.pop, options, &block)
              end
              return true
            end

1426 1427 1428 1429
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1430 1431 1432
            false
          end

J
José Valim 已提交
1433
          def action_options?(options) #:nodoc:
1434 1435 1436
            options[:only] || options[:except]
          end

J
José Valim 已提交
1437
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1438
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1439 1440
          end

J
José Valim 已提交
1441
          def scope_action_options #:nodoc:
1442 1443 1444
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1445
          def resource_scope? #:nodoc:
1446
            [:resource, :resources].include? @scope[:scope_level]
1447 1448
          end

J
José Valim 已提交
1449
          def resource_method_scope? #:nodoc:
1450
            [:collection, :member, :new].include? @scope[:scope_level]
1451 1452
          end

1453
          def with_exclusive_scope
1454
            begin
1455 1456
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1457

1458 1459 1460
              with_scope_level(:exclusive) do
                yield
              end
1461
            ensure
1462
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1463 1464 1465
            end
          end

1466
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1467
            old, @scope[:scope_level] = @scope[:scope_level], kind
1468
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1469 1470 1471
            yield
          ensure
            @scope[:scope_level] = old
1472
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1473
          end
1474

1475 1476
          def resource_scope(kind, resource) #:nodoc:
            with_scope_level(kind, resource) do
1477
              scope(parent_resource.resource_scope) do
1478 1479 1480 1481 1482
                yield
              end
            end
          end

J
José Valim 已提交
1483
          def nested_options #:nodoc:
1484 1485 1486 1487 1488 1489
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
              :"#{parent_resource.singular}_id" => id_constraint
            } if id_constraint?

            options
1490 1491
          end

J
José Valim 已提交
1492
          def id_constraint? #:nodoc:
1493
            @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
1494 1495
          end

J
José Valim 已提交
1496
          def id_constraint #:nodoc:
1497
            @scope[:constraints][:id]
1498 1499
          end

J
José Valim 已提交
1500
          def canonical_action?(action, flag) #:nodoc:
1501
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1502 1503
          end

J
José Valim 已提交
1504
          def shallow_scoping? #:nodoc:
1505
            shallow? && @scope[:scope_level] == :member
1506 1507
          end

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

1512 1513
            path = if canonical_action?(action, path.blank?)
              prefix.to_s
1514
            else
1515
              "#{prefix}/#{action_path(action, path)}"
1516 1517 1518
            end
          end

J
José Valim 已提交
1519
          def action_path(name, path = nil) #:nodoc:
1520
            name = name.to_sym if name.is_a?(String)
1521
            path || @scope[:path_names][name] || name.to_s
1522 1523
          end

J
José Valim 已提交
1524
          def prefix_name_for_action(as, action) #:nodoc:
1525
            if as
1526
              as.to_s
1527
            elsif !canonical_action?(action, @scope[:scope_level])
1528
              action.to_s
1529
            end
1530 1531
          end

J
José Valim 已提交
1532
          def name_for_action(as, action) #:nodoc:
1533
            prefix = prefix_name_for_action(as, action)
1534
            prefix = Mapper.normalize_name(prefix) if prefix
1535 1536 1537
            name_prefix = @scope[:as]

            if parent_resource
1538
              return nil unless as || action
1539

1540 1541
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1542
            end
1543

1544
            name = case @scope[:scope_level]
1545
            when :nested
1546
              [name_prefix, prefix]
1547
            when :collection
1548
              [prefix, name_prefix, collection_name]
1549
            when :new
1550 1551 1552 1553 1554
              [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]
1555
            else
1556
              [name_prefix, member_name, prefix]
1557
            end
1558

1559 1560 1561 1562 1563 1564 1565 1566 1567 1568
            if candidate = name.select(&:present?).join("_").presence
              # If a name was not explicitly given, we check if it is valid
              # and return nil in case it isn't. Otherwise, we pass the invalid name
              # forward so the underlying router engine treats it and raises an exception.
              if as.nil?
                candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
              else
                candidate
              end
            end
1569
          end
J
Joshua Peek 已提交
1570
      end
J
Joshua Peek 已提交
1571

1572 1573
      def initialize(set) #:nodoc:
        @set = set
1574
        @draw_paths = set.draw_paths
1575 1576 1577
        @scope = { :path_names => @set.resources_path_names }
      end

1578 1579
      include Base
      include HttpHelpers
1580
      include Redirection
1581 1582
      include Scoping
      include Resources
J
Joshua Peek 已提交
1583 1584
    end
  end
J
Joshua Peek 已提交
1585
end