mapper.rb 54.4 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/hash/slice'
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 104 105 106

            if @options[:constraints].is_a?(Hash)
              (@options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(@options[:constraints]))
            end
107
          end
108

109
          # match "account/overview"
110
          def using_match_shorthand?(path, options)
111
            path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX
112
          end
113

114
          def normalize_path(path)
115 116
            raise ArgumentError, "path is required" if path.blank?
            path = Mapper.normalize_path(path)
117 118 119 120 121 122 123 124

            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' }
125
              @options[:controller] ||= /.+?/
126 127
            end

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

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

146 147
          def app
            Constraints.new(
148
              to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
149 150
              blocks,
              @set.request_class
151
            )
152 153
          end

154 155 156
          def conditions
            { :path_info => @path }.merge(constraints).merge(request_method_condition)
          end
J
Joshua Peek 已提交
157

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

165
          def defaults
166 167 168 169 170 171
            @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

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

182 183
              controller ||= default_controller
              action     ||= default_action
184

185 186 187
              unless controller.is_a?(Regexp) || to_shorthand
                controller = [@scope[:module], controller].compact.join("/").presence
              end
188

189 190 191 192
              if controller.is_a?(String) && controller =~ %r{\A/}
                raise ArgumentError, "controller name should not start with a slash"
              end

193 194
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
195

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

200
              if action.blank? && segment_keys.exclude?("action")
201 202
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
203

A
Aaron Patterson 已提交
204
              hash = {}
A
Aaron Patterson 已提交
205 206
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
207
              hash
208 209
            end
          end
210

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

220 221 222
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
223

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

233
          def segment_keys
234 235 236
            return @segment_keys if @segment_keys

            @segment_keys = Journey::Path::Pattern.new(
237
              Journey::Router::Strexp.compile(@path, requirements, SEPARATORS)
238
            ).names
239
          end
240

241 242 243
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
244

245
          def default_controller
246
            @options[:controller] || @scope[:controller]
247
          end
248 249

          def default_action
250
            @options[:action] || @scope[:action]
251
          end
252 253 254 255 256

          def defaults_from_constraints(constraints)
            url_keys = [:protocol, :subdomain, :domain, :host, :port]
            constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) }
          end
257
      end
258

259
      # Invokes Rack::Mount::Utils.normalize path and ensure that
260 261
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
262
      def self.normalize_path(path)
263
        path = Journey::Router::Utils.normalize_path(path)
264
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
265 266 267
        path
      end

268
      def self.normalize_name(name)
269
        normalize_path(name)[1..-1].tr("/", "_")
270 271
      end

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

291 292 293
        # 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:
294
        #
295
        #   # sets :controller, :action and :id in params
296
        #   match ':controller/:action/:id'
297
        #
298 299 300 301 302 303 304 305 306 307 308 309
        # 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:
310 311 312 313
        #
        #   match 'photos/:id' => 'photos#show'
        #   match 'photos/:id', :to => 'photos#show'
        #   match 'photos/:id', :controller => 'photos', :action => 'show'
314
        #
315 316 317
        # A pattern can also point to a +Rack+ endpoint i.e. anything that
        # responds to +call+:
        #
A
Alexey Vakhov 已提交
318
        #   match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon"] }
319 320 321 322
        #   match 'photos/:id' => PhotoRackApp
        #   # Yes, controller actions are just rack endpoints
        #   match 'photos/:id' => PhotosController.action(:show)
        #
323
        # === Options
324
        #
325
        # Any options not seen here are passed on as params with the url.
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
        #
        # [: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]
354 355
        #   Points to a +Rack+ endpoint. Can be an object that responds to
        #   +call+ or a string representing a controller's action.
356
        #
357
        #      match 'path', :to => 'controller#action'
J
Justin Woodbridge 已提交
358
        #      match 'path', :to => lambda { |env| [200, {}, "Success!"] }
359
        #      match 'path', :to => RackApp
360 361 362
        #
        # [:on]
        #   Shorthand for wrapping routes in a specific RESTful context. Valid
363
        #   values are +:member+, +:collection+, and +:new+. Only use within
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
        #   <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
380
        #   object that responds to <tt>matches?</tt>
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
        #
        #     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.
399 400
        #
        # [:anchor]
401
        #   Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
402 403 404 405
        #   false, the pattern matches any request prefixed with the given path.
        #
        #     # Matches any request starting with 'path'
        #     match 'path' => 'c#a', :anchor => false
406 407
        #
        # [:format]
408
        #   Allows you to specify the default value for optional +format+
V
Vijay Dev 已提交
409
        #   segment or disable it by supplying +false+.
410
        def match(path, options=nil)
411
        end
412

413 414
        # Mount a Rack-based application to be used within the application.
        #
R
Ryan Bigg 已提交
415
        #   mount SomeRackApp, :at => "some_route"
416 417 418
        #
        # Alternatively:
        #
R
Ryan Bigg 已提交
419
        #   mount(SomeRackApp => "some_route")
420
        #
421 422
        # For options, see +match+, as +mount+ uses it internally.
        #
423 424 425 426 427
        # 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 已提交
428
        #   mount(SomeRackApp => "some_route", :as => "exciting")
429 430 431
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
432 433 434 435
        def mount(app, options = nil)
          if options
            path = options.delete(:at)
          else
436 437 438 439
            unless Hash === app
              raise ArgumentError, "must be called with mount point"
            end

440 441 442 443 444 445 446
            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

447 448
          options[:as] ||= app_name(app)

449
          match(path, options.merge(:to => app, :anchor => false, :format => false, :via => :all))
450 451

          define_generate_prefix(app, options[:as])
452 453 454
          self
        end

455 456 457 458
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
459

460 461 462 463 464 465
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

466 467 468
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
469 470 471 472 473

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
474
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
475
            end
476 477 478
          end

          def define_generate_prefix(app, name)
479
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
480 481

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
482 483
            _routes = @set
            app.routes.define_mounted_helper(name)
J
José Valim 已提交
484 485 486 487 488
            app.routes.singleton_class.class_eval do
              define_method :mounted? do
                true
              end

489
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
490
                prefix_options = options.slice(*_route.segment_keys)
491 492
                # 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 已提交
493 494 495
                prefix = _routes.url_helpers.send("#{name}_path", prefix_options)
                prefix = '' if prefix == '/'
                prefix
496 497 498
              end
            end
          end
499 500 501
      end

      module HttpHelpers
502
        # Define a route that only recognizes HTTP GET.
503
        # For supported arguments, see <tt>Base#match</tt>.
504
        #
505
        #   get 'bacon', :to => 'food#bacon'
506
        def get(*args, &block)
507
          map_method(:get, args, &block)
508 509
        end

510
        # Define a route that only recognizes HTTP POST.
511
        # For supported arguments, see <tt>Base#match</tt>.
512
        #
513
        #   post 'bacon', :to => 'food#bacon'
514
        def post(*args, &block)
515
          map_method(:post, args, &block)
516 517
        end

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

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

534
        # Define a route that only recognizes HTTP DELETE.
535
        # For supported arguments, see <tt>Base#match</tt>.
536
        #
537
        #   delete 'broccoli', :to => 'food#broccoli'
538
        def delete(*args, &block)
539
          map_method(:delete, args, &block)
540 541 542
        end

        private
543
          def map_method(method, args, &block)
544
            options = args.extract_options!
545 546
            options[:via]    = method
            options[:path] ||= args.first if args.first.is_a?(String)
547
            match(*args, options, &block)
548 549 550 551
            self
          end
      end

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

643
          options[:path] = args.first if args.first.is_a?(String)
644
          recover = {}
645

646 647
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
648
            block, options[:constraints] = options[:constraints], {}
649
          end
650

651 652 653 654
          if options[:constraints].is_a?(Hash)
            (options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(options[:constraints]))
          end

655 656 657 658 659
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
660 661
          end

662 663 664 665 666 667
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)

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

668 669 670
          yield
          self
        ensure
671 672 673
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end
674 675 676

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
677 678
        end

679 680 681 682 683
        # Scopes routes to a specific controller
        #
        #   controller "food" do
        #     match "bacon", :action => "bacon"
        #   end
684 685 686
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
687 688
        end

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

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

R
Ryan Bigg 已提交
795
        # Allows you to set default parameters for a route, such as this:
796 797 798
        #   defaults :id => 'home' do
        #     match 'scoped_pages/(:id)', :to => 'pages#show'
        #   end
R
Ryan Bigg 已提交
799
        # Using this, the +:id+ parameter here will default to 'home'.
800 801 802 803
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

804
        private
J
José Valim 已提交
805
          def scope_options #:nodoc:
806 807 808
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

J
José Valim 已提交
809
          def merge_path_scope(parent, child) #:nodoc:
810
            Mapper.normalize_path("#{parent}/#{child}")
811 812
          end

J
José Valim 已提交
813
          def merge_shallow_path_scope(parent, child) #:nodoc:
814 815 816
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
817
          def merge_as_scope(parent, child) #:nodoc:
818
            parent ? "#{parent}_#{child}" : child
819 820
          end

J
José Valim 已提交
821
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
822 823 824
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
825
          def merge_module_scope(parent, child) #:nodoc:
826 827 828
            parent ? "#{parent}/#{child}" : child
          end

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

J
José Valim 已提交
833
          def merge_path_names_scope(parent, child) #:nodoc:
834 835 836
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
837
          def merge_constraints_scope(parent, child) #:nodoc:
838 839 840
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
841
          def merge_defaults_scope(parent, child) #:nodoc:
842 843 844
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
845
          def merge_blocks_scope(parent, child) #:nodoc:
846 847 848
            merged = parent ? parent.dup : []
            merged << child if child
            merged
849 850
          end

J
José Valim 已提交
851
          def merge_options_scope(parent, child) #:nodoc:
852
            (parent || {}).except(*override_keys(child)).merge(child)
853
          end
854

J
José Valim 已提交
855
          def merge_shallow_scope(parent, child) #:nodoc:
856 857
            child ? true : false
          end
858

J
José Valim 已提交
859
          def override_keys(child) #:nodoc:
860 861
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
862 863 864 865 866

          def defaults_from_constraints(constraints)
            url_keys = [:protocol, :subdomain, :domain, :host, :port]
            constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) }
          end
867 868
      end

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

915
        class Resource #:nodoc:
916
          attr_reader :controller, :path, :options, :param
917 918

          def initialize(entities, options = {})
919
            @name       = entities.to_s
920 921 922
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
923
            @param      = (options[:param] || :id).to_sym
924
            @options    = options
925 926
          end

927
          def default_actions
928
            [:index, :create, :new, :show, :update, :destroy, :edit]
929 930
          end

931
          def actions
932
            if only = @options[:only]
933
              Array(only).map(&:to_sym)
934
            elsif except = @options[:except]
935 936 937 938 939 940
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

941
          def name
942
            @as || @name
943 944
          end

945
          def plural
946
            @plural ||= name.to_s
947 948 949
          end

          def singular
950
            @singular ||= name.to_s.singularize
951 952
          end

953
          alias :member_name :singular
954

955
          # Checks for uncountable plurals, and appends "_index" if the plural
956
          # and singular form are the same.
957
          def collection_name
958
            singular == plural ? "#{plural}_index" : plural
959 960
          end

961
          def resource_scope
962
            { :controller => controller }
963 964
          end

965
          alias :collection_scope :path
966 967

          def member_scope
968
            "#{path}/:#{param}"
969 970
          end

971 972
          alias :shallow_scope :member_scope

973
          def new_scope(new_path)
974
            "#{path}/#{new_path}"
975 976
          end

977 978 979 980
          def nested_param
            :"#{singular}_#{param}"
          end

981
          def nested_scope
982
            "#{path}/:#{nested_param}"
983
          end
984

985 986 987
        end

        class SingletonResource < Resource #:nodoc:
988
          def initialize(entities, options)
989
            super
990
            @as         = nil
991 992
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
993 994
          end

995 996 997 998
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

999 1000
          def plural
            @plural ||= name.to_s.pluralize
1001 1002
          end

1003 1004
          def singular
            @singular ||= name.to_s
1005
          end
1006 1007 1008 1009 1010 1011

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1012 1013
        end

1014 1015 1016 1017
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

1018 1019 1020 1021 1022 1023 1024 1025 1026
        # 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 已提交
1027
        # the +GeoCoders+ controller (note that the controller is named after
1028 1029
        # the plural):
        #
1030 1031 1032 1033
        #   GET       /geocoder/new
        #   POST      /geocoder
        #   GET       /geocoder
        #   GET       /geocoder/edit
1034
        #   PATCH/PUT /geocoder
1035
        #   DELETE    /geocoder
1036
        #
1037
        # === Options
1038
        # Takes same options as +resources+.
J
Joshua Peek 已提交
1039
        def resource(*resources, &block)
J
Joshua Peek 已提交
1040
          options = resources.extract_options!
J
Joshua Peek 已提交
1041

1042
          if apply_common_behavior_for(:resource, resources, options, &block)
1043 1044 1045
            return self
          end

1046
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1047
            yield if block_given?
1048

1049
            collection do
1050
              post :create
1051
            end if parent_resource.actions.include?(:create)
1052

1053
            new do
1054
              get :new
1055
            end if parent_resource.actions.include?(:new)
1056

1057
            member do
1058 1059
              get :edit if parent_resource.actions.include?(:edit)
              get :show if parent_resource.actions.include?(:show)
1060
              if parent_resource.actions.include?(:update)
1061 1062
                patch :update
                put   :update
1063
              end
1064
              delete :destroy if parent_resource.actions.include?(:destroy)
1065 1066 1067
            end
          end

J
Joshua Peek 已提交
1068
          self
1069 1070
        end

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

1206
          if apply_common_behavior_for(:resources, resources, options, &block)
1207 1208 1209
            return self
          end

1210
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1211
            yield if block_given?
J
Joshua Peek 已提交
1212

1213
            collection do
1214 1215
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1216
            end
1217

1218
            new do
1219
              get :new
1220
            end if parent_resource.actions.include?(:new)
1221

1222
            member do
1223 1224
              get :edit if parent_resource.actions.include?(:edit)
              get :show if parent_resource.actions.include?(:show)
1225
              if parent_resource.actions.include?(:update)
1226 1227
                patch :update
                put   :update
1228
              end
1229
              delete :destroy if parent_resource.actions.include?(:destroy)
1230 1231 1232
            end
          end

J
Joshua Peek 已提交
1233
          self
1234 1235
        end

1236 1237 1238 1239 1240 1241 1242 1243 1244
        # 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 已提交
1245
        # with GET, and route to the search action of +PhotosController+. It will also
1246 1247
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1248
        def collection
1249 1250
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1251 1252
          end

1253 1254 1255 1256
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1257
          end
1258
        end
J
Joshua Peek 已提交
1259

1260 1261 1262 1263 1264 1265 1266 1267 1268
        # 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 已提交
1269
        # preview action of +PhotosController+. It will also create the
1270
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1271
        def member
1272 1273
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1274
          end
J
Joshua Peek 已提交
1275

1276 1277 1278 1279
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1280 1281 1282 1283 1284 1285 1286
          end
        end

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

1288 1289 1290 1291
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1292
          end
J
Joshua Peek 已提交
1293 1294
        end

1295
        def nested
1296 1297
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1298 1299 1300
          end

          with_scope_level(:nested) do
1301
            if shallow?
1302
              with_exclusive_scope do
1303
                if @scope[:shallow_path].blank?
1304
                  scope(parent_resource.nested_scope, nested_options) { yield }
1305
                else
1306
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1307
                    scope(parent_resource.nested_scope, nested_options) { yield }
1308 1309 1310 1311
                  end
                end
              end
            else
1312
              scope(parent_resource.nested_scope, nested_options) { yield }
1313 1314 1315 1316
            end
          end
        end

1317
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1318
        def namespace(path, options = {})
1319
          if resource_scope?
1320 1321 1322 1323 1324 1325
            nested { super }
          else
            super
          end
        end

1326
        def shallow
1327
          scope(:shallow => true, :shallow_path => @scope[:path]) do
1328 1329 1330 1331
            yield
          end
        end

1332 1333 1334 1335
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

1336
        # match 'path' => 'controller#action'
R
Rafael Mendonça França 已提交
1337
        # match 'path', to: 'controller#action'
1338
        # match 'path', 'otherpath', on: :member, via: :get
1339 1340 1341 1342
        def match(path, *rest)
          if rest.empty? && Hash === path
            options  = path
            path, to = options.find { |name, value| name.is_a?(String) }
1343 1344
            options[:to] = to
            options.delete(path)
1345 1346 1347 1348 1349 1350
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1351 1352
          options[:anchor] = true unless options.key?(:anchor)

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

1357
          paths.each { |_path| decomposed_match(_path, options.dup) }
1358 1359
          self
        end
1360

1361
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1362 1363
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1364
          else
A
Aaron Patterson 已提交
1365 1366 1367 1368 1369 1370 1371 1372
            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 已提交
1373
          end
1374
        end
J
Joshua Peek 已提交
1375

1376
        def add_route(action, options) # :nodoc:
1377
          path = path_for_action(action, options.delete(:path))
1378

1379 1380 1381
          if action.to_s =~ /^[\w\/]+$/
            options[:action] ||= action unless action.to_s.include?("/")
          else
1382 1383 1384
            action = nil
          end

1385
          if !options.fetch(:as, true)
1386 1387 1388
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1389
          end
J
Joshua Peek 已提交
1390

1391
          mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1392 1393
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1394 1395
        end

1396
        def root(options={})
1397
          if @scope[:scope_level] == :resources
1398 1399
            with_scope_level(:root) do
              scope(parent_resource.path) do
1400 1401 1402 1403 1404 1405
                super(options)
              end
            end
          else
            super(options)
          end
1406 1407
        end

1408
        protected
1409

1410
          def parent_resource #:nodoc:
1411 1412 1413
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1414
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1415 1416 1417 1418 1419
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1420 1421 1422 1423 1424
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1425
            options.keys.each do |k|
1426 1427 1428
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1429 1430 1431
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1432 1433 1434 1435 1436
                send(method, resources.pop, options, &block)
              end
              return true
            end

1437 1438 1439 1440
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1441 1442 1443
            false
          end

J
José Valim 已提交
1444
          def action_options?(options) #:nodoc:
1445 1446 1447
            options[:only] || options[:except]
          end

J
José Valim 已提交
1448
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1449
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1450 1451
          end

J
José Valim 已提交
1452
          def scope_action_options #:nodoc:
1453 1454 1455
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1456
          def resource_scope? #:nodoc:
1457
            [:resource, :resources].include? @scope[:scope_level]
1458 1459
          end

J
José Valim 已提交
1460
          def resource_method_scope? #:nodoc:
1461
            [:collection, :member, :new].include? @scope[:scope_level]
1462 1463
          end

1464
          def with_exclusive_scope
1465
            begin
1466 1467
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1468

1469 1470 1471
              with_scope_level(:exclusive) do
                yield
              end
1472
            ensure
1473
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1474 1475 1476
            end
          end

1477
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1478
            old, @scope[:scope_level] = @scope[:scope_level], kind
1479
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1480 1481 1482
            yield
          ensure
            @scope[:scope_level] = old
1483
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1484
          end
1485

1486 1487
          def resource_scope(kind, resource) #:nodoc:
            with_scope_level(kind, resource) do
1488
              scope(parent_resource.resource_scope) do
1489 1490 1491 1492 1493
                yield
              end
            end
          end

J
José Valim 已提交
1494
          def nested_options #:nodoc:
1495 1496
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1497 1498
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1499 1500

            options
1501 1502
          end

1503 1504
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1505 1506
          end

1507 1508
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1509 1510
          end

J
José Valim 已提交
1511
          def canonical_action?(action, flag) #:nodoc:
1512
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1513 1514
          end

J
José Valim 已提交
1515
          def shallow_scoping? #:nodoc:
1516
            shallow? && @scope[:scope_level] == :member
1517 1518
          end

J
José Valim 已提交
1519
          def path_for_action(action, path) #:nodoc:
1520
            prefix = shallow_scoping? ?
1521
              "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path]
1522

1523
            if canonical_action?(action, path.blank?)
1524
              prefix.to_s
1525
            else
1526
              "#{prefix}/#{action_path(action, path)}"
1527 1528 1529
            end
          end

J
José Valim 已提交
1530
          def action_path(name, path = nil) #:nodoc:
1531
            name = name.to_sym if name.is_a?(String)
1532
            path || @scope[:path_names][name] || name.to_s
1533 1534
          end

J
José Valim 已提交
1535
          def prefix_name_for_action(as, action) #:nodoc:
1536
            if as
1537
              as.to_s
1538
            elsif !canonical_action?(action, @scope[:scope_level])
1539
              action.to_s
1540
            end
1541 1542
          end

J
José Valim 已提交
1543
          def name_for_action(as, action) #:nodoc:
1544
            prefix = prefix_name_for_action(as, action)
1545
            prefix = Mapper.normalize_name(prefix) if prefix
1546 1547 1548
            name_prefix = @scope[:as]

            if parent_resource
1549
              return nil unless as || action
1550

1551 1552
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1553
            end
1554

1555
            name = case @scope[:scope_level]
1556
            when :nested
1557
              [name_prefix, prefix]
1558
            when :collection
1559
              [prefix, name_prefix, collection_name]
1560
            when :new
1561 1562 1563 1564 1565
              [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]
1566
            else
1567
              [name_prefix, member_name, prefix]
1568
            end
1569

1570 1571 1572 1573 1574 1575 1576 1577 1578 1579
            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
1580
          end
J
Joshua Peek 已提交
1581
      end
J
Joshua Peek 已提交
1582

1583 1584 1585 1586 1587
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
      end

1588 1589
      include Base
      include HttpHelpers
1590
      include Redirection
1591 1592
      include Scoping
      include Resources
J
Joshua Peek 已提交
1593 1594
    end
  end
J
Joshua Peek 已提交
1595
end