mapper.rb 57.9 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

            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
A
AvnerCohen 已提交
124
              # => { 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
              unless controller.is_a?(Regexp) || to_shorthand
186 187
                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
        # You can specify what Rails should route "/" to with the root method:
        #
A
AvnerCohen 已提交
275
        #   root to: 'pages#main'
276
        #
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
        # 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:
        #
302
        #   match 'songs/*category/:title', to: 'songs#show'
303 304 305 306 307 308 309
        #
        #   # '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
        #
        #   match 'photos/:id' => 'photos#show'
A
AvnerCohen 已提交
312 313
        #   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+:
        #
318 319
        #   match 'photos/:id', to: lambda {|hash| [200, {}, "Coming soon"] }
        #   match 'photos/:id', to: PhotoRackApp
320
        #   # Yes, controller actions are just rack endpoints
321 322 323 324 325
        #   match 'photos/:id', to: PhotosController.action(:show)
        #
        # Because request various HTTP verbs with a single action has security
        # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers]
        # instead +match+
326
        #
327
        # === Options
328
        #
329
        # Any options not seen here are passed on as params with the url.
330 331 332 333 334 335 336 337 338 339 340 341 342
        #
        # [:controller]
        #   The route's controller.
        #
        # [:action]
        #   The route's action.
        #
        # [:path]
        #   The path prefix for the routes.
        #
        # [:module]
        #   The namespace for :controller.
        #
343
        #     match 'path', to: 'c#a', module: 'sekret', controller: 'posts'
344 345 346 347 348 349 350 351 352 353
        #     #=> 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.
        #
354 355 356
        #      match 'path', to: 'c#a', via: :get
        #      match 'path', to: 'c#a', via: [:get, :post]
        #      match 'path', to: 'c#a', via: :all
357 358
        #
        # [:to]
359 360
        #   Points to a +Rack+ endpoint. Can be an object that responds to
        #   +call+ or a string representing a controller's action.
361
        #
A
AvnerCohen 已提交
362 363 364
        #      match 'path', to: 'controller#action'
        #      match 'path', to: lambda { |env| [200, {}, "Success!"] }
        #      match 'path', to: RackApp
365 366 367
        #
        # [:on]
        #   Shorthand for wrapping routes in a specific RESTful context. Valid
368
        #   values are +:member+, +:collection+, and +:new+. Only use within
369 370 371
        #   <tt>resource(s)</tt> block. For example:
        #
        #      resource :bar do
372
        #        match 'foo', to: 'c#a', on: :member, via: [:get, :post]
373 374 375 376 377 378
        #      end
        #
        #   Is equivalent to:
        #
        #      resource :bar do
        #        member do
379
        #          match 'foo', to: 'c#a', via: [:get, :post]
380 381 382 383 384
        #        end
        #      end
        #
        # [:constraints]
        #   Constrains parameters with a hash of regular expressions or an
385
        #   object that responds to <tt>matches?</tt>
386
        #
A
AvnerCohen 已提交
387
        #     match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }
388 389 390 391
        #
        #     class Blacklist
        #       def matches?(request) request.remote_ip == '1.2.3.4' end
        #     end
392
        #     match 'path', to: 'c#a', constraints: Blacklist.new
393 394 395 396 397 398 399 400
        #
        #   See <tt>Scoping#constraints</tt> for more examples with its scope
        #   equivalent.
        #
        # [:defaults]
        #   Sets defaults for parameters
        #
        #     # Sets params[:format] to 'jpg' by default
401
        #     match 'path', to: 'c#a', defaults: { format: 'jpg' }
402 403
        #
        #   See <tt>Scoping#defaults</tt> for its scope equivalent.
404 405
        #
        # [:anchor]
406
        #   Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
407 408 409
        #   false, the pattern matches any request prefixed with the given path.
        #
        #     # Matches any request starting with 'path'
410
        #     match 'path', to: 'c#a', anchor: false
411 412
        #
        # [:format]
413
        #   Allows you to specify the default value for optional +format+
V
Vijay Dev 已提交
414
        #   segment or disable it by supplying +false+.
415
        def match(path, options=nil)
416
        end
417

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

445 446 447 448 449 450 451
            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

P
Pratik Naik 已提交
452 453
          options[:as]  ||= app_name(app)
          options[:via] ||= :all
454

P
Pratik Naik 已提交
455
          match(path, options.merge(:to => app, :anchor => false, :format => false))
456 457

          define_generate_prefix(app, options[:as])
458 459 460
          self
        end

461 462 463 464
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
465

466 467 468 469 470 471
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

472 473 474
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
475 476 477 478 479

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
480
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
481
            end
482 483 484
          end

          def define_generate_prefix(app, name)
485
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
486 487

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
488 489
            _routes = @set
            app.routes.define_mounted_helper(name)
J
José Valim 已提交
490 491 492 493 494
            app.routes.singleton_class.class_eval do
              define_method :mounted? do
                true
              end

495
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
496
                prefix_options = options.slice(*_route.segment_keys)
497 498
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
499
                _routes.url_helpers.send("#{name}_path", prefix_options)
500 501 502
              end
            end
          end
503 504 505
      end

      module HttpHelpers
506
        # Define a route that only recognizes HTTP GET.
C
Cesar Carruitero 已提交
507
        # For supported arguments, see match[rdoc-ref:Base#match]
508
        #
A
AvnerCohen 已提交
509
        #   get 'bacon', to: 'food#bacon'
510
        def get(*args, &block)
511
          map_method(:get, args, &block)
512 513
        end

514
        # Define a route that only recognizes HTTP POST.
C
Cesar Carruitero 已提交
515
        # For supported arguments, see match[rdoc-ref:Base#match]
516
        #
A
AvnerCohen 已提交
517
        #   post 'bacon', to: 'food#bacon'
518
        def post(*args, &block)
519
          map_method(:post, args, &block)
520 521
        end

522
        # Define a route that only recognizes HTTP PATCH.
C
Cesar Carruitero 已提交
523
        # For supported arguments, see match[rdoc-ref:Base#match]
524
        #
A
AvnerCohen 已提交
525
        #   patch 'bacon', to: 'food#bacon'
526 527 528 529
        def patch(*args, &block)
          map_method(:patch, args, &block)
        end

530
        # Define a route that only recognizes HTTP PUT.
C
Cesar Carruitero 已提交
531
        # For supported arguments, see match[rdoc-ref:Base#match]
532
        #
A
AvnerCohen 已提交
533
        #   put 'bacon', to: 'food#bacon'
534
        def put(*args, &block)
535
          map_method(:put, args, &block)
536 537
        end

538
        # Define a route that only recognizes HTTP DELETE.
C
Cesar Carruitero 已提交
539
        # For supported arguments, see match[rdoc-ref:Base#match]
540
        #
A
AvnerCohen 已提交
541
        #   delete 'broccoli', to: 'food#broccoli'
542
        def delete(*args, &block)
543
          map_method(:delete, args, &block)
544 545 546
        end

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

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

647
          options[:path] = args.flatten.join('/') if args.any?
648
          options[:constraints] ||= {}
649

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

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

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

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

669 670 671
          yield
          self
        ensure
672
          @scope.merge!(recover)
673 674
        end

675 676 677
        # Scopes routes to a specific controller
        #
        #   controller "food" do
A
AvnerCohen 已提交
678
        #     match "bacon", action: "bacon"
679
        #   end
680 681 682
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
683 684
        end

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

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

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

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

J
José Valim 已提交
805
          def merge_path_scope(parent, child) #:nodoc:
806
            Mapper.normalize_path("#{parent}/#{child}")
807 808
          end

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

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

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

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

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

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

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

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

J
José Valim 已提交
841
          def merge_blocks_scope(parent, child) #:nodoc:
842 843 844
            merged = parent ? parent.dup : []
            merged << child if child
            merged
845 846
          end

J
José Valim 已提交
847
          def merge_options_scope(parent, child) #:nodoc:
848
            (parent || {}).except(*override_keys(child)).merge(child)
849
          end
850

J
José Valim 已提交
851
          def merge_shallow_scope(parent, child) #:nodoc:
852 853
            child ? true : false
          end
854

J
José Valim 已提交
855
          def override_keys(child) #:nodoc:
856 857
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
858 859 860 861 862

          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
863 864
      end

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

911
        class Resource #:nodoc:
912
          attr_reader :controller, :path, :options, :param
913 914

          def initialize(entities, options = {})
915
            @name       = entities.to_s
916 917 918
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
919
            @param      = (options[:param] || :id).to_sym
920
            @options    = options
921 922
          end

923
          def default_actions
924
            [:index, :create, :new, :show, :update, :destroy, :edit]
925 926
          end

927
          def actions
928
            if only = @options[:only]
929
              Array(only).map(&:to_sym)
930
            elsif except = @options[:except]
931 932 933 934 935 936
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

937
          def name
938
            @as || @name
939 940
          end

941
          def plural
942
            @plural ||= name.to_s
943 944 945
          end

          def singular
946
            @singular ||= name.to_s.singularize
947 948
          end

949
          alias :member_name :singular
950

951
          # Checks for uncountable plurals, and appends "_index" if the plural
952
          # and singular form are the same.
953
          def collection_name
954
            singular == plural ? "#{plural}_index" : plural
955 956
          end

957
          def resource_scope
958
            { :controller => controller }
959 960
          end

961
          alias :collection_scope :path
962 963

          def member_scope
964
            "#{path}/:#{param}"
965 966
          end

967 968
          alias :shallow_scope :member_scope

969
          def new_scope(new_path)
970
            "#{path}/#{new_path}"
971 972
          end

973 974 975 976
          def nested_param
            :"#{singular}_#{param}"
          end

977
          def nested_scope
978
            "#{path}/:#{nested_param}"
979
          end
980

981 982 983
        end

        class SingletonResource < Resource #:nodoc:
984
          def initialize(entities, options)
985
            super
986
            @as         = nil
987 988
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
989 990
          end

991 992 993 994
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

995 996
          def plural
            @plural ||= name.to_s.pluralize
997 998
          end

999 1000
          def singular
            @singular ||= name.to_s
1001
          end
1002 1003 1004 1005 1006 1007

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1008 1009
        end

1010 1011 1012 1013
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

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

1038
          if apply_common_behavior_for(:resource, resources, options, &block)
1039 1040 1041
            return self
          end

1042
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1043
            yield if block_given?
1044

1045 1046
            concerns(options[:concerns]) if options[:concerns]

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

1051
            new do
1052
              get :new
1053
            end if parent_resource.actions.include?(:new)
1054

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

J
Joshua Peek 已提交
1066
          self
1067 1068
        end

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

1204
          if apply_common_behavior_for(:resources, resources, options, &block)
1205 1206 1207
            return self
          end

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

1211 1212
            concerns(options[:concerns]) if options[:concerns]

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
      # Routing Concerns allow you to declare common routes that can be reused
1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602
      # inside others resources and routes.
      #
      #   concern :commentable do
      #     resources :comments
      #   end
      #
      #   concern :image_attachable do
      #     resources :images, only: :index
      #   end
      #
      # These concerns are used in Resources routing:
      #
      #   resources :messages, concerns: [:commentable, :image_attachable]
      #
      # or in a scope or namespace:
      #
      #   namespace :posts do
      #     concerns :commentable
      #   end
1603
      module Concerns
1604
        # Define a routing concern using a name.
1605
        #
1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627
        # Concerns may be defined inline, using a block, or handled by
        # another object, by passing that object as the second parameter.
        #
        # The concern object, if supplied, should respond to <tt>call</tt>,
        # which will receive two parameters:
        #
        #   * The current mapper
        #   * A hash of options which the concern object may use
        #
        # Options may also be used by concerns defined in a block by accepting
        # a block parameter. So, using a block, you might do something as
        # simple as limit the actions available on certain resources, passing
        # standard resource options through the concern:
        #
        #   concern :commentable do |options|
        #     resources :comments, options
        #   end
        #
        #   resources :posts, concerns: :commentable
        #   resources :archived_posts do
        #     # Don't allow comments on archived posts
        #     concerns :commentable, only: [:index, :show]
1628 1629
        #   end
        #
1630 1631 1632
        # Or, using a callable object, you might implement something more
        # specific to your application, which would be out of place in your
        # routes file.
1633
        #
1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644
        #   # purchasable.rb
        #   class Purchasable
        #     def initialize(defaults = {})
        #       @defaults = defaults
        #     end
        #
        #     def call(mapper, options = {})
        #       options = @defaults.merge(options)
        #       mapper.resources :purchases
        #       mapper.resources :receipts
        #       mapper.resources :returns if options[:returnable]
1645 1646 1647
        #     end
        #   end
        #
1648 1649 1650 1651 1652 1653 1654 1655
        #   # routes.rb
        #   concern :purchasable, Purchasable.new(returnable: true)
        #
        #   resources :toys, concerns: :purchasable
        #   resources :electronics, concerns: :purchasable
        #   resources :pets do
        #     concerns :purchasable, returnable: false
        #   end
1656
        #
1657 1658 1659
        # Any routing helpers can be used inside a concern. If using a
        # callable, they're accessible from the Mapper that's passed to
        # <tt>call</tt>.
1660
        def concern(name, callable = nil, &block)
1661 1662
          callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
          @concerns[name] = callable
1663 1664
        end

1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675
        # Use the named concerns
        #
        #   resources :posts do
        #     concerns :commentable
        #   end
        #
        # concerns also work in any routes helper that you want to use:
        #
        #   namespace :posts do
        #     concerns :commentable
        #   end
1676 1677 1678
        def concerns(*args)
          options = args.extract_options!
          args.flatten.each do |name|
1679
            if concern = @concerns[name]
1680
              concern.call(self, options)
1681 1682 1683 1684 1685 1686 1687
            else
              raise ArgumentError, "No concern named #{name} was found!"
            end
          end
        end
      end

1688 1689 1690
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1691
        @concerns = {}
1692 1693
      end

1694 1695
      include Base
      include HttpHelpers
1696
      include Redirection
1697
      include Scoping
1698
      include Concerns
1699
      include Resources
J
Joshua Peek 已提交
1700 1701
    end
  end
J
Joshua Peek 已提交
1702
end