mapper.rb 64.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/core_ext/array/extract_options'
6
require 'active_support/core_ext/module/remove_method'
7
require 'active_support/inflector'
8
require 'action_dispatch/routing/redirection'
9
require 'action_dispatch/routing/endpoint'
10
require 'active_support/deprecation'
11

J
Joshua Peek 已提交
12 13
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
14
    class Mapper
15 16
      URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]

17
      class Constraints < Endpoint #:nodoc:
18
        attr_reader :app, :constraints
19

20
        def initialize(app, constraints, dispatcher_p)
21 22 23 24
          # Unwrap Constraints objects.  I don't actually think it's possible
          # to pass a Constraints object to this constructor, but there were
          # multiple places that kept testing children of this object.  I
          # *think* they were just being defensive, but I have no idea.
25
          if app.is_a?(self.class)
26 27 28 29
            constraints += app.constraints
            app = app.app
          end

30
          @dispatcher = dispatcher_p
31

32
          @app, @constraints, = app, constraints
33 34
        end

35 36
        def dispatcher?; @dispatcher; end

37
        def matches?(req)
38 39 40
          @constraints.all? do |constraint|
            (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
              (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
G
Gosha Arinich 已提交
41
          end
42 43
        end

44
        def serve(req)
45 46 47 48 49 50 51
          return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)

          if dispatcher?
            @app.serve req
          else
            @app.call req.env
          end
52
        end
53 54 55

        private
          def constraint_args(constraint, request)
56
            constraint.arity == 1 ? [request] : [request.path_parameters, request]
57
          end
58 59
      end

60
      class Mapping #:nodoc:
61
        ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
62

A
Aaron Patterson 已提交
63
        attr_reader :requirements, :conditions, :defaults
64
        attr_reader :to, :default_controller, :default_action, :as, :anchor
65

66
        def self.build(scope, set, path, as, options)
67 68 69 70 71 72 73
          options = scope[:options].merge(options) if scope[:options]

          options.delete :only
          options.delete :except
          options.delete :shallow_path
          options.delete :shallow_prefix
          options.delete :shallow
74

75 76
          defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}

77
          new scope, set, path, defaults, as, options
78 79
        end

80
        def initialize(scope, set, path, defaults, as, options)
81 82
          @requirements, @conditions = {}, {}
          @defaults = defaults
83
          @set = set
84

85
          @to                 = options.delete :to
86 87
          @default_controller = options.delete(:controller) || scope[:controller]
          @default_action     = options.delete(:action) || scope[:action]
88
          @as                 = as
89
          @anchor             = options.delete :anchor
90

A
Aaron Patterson 已提交
91
          formatted = options.delete :format
92
          via = Array(options.delete(:via) { [] })
93
          options_constraints = options.delete :constraints
94

95
          path = normalize_path! path, formatted
96
          ast  = path_ast path
A
Aaron Patterson 已提交
97
          path_params = path_params ast
98

A
Aaron Patterson 已提交
99
          options = normalize_options!(options, formatted, path_params, ast, scope[:module])
100

A
Aaron Patterson 已提交
101

102 103
          split_constraints(path_params, scope[:constraints]) if scope[:constraints]
          constraints = constraints(options, path_params)
104

105 106
          split_constraints path_params, constraints

107 108
          @blocks = blocks(options_constraints, scope[:blocks])

109 110 111 112 113 114 115 116 117 118
          if options_constraints.is_a?(Hash)
            split_constraints path_params, options_constraints
            options_constraints.each do |key, default|
              if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
                @defaults[key] ||= default
              end
            end
          end

          normalize_format!(formatted)
119 120 121 122

          @conditions[:path_info] = path
          @conditions[:parsed_path_info] = ast

123
          add_request_method(via, @conditions)
124
          normalize_defaults!(options)
125
        end
J
Joshua Peek 已提交
126

127
        def to_route
128
          [ app(@blocks), conditions, requirements, defaults, as, anchor ]
129
        end
J
Joshua Peek 已提交
130

131
        private
132

133 134
          def normalize_path!(path, format)
            path = Mapper.normalize_path(path)
135

136 137 138 139 140 141
            if format == true
              "#{path}.:format"
            elsif optional_format?(path, format)
              "#{path}(.:format)"
            else
              path
142 143 144
            end
          end

145 146
          def optional_format?(path, format)
            format != false && !path.include?(':format') && !path.end_with?('/')
147 148
          end

149
          def normalize_options!(options, formatted, path_params, path_ast, modyoule)
150 151
            # Add a constraint for wildcard route to make it non-greedy and match the
            # optional format part of the route by default
152
            if formatted != false
A
Aaron Patterson 已提交
153 154 155
              path_ast.grep(Journey::Nodes::Star) do |node|
                options[node.name.to_sym] ||= /.+?/
              end
156 157
            end

158
            if path_params.include?(:controller)
159
              raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
160 161 162 163 164

              # 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' }
165
              options[:controller] ||= /.+?/
166
            end
167

A
Aaron Patterson 已提交
168 169 170
            if to.respond_to? :call
              options
            else
171 172 173 174 175 176 177
              to_endpoint = split_to to
              controller  = to_endpoint[0] || default_controller
              action      = to_endpoint[1] || default_action

              controller = add_controller_module(controller, modyoule)

              options.merge! check_controller_and_action(path_params, controller, action)
A
Aaron Patterson 已提交
178
            end
179 180
          end

181
          def split_constraints(path_params, constraints)
182 183 184 185 186 187 188
            constraints.each_pair do |key, requirement|
              if path_params.include?(key) || key == :controller
                verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
                @requirements[key] = requirement
              else
                @conditions[key] = requirement
              end
189
            end
190
          end
191

192
          def normalize_format!(formatted)
193
            if formatted == true
194
              @requirements[:format] ||= /.+/
195 196
            elsif Regexp === formatted
              @requirements[:format] = formatted
197
              @defaults[:format] = nil
198 199
            elsif String === formatted
              @requirements[:format] = Regexp.compile(formatted)
200
              @defaults[:format] = formatted
201
            end
202
          end
203

Y
Yves Senn 已提交
204 205 206 207 208 209 210 211 212 213
          def verify_regexp_requirement(requirement)
            if requirement.source =~ ANCHOR_CHARACTERS_REGEX
              raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
            end

            if requirement.multiline?
              raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
            end
          end

214 215
          def normalize_defaults!(options)
            options.each_pair do |key, default|
A
Aaron Patterson 已提交
216
              unless Regexp === default
A
Akshay Vishnoi 已提交
217 218
                @defaults[key] = default
              end
219
            end
220
          end
221

222
          def verify_callable_constraint(callable_constraint)
223 224 225
            unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
              raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
            end
226 227
          end

228
          def add_request_method(via, conditions)
229 230 231 232 233 234 235 236 237
            return if via == [:all]

            if via.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 both GET and POST, add `via: [:get, :post]` option.\n" \
                    "If you want to expose your action to GET, use `get` in the router:\n" \
                    "  Instead of: match \"controller#action\"\n" \
                    "  Do: get \"controller#action\""
              raise ArgumentError, msg
238
            end
239 240

            conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
241 242
          end

243
          def app(blocks)
244 245
            return to if Redirect === to

246
            if to.respond_to?(:call)
247
              Constraints.new(to, blocks, false)
248
            else
249
              if blocks.any?
250
                Constraints.new(dispatcher(defaults), blocks, true)
251
              else
252
                dispatcher(defaults)
253
              end
254
            end
255 256
          end

257
          def check_controller_and_action(path_params, controller, action)
258
            hash = check_part(:controller, controller, path_params, {}) do |part|
A
Aaron Patterson 已提交
259
              translate_controller(part) {
260 261
                message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
                message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
262

A
Aaron Patterson 已提交
263 264
                raise ArgumentError, message
              }
265
            end
266

267
            check_part(:action, action, path_params, hash) { |part|
A
Aaron Patterson 已提交
268
              part.is_a?(Regexp) ? part : part.to_s
269
            }
270
          end
271

272
          def check_part(name, part, path_params, hash)
273
            if part
A
Aaron Patterson 已提交
274
              hash[name] = yield(part)
275
            else
276
              unless path_params.include?(name)
277
                message = "Missing :#{name} key on routes definition, please check your routes."
278 279
                raise ArgumentError, message
              end
280
            end
281
            hash
282
          end
283

284
          def split_to(to)
285
            case to
286 287
            when Symbol
              ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated.  Please change \"to: :#{to}\" to \"action: :#{to}\""
288 289
              [nil, to.to_s]
            when /#/    then to.split('#')
290 291
            when String
              ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated.  Please change \"to: :#{to}\" to \"controller: :#{to}\""
292 293 294
              [to, nil]
            else
              []
295
            end
296
          end
297

298
          def add_controller_module(controller, modyoule)
299 300
            if modyoule && !controller.is_a?(Regexp)
              if controller =~ %r{\A/}
301
                controller[1..-1]
302
              else
303
                [modyoule, controller].compact.join("/")
304
              end
305 306
            else
              controller
307 308 309
            end
          end

A
Aaron Patterson 已提交
310
          def translate_controller(controller)
311 312
            return controller if Regexp === controller
            return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
313

314
            yield
315 316
          end

317
          def blocks(options_constraints, scope_blocks)
318 319
            if options_constraints && !options_constraints.is_a?(Hash)
              verify_callable_constraint(options_constraints)
320
              [options_constraints]
321
            else
322
              scope_blocks || []
323 324
            end
          end
J
Joshua Peek 已提交
325

326 327
          def constraints(options, path_params)
            constraints = {}
328
            required_defaults = []
329
            options.each_pair do |key, option|
A
Aaron Patterson 已提交
330 331 332 333
              if Regexp === option
                constraints[key] = option
              else
                required_defaults << key unless path_params.include?(key)
334
              end
335
            end
336
            @conditions[:required_defaults] = required_defaults
337
            constraints
338
          end
J
Joshua Peek 已提交
339

A
Aaron Patterson 已提交
340 341
          def path_params(ast)
            ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
342
          end
343

344 345 346
          def path_ast(path)
            parser = Journey::Parser.new
            parser.parse path
347 348
          end

349 350
          def dispatcher(defaults)
            @set.dispatcher defaults
351 352
          end
      end
353

354
      # Invokes Journey::Router::Utils.normalize_path and ensure that
355 356
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
357
      def self.normalize_path(path)
358
        path = Journey::Router::Utils.normalize_path(path)
359
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
360 361 362
        path
      end

363
      def self.normalize_name(name)
364
        normalize_path(name)[1..-1].tr("/", "_")
365 366
      end

367
      module Base
368 369
        # You can specify what Rails should route "/" to with the root method:
        #
A
AvnerCohen 已提交
370
        #   root to: 'pages#main'
371
        #
372
        # For options, see +match+, as +root+ uses it internally.
373
        #
374 375 376 377
        # You can also pass a string which will expand
        #
        #   root 'pages#main'
        #
378 379 380
        # 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.
381
        def root(options = {})
382
          match '/', { :as => :root, :via => :get }.merge!(options)
383
        end
384

385
        # Matches a url pattern to one or more routes.
386
        #
387 388 389 390
        # You should not use the `match` method in your router
        # without specifying an HTTP method.
        #
        # If you want to expose your action to both GET and POST, use:
391
        #
392
        #   # sets :controller, :action and :id in params
393 394
        #   match ':controller/:action/:id', via: [:get, :post]
        #
395 396
        # Note that +:controller+, +:action+ and +:id+ are interpreted as url
        # query parameters and thus available through +params+ in an action.
397 398 399 400 401 402 403 404 405 406
        #
        # If you want to expose your action to GET, use `get` in the router:
        #
        # Instead of:
        #
        #   match ":controller/:action/:id"
        #
        # Do:
        #
        #   get ":controller/:action/:id"
407
        #
408 409 410 411
        # 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:
        #
412
        #   get 'songs/*category/:title', to: 'songs#show'
413 414 415 416 417
        #
        #   # 'songs/rock/classic/stairway-to-heaven' sets
        #   #  params[:category] = 'rock/classic'
        #   #  params[:title] = 'stairway-to-heaven'
        #
418 419 420 421
        # To match a wildcard parameter, it must have a name assigned to it.
        # Without a variable name to attach the glob parameter to, the route
        # can't be parsed.
        #
422 423
        # When a pattern points to an internal route, the route's +:action+ and
        # +:controller+ should be set in options or hash shorthand. Examples:
424
        #
425 426 427
        #   match 'photos/:id' => 'photos#show', via: :get
        #   match 'photos/:id', to: 'photos#show', via: :get
        #   match 'photos/:id', controller: 'photos', action: 'show', via: :get
428
        #
429 430 431
        # A pattern can also point to a +Rack+ endpoint i.e. anything that
        # responds to +call+:
        #
432 433
        #   match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
        #   match 'photos/:id', to: PhotoRackApp, via: :get
434
        #   # Yes, controller actions are just rack endpoints
435
        #   match 'photos/:id', to: PhotosController.action(:show), via: :get
436
        #
437 438 439
        # Because requesting various HTTP verbs with a single action has security
        # implications, you must either specify the actions in
        # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers]
440
        # instead +match+
441
        #
442
        # === Options
443
        #
444
        # Any options not seen here are passed on as params with the url.
445 446 447 448 449 450 451
        #
        # [:controller]
        #   The route's controller.
        #
        # [:action]
        #   The route's action.
        #
452 453 454 455 456 457
        # [:param]
        #   Overrides the default resource identifier `:id` (name of the
        #   dynamic segment used to generate the routes).
        #   You can access that segment from your controller using
        #   <tt>params[<:param>]</tt>.
        #
458 459 460 461 462 463
        # [:path]
        #   The path prefix for the routes.
        #
        # [:module]
        #   The namespace for :controller.
        #
464
        #     match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
465
        #     # => Sekret::PostsController
466 467 468 469 470 471 472 473 474
        #
        #   See <tt>Scoping#namespace</tt> for its scope equivalent.
        #
        # [:as]
        #   The name used to generate routing helpers.
        #
        # [:via]
        #   Allowed HTTP verb(s) for route.
        #
475 476 477
        #      match 'path', to: 'c#a', via: :get
        #      match 'path', to: 'c#a', via: [:get, :post]
        #      match 'path', to: 'c#a', via: :all
478 479
        #
        # [:to]
480 481
        #   Points to a +Rack+ endpoint. Can be an object that responds to
        #   +call+ or a string representing a controller's action.
482
        #
483 484 485
        #      match 'path', to: 'controller#action', via: :get
        #      match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
        #      match 'path', to: RackApp, via: :get
486 487 488
        #
        # [:on]
        #   Shorthand for wrapping routes in a specific RESTful context. Valid
489
        #   values are +:member+, +:collection+, and +:new+. Only use within
490 491 492
        #   <tt>resource(s)</tt> block. For example:
        #
        #      resource :bar do
493
        #        match 'foo', to: 'c#a', on: :member, via: [:get, :post]
494 495 496 497 498 499
        #      end
        #
        #   Is equivalent to:
        #
        #      resource :bar do
        #        member do
500
        #          match 'foo', to: 'c#a', via: [:get, :post]
501 502 503 504
        #        end
        #      end
        #
        # [:constraints]
Y
Yves Senn 已提交
505 506 507 508
        #   Constrains parameters with a hash of regular expressions
        #   or an object that responds to <tt>matches?</tt>. In addition, constraints
        #   other than path can also be specified with any object
        #   that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
509
        #
510
        #     match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
511
        #
512
        #     match 'json_only', constraints: { format: 'json' }, via: :get
Y
Yves Senn 已提交
513
        #
514
        #     class Whitelist
515 516
        #       def matches?(request) request.remote_ip == '1.2.3.4' end
        #     end
517
        #     match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
518 519 520 521 522 523 524 525
        #
        #   See <tt>Scoping#constraints</tt> for more examples with its scope
        #   equivalent.
        #
        # [:defaults]
        #   Sets defaults for parameters
        #
        #     # Sets params[:format] to 'jpg' by default
526
        #     match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
527 528
        #
        #   See <tt>Scoping#defaults</tt> for its scope equivalent.
529 530
        #
        # [:anchor]
531
        #   Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
532 533 534
        #   false, the pattern matches any request prefixed with the given path.
        #
        #     # Matches any request starting with 'path'
535
        #     match 'path', to: 'c#a', anchor: false, via: :get
536 537
        #
        # [:format]
538
        #   Allows you to specify the default value for optional +format+
V
Vijay Dev 已提交
539
        #   segment or disable it by supplying +false+.
540
        def match(path, options=nil)
541
        end
542

543 544
        # Mount a Rack-based application to be used within the application.
        #
A
AvnerCohen 已提交
545
        #   mount SomeRackApp, at: "some_route"
546 547 548
        #
        # Alternatively:
        #
R
Ryan Bigg 已提交
549
        #   mount(SomeRackApp => "some_route")
550
        #
551 552
        # For options, see +match+, as +mount+ uses it internally.
        #
553 554 555 556 557
        # 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 已提交
558
        #   mount(SomeRackApp => "some_route", as: "exciting")
559 560 561
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
562 563 564 565
        def mount(app, options = nil)
          if options
            path = options.delete(:at)
          else
566 567 568 569
            unless Hash === app
              raise ArgumentError, "must be called with mount point"
            end

570
            options = app
571
            app, path = options.find { |k, _| k.respond_to?(:call) }
572 573 574 575 576
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

A
Aaron Patterson 已提交
577 578 579 580 581 582 583 584 585
          rails_app = rails_app? app

          if rails_app
            options[:as]  ||= app.railtie_name
          else
            # non rails apps can't have an :as
            options[:as]  = nil
          end

586
          target_as       = name_for_action(options[:as], path)
P
Pratik Naik 已提交
587
          options[:via] ||= :all
588

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

A
Aaron Patterson 已提交
591
          define_generate_prefix(app, target_as) if rails_app
592 593 594
          self
        end

595 596 597 598
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
599

600 601 602 603 604 605
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

606 607 608 609 610
        # Query if the following named route was already defined.
        def has_named_route?(name)
          @set.named_routes.routes[name.to_sym]
        end

611
        private
A
Aaron Patterson 已提交
612 613
          def rails_app?(app)
            app.is_a?(Class) && app < Rails::Railtie
614 615 616
          end

          def define_generate_prefix(app, name)
617
            _route = @set.named_routes.get name
P
Piotr Sarnacki 已提交
618 619
            _routes = @set
            app.routes.define_mounted_helper(name)
620
            app.routes.extend Module.new {
A
Aaron Patterson 已提交
621
              def optimize_routes_generation?; false; end
622
              define_method :find_script_name do |options|
623 624 625 626 627 628 629
                if options.key? :script_name
                  super(options)
                else
                  prefix_options = options.slice(*_route.segment_keys)
                  # we must actually delete prefix segment keys to avoid passing them to next url_for
                  _route.segment_keys.each { |k| options.delete(k) }
                  _routes.url_helpers.send("#{name}_path", prefix_options)
630
                end
631
              end
632
            }
633
          end
634 635 636
      end

      module HttpHelpers
637
        # Define a route that only recognizes HTTP GET.
C
Cesar Carruitero 已提交
638
        # For supported arguments, see match[rdoc-ref:Base#match]
639
        #
A
AvnerCohen 已提交
640
        #   get 'bacon', to: 'food#bacon'
641
        def get(*args, &block)
642
          map_method(:get, args, &block)
643 644
        end

645
        # Define a route that only recognizes HTTP POST.
C
Cesar Carruitero 已提交
646
        # For supported arguments, see match[rdoc-ref:Base#match]
647
        #
A
AvnerCohen 已提交
648
        #   post 'bacon', to: 'food#bacon'
649
        def post(*args, &block)
650
          map_method(:post, args, &block)
651 652
        end

653
        # Define a route that only recognizes HTTP PATCH.
C
Cesar Carruitero 已提交
654
        # For supported arguments, see match[rdoc-ref:Base#match]
655
        #
A
AvnerCohen 已提交
656
        #   patch 'bacon', to: 'food#bacon'
657 658 659 660
        def patch(*args, &block)
          map_method(:patch, args, &block)
        end

661
        # Define a route that only recognizes HTTP PUT.
C
Cesar Carruitero 已提交
662
        # For supported arguments, see match[rdoc-ref:Base#match]
663
        #
A
AvnerCohen 已提交
664
        #   put 'bacon', to: 'food#bacon'
665
        def put(*args, &block)
666
          map_method(:put, args, &block)
667 668
        end

669
        # Define a route that only recognizes HTTP DELETE.
C
Cesar Carruitero 已提交
670
        # For supported arguments, see match[rdoc-ref:Base#match]
671
        #
A
AvnerCohen 已提交
672
        #   delete 'broccoli', to: 'food#broccoli'
673
        def delete(*args, &block)
674
          map_method(:delete, args, &block)
675 676 677
        end

        private
678
          def map_method(method, args, &block)
679
            options = args.extract_options!
680
            options[:via] = method
681
            match(*args, options, &block)
682 683 684 685
            self
          end
      end

686 687 688
      # 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 已提交
689 690
      # the <tt>app/controllers/admin</tt> directory, and you can group them
      # together in your router:
691 692 693 694
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
695
      #
696
      # This will create a number of routes for each of the posts and comments
S
Sebastian Martinez 已提交
697
      # controller. For <tt>Admin::PostsController</tt>, Rails will create:
698
      #
699 700 701 702 703
      #   GET       /admin/posts
      #   GET       /admin/posts/new
      #   POST      /admin/posts
      #   GET       /admin/posts/1
      #   GET       /admin/posts/1/edit
704
      #   PATCH/PUT /admin/posts/1
705
      #   DELETE    /admin/posts/1
706
      #
707
      # If you want to route /posts (without the prefix /admin) to
S
Sebastian Martinez 已提交
708
      # <tt>Admin::PostsController</tt>, you could use
709
      #
A
AvnerCohen 已提交
710
      #   scope module: "admin" do
711
      #     resources :posts
712 713 714
      #   end
      #
      # or, for a single case
715
      #
A
AvnerCohen 已提交
716
      #   resources :posts, module: "admin"
717
      #
S
Sebastian Martinez 已提交
718
      # If you want to route /admin/posts to +PostsController+
719
      # (without the <tt>Admin::</tt> module prefix), you could use
720
      #
721
      #   scope "/admin" do
722
      #     resources :posts
723 724 725
      #   end
      #
      # or, for a single case
726
      #
A
AvnerCohen 已提交
727
      #   resources :posts, path: "/admin/posts"
728 729 730
      #
      # 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 已提交
731
      # +PostsController+:
732
      #
733 734 735 736 737
      #   GET       /admin/posts
      #   GET       /admin/posts/new
      #   POST      /admin/posts
      #   GET       /admin/posts/1
      #   GET       /admin/posts/1/edit
738
      #   PATCH/PUT /admin/posts/1
739
      #   DELETE    /admin/posts/1
740
      module Scoping
741
        # Scopes a set of routes to the given default options.
742 743 744
        #
        # Take the following route definition as an example:
        #
A
AvnerCohen 已提交
745
        #   scope path: ":account_id", as: "account" do
746 747 748 749
        #     resources :projects
        #   end
        #
        # This generates helpers such as +account_projects_path+, just like +resources+ does.
750 751
        # The difference here being that the routes generated are like /:account_id/projects,
        # rather than /accounts/:account_id/projects.
752
        #
753
        # === Options
754
        #
755
        # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
756
        #
S
Sebastian Martinez 已提交
757
        #   # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
758
        #   scope module: "admin" do
759 760
        #     resources :posts
        #   end
761
        #
762
        #   # prefix the posts resource's requests with '/admin'
A
AvnerCohen 已提交
763
        #   scope path: "/admin" do
764 765
        #     resources :posts
        #   end
766
        #
S
Sebastian Martinez 已提交
767
        #   # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
A
AvnerCohen 已提交
768
        #   scope as: "sekret" do
769 770
        #     resources :posts
        #   end
771
        def scope(*args)
772
          options = args.extract_options!.dup
A
Aaron Patterson 已提交
773
          scope = {}
774

775
          options[:path] = args.flatten.join('/') if args.any?
776
          options[:constraints] ||= {}
777

778
          unless nested_scope?
779 780
            options[:shallow_path] ||= options[:path] if options.key?(:path)
            options[:shallow_prefix] ||= options[:as] if options.key?(:as)
781 782
          end

783
          if options[:constraints].is_a?(Hash)
784 785 786 787 788
            defaults = options[:constraints].select do
              |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
            end

            (options[:defaults] ||= {}).reverse_merge!(defaults)
789 790
          else
            block, options[:constraints] = options[:constraints], {}
791 792
          end

793
          @scope.options.each do |option|
794 795 796 797 798 799 800 801 802
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
              value = options.delete(option)
            end

            if value
A
Aaron Patterson 已提交
803
              scope[option] = send("merge_#{option}_scope", @scope[option], value)
804
            end
805 806
          end

A
Aaron Patterson 已提交
807
          @scope = @scope.new scope
808 809 810
          yield
          self
        ensure
A
Aaron Patterson 已提交
811
          @scope = @scope.parent
812 813
        end

814 815 816
        # Scopes routes to a specific controller
        #
        #   controller "food" do
A
AvnerCohen 已提交
817
        #     match "bacon", action: "bacon"
818
        #   end
819 820 821
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
822 823
        end

824 825 826 827 828 829 830 831
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
832 833 834 835 836
        #       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
837
        #        admin_post PATCH/PUT /admin/posts/:id(.:format)      admin/posts#update
838
        #        admin_post DELETE    /admin/posts/:id(.:format)      admin/posts#destroy
839
        #
840
        # === Options
841
        #
842 843
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
        # options all default to the name of the namespace.
844
        #
845 846
        # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
        # <tt>Resources#resources</tt>.
847
        #
848
        #   # accessible through /sekret/posts rather than /admin/posts
A
AvnerCohen 已提交
849
        #   namespace :admin, path: "sekret" do
850 851
        #     resources :posts
        #   end
852
        #
S
Sebastian Martinez 已提交
853
        #   # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
854
        #   namespace :admin, module: "sekret" do
855 856
        #     resources :posts
        #   end
857
        #
S
Sebastian Martinez 已提交
858
        #   # generates +sekret_posts_path+ rather than +admin_posts_path+
A
AvnerCohen 已提交
859
        #   namespace :admin, as: "sekret" do
860 861
        #     resources :posts
        #   end
862
        def namespace(path, options = {})
863
          path = path.to_s
864 865 866 867 868 869 870 871 872 873

          defaults = {
            module:         path,
            path:           options.fetch(:path, path),
            as:             options.fetch(:as, path),
            shallow_path:   options.fetch(:path, path),
            shallow_prefix: options.fetch(:as, path)
          }

          scope(defaults.merge!(options)) { yield }
874
        end
875

R
Ryan Bigg 已提交
876 877 878 879
        # === 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 已提交
880
        #   constraints(id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
881 882 883 884 885
        #     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.
886
        #
R
R.T. Lechow 已提交
887
        # You may use this to also restrict other parameters:
R
Ryan Bigg 已提交
888 889
        #
        #   resources :posts do
A
AvnerCohen 已提交
890
        #     constraints(post_id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
891 892
        #       resources :comments
        #     end
J
James Miller 已提交
893
        #   end
R
Ryan Bigg 已提交
894 895 896 897 898
        #
        # === Restricting based on IP
        #
        # Routes can also be constrained to an IP or a certain range of IP addresses:
        #
A
AvnerCohen 已提交
899
        #   constraints(ip: /192\.168\.\d+\.\d+/) do
R
Ryan Bigg 已提交
900 901 902 903 904 905 906 907
        #     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 已提交
908
        # Requests to routes can be constrained based on specific criteria:
R
Ryan Bigg 已提交
909 910 911 912 913 914 915 916 917 918
        #
        #    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
919
        #      def self.matches?(request)
R
Ryan Bigg 已提交
920 921 922 923 924 925 926 927 928 929 930
        #        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
931 932 933 934
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
935
        # Allows you to set default parameters for a route, such as this:
A
AvnerCohen 已提交
936 937
        #   defaults id: 'home' do
        #     match 'scoped_pages/(:id)', to: 'pages#show'
938
        #   end
R
Ryan Bigg 已提交
939
        # Using this, the +:id+ parameter here will default to 'home'.
940 941 942 943
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

944
        private
J
José Valim 已提交
945
          def merge_path_scope(parent, child) #:nodoc:
946
            Mapper.normalize_path("#{parent}/#{child}")
947 948
          end

J
José Valim 已提交
949
          def merge_shallow_path_scope(parent, child) #:nodoc:
950 951 952
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
953
          def merge_as_scope(parent, child) #:nodoc:
954
            parent ? "#{parent}_#{child}" : child
955 956
          end

J
José Valim 已提交
957
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
958 959 960
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
961
          def merge_module_scope(parent, child) #:nodoc:
962 963 964
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
965
          def merge_controller_scope(parent, child) #:nodoc:
966
            child
967 968
          end

969 970 971 972
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

J
José Valim 已提交
973
          def merge_path_names_scope(parent, child) #:nodoc:
974 975 976
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
977
          def merge_constraints_scope(parent, child) #:nodoc:
978 979 980
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
981
          def merge_defaults_scope(parent, child) #:nodoc:
982 983 984
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
985
          def merge_blocks_scope(parent, child) #:nodoc:
986 987 988
            merged = parent ? parent.dup : []
            merged << child if child
            merged
989 990
          end

J
José Valim 已提交
991
          def merge_options_scope(parent, child) #:nodoc:
992
            (parent || {}).except(*override_keys(child)).merge!(child)
993
          end
994

J
José Valim 已提交
995
          def merge_shallow_scope(parent, child) #:nodoc:
996 997
            child ? true : false
          end
998

J
José Valim 已提交
999
          def override_keys(child) #:nodoc:
1000 1001
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
1002 1003
      end

1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
      # 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 已提交
1028 1029
      # <tt>app/controllers/admin</tt> directory, and you can group them together
      # in your router:
1030 1031 1032 1033 1034
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
S
Sebastian Martinez 已提交
1035 1036
      # 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
1037 1038
      # overrides this restriction, e.g:
      #
A
AvnerCohen 已提交
1039
      #   resources :articles, id: /[^\/]+/
1040
      #
S
Sebastian Martinez 已提交
1041
      # This allows any character other than a slash as part of your +:id+.
1042
      #
J
Joshua Peek 已提交
1043
      module Resources
1044 1045
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
1046
        VALID_ON_OPTIONS  = [:new, :collection, :member]
1047
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except, :param, :concerns]
1048
        CANONICAL_ACTIONS = %w(index create new show update destroy)
1049 1050
        RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
        RESOURCE_SCOPES = [:resource, :resources]
1051

1052
        class Resource #:nodoc:
1053
          attr_reader :controller, :path, :options, :param
1054 1055

          def initialize(entities, options = {})
1056
            @name       = entities.to_s
1057 1058 1059
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
1060
            @param      = (options[:param] || :id).to_sym
1061
            @options    = options
1062
            @shallow    = false
1063 1064
          end

1065
          def default_actions
1066
            [:index, :create, :new, :show, :update, :destroy, :edit]
1067 1068
          end

1069
          def actions
1070
            if only = @options[:only]
1071
              Array(only).map(&:to_sym)
1072
            elsif except = @options[:except]
1073 1074 1075 1076 1077 1078
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

1079
          def name
1080
            @as || @name
1081 1082
          end

1083
          def plural
1084
            @plural ||= name.to_s
1085 1086 1087
          end

          def singular
1088
            @singular ||= name.to_s.singularize
1089 1090
          end

1091
          alias :member_name :singular
1092

1093
          # Checks for uncountable plurals, and appends "_index" if the plural
1094
          # and singular form are the same.
1095
          def collection_name
1096
            singular == plural ? "#{plural}_index" : plural
1097 1098
          end

1099
          def resource_scope
1100
            { :controller => controller }
1101 1102
          end

1103
          alias :collection_scope :path
1104 1105

          def member_scope
1106
            "#{path}/:#{param}"
1107 1108
          end

1109 1110
          alias :shallow_scope :member_scope

1111
          def new_scope(new_path)
1112
            "#{path}/#{new_path}"
1113 1114
          end

1115 1116 1117 1118
          def nested_param
            :"#{singular}_#{param}"
          end

1119
          def nested_scope
1120
            "#{path}/:#{nested_param}"
1121
          end
1122

1123 1124 1125 1126 1127 1128 1129
          def shallow=(value)
            @shallow = value
          end

          def shallow?
            @shallow
          end
1130 1131 1132
        end

        class SingletonResource < Resource #:nodoc:
1133
          def initialize(entities, options)
1134
            super
1135
            @as         = nil
1136 1137
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1138 1139
          end

1140 1141 1142 1143
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

1144 1145
          def plural
            @plural ||= name.to_s.pluralize
1146 1147
          end

1148 1149
          def singular
            @singular ||= name.to_s
1150
          end
1151 1152 1153 1154 1155 1156

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1157 1158
        end

1159 1160 1161 1162
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

1163 1164 1165 1166 1167 1168
        # 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:
        #
1169
        #   resource :profile
1170 1171
        #
        # creates six different routes in your application, all mapping to
1172
        # the +Profiles+ controller (note that the controller is named after
1173 1174
        # the plural):
        #
1175 1176 1177 1178 1179 1180
        #   GET       /profile/new
        #   POST      /profile
        #   GET       /profile
        #   GET       /profile/edit
        #   PATCH/PUT /profile
        #   DELETE    /profile
1181
        #
1182
        # === Options
1183
        # Takes same options as +resources+.
J
Joshua Peek 已提交
1184
        def resource(*resources, &block)
1185
          options = resources.extract_options!.dup
J
Joshua Peek 已提交
1186

1187
          if apply_common_behavior_for(:resource, resources, options, &block)
1188 1189 1190
            return self
          end

1191
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1192
            yield if block_given?
1193

1194 1195
            concerns(options[:concerns]) if options[:concerns]

1196
            collection do
1197
              post :create
1198
            end if parent_resource.actions.include?(:create)
1199

1200
            new do
1201
              get :new
1202
            end if parent_resource.actions.include?(:new)
1203

1204
            set_member_mappings_for_resource
1205 1206
          end

J
Joshua Peek 已提交
1207
          self
1208 1209
        end

1210 1211 1212 1213 1214 1215 1216 1217
        # 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 已提交
1218
        # the +Photos+ controller:
1219
        #
1220 1221 1222 1223 1224
        #   GET       /photos
        #   GET       /photos/new
        #   POST      /photos
        #   GET       /photos/:id
        #   GET       /photos/:id/edit
1225
        #   PATCH/PUT /photos/:id
1226
        #   DELETE    /photos/:id
1227
        #
1228 1229 1230 1231 1232 1233 1234 1235
        # Resources can also be nested infinitely by using this block syntax:
        #
        #   resources :photos do
        #     resources :comments
        #   end
        #
        # This generates the following comments routes:
        #
1236 1237 1238 1239 1240
        #   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
1241
        #   PATCH/PUT /photos/:photo_id/comments/:id
1242
        #   DELETE    /photos/:photo_id/comments/:id
1243
        #
1244
        # === Options
1245 1246
        # Takes same options as <tt>Base#match</tt> as well as:
        #
1247
        # [:path_names]
A
Aviv Ben-Yosef 已提交
1248 1249
        #   Allows you to change the segment component of the +edit+ and +new+ actions.
        #   Actions not specified are not changed.
1250
        #
A
AvnerCohen 已提交
1251
        #     resources :posts, path_names: { new: "brand_new" }
1252 1253
        #
        #   The above example will now change /posts/new to /posts/brand_new
1254
        #
1255 1256 1257
        # [:path]
        #   Allows you to change the path prefix for the resource.
        #
A
AvnerCohen 已提交
1258
        #     resources :posts, path: 'postings'
1259 1260 1261
        #
        #   The resource and all segments will now route to /postings instead of /posts
        #
1262 1263
        # [:only]
        #   Only generate routes for the given actions.
1264
        #
A
AvnerCohen 已提交
1265 1266
        #     resources :cows, only: :show
        #     resources :cows, only: [:show, :index]
1267
        #
1268 1269
        # [:except]
        #   Generate all routes except for the given actions.
1270
        #
A
AvnerCohen 已提交
1271 1272
        #     resources :cows, except: :show
        #     resources :cows, except: [:show, :index]
1273 1274 1275 1276 1277
        #
        # [:shallow]
        #   Generates shallow routes for nested resource(s). When placed on a parent resource,
        #   generates shallow routes for all nested resources.
        #
A
AvnerCohen 已提交
1278
        #     resources :posts, shallow: true do
1279 1280 1281 1282 1283 1284
        #       resources :comments
        #     end
        #
        #   Is the same as:
        #
        #     resources :posts do
A
AvnerCohen 已提交
1285
        #       resources :comments, except: [:show, :edit, :update, :destroy]
1286
        #     end
A
AvnerCohen 已提交
1287
        #     resources :comments, only: [:show, :edit, :update, :destroy]
1288 1289 1290 1291
        #
        #   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>.
1292 1293 1294 1295
        #
        # [:shallow_path]
        #   Prefixes nested shallow routes with the specified path.
        #
A
AvnerCohen 已提交
1296
        #     scope shallow_path: "sekret" do
1297
        #       resources :posts do
A
AvnerCohen 已提交
1298
        #         resources :comments, shallow: true
1299
        #       end
1300 1301 1302 1303
        #     end
        #
        #   The +comments+ resource here will have the following routes generated for it:
        #
1304 1305 1306 1307 1308
        #     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)
1309
        #     comment          PATCH/PUT /sekret/comments/:id(.:format)
1310
        #     comment          DELETE    /sekret/comments/:id(.:format)
1311
        #
1312 1313 1314
        # [:shallow_prefix]
        #   Prefixes nested shallow route names with specified prefix.
        #
A
AvnerCohen 已提交
1315
        #     scope shallow_prefix: "sekret" do
1316
        #       resources :posts do
A
AvnerCohen 已提交
1317
        #         resources :comments, shallow: true
1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330
        #       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)
        #
1331
        # [:format]
1332
        #   Allows you to specify the default value for optional +format+
V
Vijay Dev 已提交
1333
        #   segment or disable it by supplying +false+.
1334
        #
1335
        # === Examples
1336
        #
S
Sebastian Martinez 已提交
1337
        #   # routes call <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
1338
        #   resources :posts, module: "admin"
1339
        #
1340
        #   # resource actions are at /admin/posts.
A
AvnerCohen 已提交
1341
        #   resources :posts, path: "admin/posts"
J
Joshua Peek 已提交
1342
        def resources(*resources, &block)
1343
          options = resources.extract_options!.dup
1344

1345
          if apply_common_behavior_for(:resources, resources, options, &block)
1346 1347 1348
            return self
          end

1349
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1350
            yield if block_given?
J
Joshua Peek 已提交
1351

1352 1353
            concerns(options[:concerns]) if options[:concerns]

1354
            collection do
1355 1356
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1357
            end
1358

1359
            new do
1360
              get :new
1361
            end if parent_resource.actions.include?(:new)
1362

1363
            set_member_mappings_for_resource
1364 1365
          end

J
Joshua Peek 已提交
1366
          self
1367 1368
        end

1369 1370 1371 1372 1373 1374 1375 1376 1377
        # 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 已提交
1378
        # with GET, and route to the search action of +PhotosController+. It will also
1379 1380
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1381
        def collection
1382 1383
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1384 1385
          end

1386 1387 1388 1389
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1390
          end
1391
        end
J
Joshua Peek 已提交
1392

1393 1394 1395 1396 1397 1398 1399 1400 1401
        # 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 已提交
1402
        # preview action of +PhotosController+. It will also create the
1403
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1404
        def member
1405 1406
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1407
          end
J
Joshua Peek 已提交
1408

1409
          with_scope_level(:member) do
1410 1411 1412 1413
            if shallow?
              shallow_scope(parent_resource.member_scope) { yield }
            else
              scope(parent_resource.member_scope) { yield }
1414
            end
1415 1416 1417 1418 1419 1420 1421
          end
        end

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

1423 1424 1425 1426
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1427
          end
J
Joshua Peek 已提交
1428 1429
        end

1430
        def nested
1431 1432
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1433 1434 1435
          end

          with_scope_level(:nested) do
1436
            if shallow? && shallow_nesting_depth >= 1
1437
              shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1438
            else
1439
              scope(parent_resource.nested_scope, nested_options) { yield }
1440 1441 1442 1443
            end
          end
        end

1444
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1445
        def namespace(path, options = {})
1446
          if resource_scope?
1447 1448 1449 1450 1451 1452
            nested { super }
          else
            super
          end
        end

1453
        def shallow
1454
          scope(:shallow => true) do
1455 1456 1457 1458
            yield
          end
        end

1459 1460 1461 1462
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

1463
        # match 'path' => 'controller#action'
R
Rafael Mendonça França 已提交
1464
        # match 'path', to: 'controller#action'
1465
        # match 'path', 'otherpath', on: :member, via: :get
1466 1467 1468
        def match(path, *rest)
          if rest.empty? && Hash === path
            options  = path
1469
            path, to = options.find { |name, _value| name.is_a?(String) }
1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483

            case to
            when Symbol
              options[:action] = to
            when String
              if to =~ /#/
                options[:to] = to
              else
                options[:controller] = to
              end
            else
              options[:to] = to
            end

1484
            options.delete(path)
1485 1486 1487 1488 1489 1490
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1491 1492
          options[:anchor] = true unless options.key?(:anchor)

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

1497 1498 1499 1500
          if @scope[:controller] && @scope[:action]
            options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
          end

1501 1502 1503
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
1504 1505 1506 1507

            path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
            if using_match_shorthand?(path_without_format, route_options)
              route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
1508
              route_options[:to].tr!("-", "_")
1509 1510
            end

1511 1512
            decomposed_match(_path, route_options)
          end
1513 1514
          self
        end
1515

1516 1517 1518 1519
        def using_match_shorthand?(path, options)
          path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
        end

1520
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1521 1522
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1523
          else
A
Aaron Patterson 已提交
1524 1525 1526 1527 1528 1529 1530 1531
            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 已提交
1532
          end
1533
        end
J
Joshua Peek 已提交
1534

1535
        def add_route(action, options) # :nodoc:
1536
          path = path_for_action(action, options.delete(:path))
1537 1538
          raise ArgumentError, "path is required" if path.blank?

1539
          action = action.to_s.dup
1540

1541
          if action =~ /^[\w\-\/]+$/
1542
            options[:action] ||= action.tr('-', '_') unless action.include?("/")
1543
          else
1544 1545 1546
            action = nil
          end

1547 1548 1549 1550 1551
          as = if !options.fetch(:as, true) # if it's set to nil or false
                 options.delete(:as)
               else
                 name_for_action(options.delete(:as), action)
               end
J
Joshua Peek 已提交
1552

1553
          mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
1554 1555
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1556 1557
        end

1558 1559 1560 1561 1562 1563 1564 1565 1566
        def root(path, options={})
          if path.is_a?(String)
            options[:to] = path
          elsif path.is_a?(Hash) and options.empty?
            options = path
          else
            raise ArgumentError, "must be called with a path and/or options"
          end

1567
          if @scope[:scope_level] == :resources
1568 1569
            with_scope_level(:root) do
              scope(parent_resource.path) do
1570 1571 1572 1573 1574 1575
                super(options)
              end
            end
          else
            super(options)
          end
1576 1577
        end

1578
        protected
1579

1580
          def parent_resource #:nodoc:
1581 1582 1583
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1584
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1585 1586 1587 1588 1589
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1590 1591 1592 1593 1594 1595 1596
            if options.delete(:shallow)
              shallow do
                send(method, resources.pop, options, &block)
              end
              return true
            end

1597 1598 1599 1600 1601
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1602
            options.keys.each do |k|
1603 1604 1605
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1606 1607 1608
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1609 1610 1611 1612 1613
                send(method, resources.pop, options, &block)
              end
              return true
            end

1614 1615 1616 1617
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1618 1619 1620
            false
          end

J
José Valim 已提交
1621
          def action_options?(options) #:nodoc:
1622 1623 1624
            options[:only] || options[:except]
          end

J
José Valim 已提交
1625
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1626
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1627 1628
          end

J
José Valim 已提交
1629
          def scope_action_options #:nodoc:
1630 1631 1632
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1633
          def resource_scope? #:nodoc:
1634
            RESOURCE_SCOPES.include? @scope[:scope_level]
1635 1636
          end

1637 1638
          def resource_method_scope?(scope_level) #:nodoc:
            RESOURCE_METHOD_SCOPES.include? scope_level
1639 1640
          end

1641 1642 1643 1644
          def nested_scope? #:nodoc:
            @scope[:scope_level] == :nested
          end

1645
          def with_exclusive_scope
1646
            begin
A
Aaron Patterson 已提交
1647
              @scope = @scope.new(:as => nil, :path => nil)
1648

1649 1650 1651
              with_scope_level(:exclusive) do
                yield
              end
1652
            ensure
A
Aaron Patterson 已提交
1653
              @scope = @scope.parent
1654 1655 1656
            end
          end

1657
          def with_scope_level(kind)
A
Aaron Patterson 已提交
1658
            @scope = @scope.new(:scope_level => kind)
J
Joshua Peek 已提交
1659 1660
            yield
          ensure
A
Aaron Patterson 已提交
1661
            @scope = @scope.parent
J
Joshua Peek 已提交
1662
          end
1663

1664
          def resource_scope(kind, resource) #:nodoc:
1665
            resource.shallow = @scope[:shallow]
A
Aaron Patterson 已提交
1666
            @scope = @scope.new(:scope_level_resource => resource)
1667
            @nesting.push(resource)
1668 1669 1670

            with_scope_level(kind) do
              scope(parent_resource.resource_scope) { yield }
1671
            end
1672
          ensure
1673
            @nesting.pop
A
Aaron Patterson 已提交
1674
            @scope = @scope.parent
1675 1676
          end

J
José Valim 已提交
1677
          def nested_options #:nodoc:
1678 1679
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1680 1681
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1682 1683

            options
1684 1685
          end

1686 1687 1688 1689
          def nesting_depth #:nodoc:
            @nesting.size
          end

1690 1691 1692 1693
          def shallow_nesting_depth #:nodoc:
            @nesting.select(&:shallow?).size
          end

1694 1695
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1696 1697
          end

1698 1699
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1700 1701
          end

1702 1703
          def canonical_action?(action, scope_level) #:nodoc:
            scope_level && resource_method_scope?(scope_level) && CANONICAL_ACTIONS.include?(action.to_s)
1704 1705
          end

1706
          def shallow_scope(path, options = {}) #:nodoc:
A
Aaron Patterson 已提交
1707 1708 1709
            scope = { :as   => @scope[:shallow_prefix],
                      :path => @scope[:shallow_path] }
            @scope = @scope.new scope
1710 1711 1712

            scope(path, options) { yield }
          ensure
A
Aaron Patterson 已提交
1713
            @scope = @scope.parent
1714 1715
          end

J
José Valim 已提交
1716
          def path_for_action(action, path) #:nodoc:
1717
            if path.blank? && canonical_action?(action, @scope[:scope_level])
1718
              @scope[:path].to_s
1719
            else
1720
              "#{@scope[:path]}/#{action_path(action, path)}"
1721 1722 1723
            end
          end

J
José Valim 已提交
1724
          def action_path(name, path = nil) #:nodoc:
1725
            name = name.to_sym if name.is_a?(String)
1726
            path || @scope[:path_names][name] || name.to_s
1727 1728
          end

J
José Valim 已提交
1729
          def prefix_name_for_action(as, action) #:nodoc:
1730
            if as
1731
              prefix = as
1732
            elsif !canonical_action?(action, @scope[:scope_level])
1733
              prefix = action
1734
            end
A
Aaron Patterson 已提交
1735 1736 1737 1738

            if prefix
              Mapper.normalize_name prefix.to_s.tr('-', '_')
            end
1739 1740
          end

J
José Valim 已提交
1741
          def name_for_action(as, action) #:nodoc:
1742
            prefix = prefix_name_for_action(as, action)
1743 1744 1745
            name_prefix = @scope[:as]

            if parent_resource
1746
              return nil unless as || action
1747

1748 1749
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1750
            end
1751

1752
            name = case @scope[:scope_level]
1753
            when :nested
1754
              [name_prefix, prefix]
1755
            when :collection
1756
              [prefix, name_prefix, collection_name]
1757
            when :new
1758 1759
              [prefix, :new, name_prefix, member_name]
            when :member
1760
              [prefix, name_prefix, member_name]
1761 1762
            when :root
              [name_prefix, collection_name, prefix]
1763
            else
1764
              [name_prefix, member_name, prefix]
1765
            end
1766

1767 1768 1769 1770 1771
            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?
A
Aaron Patterson 已提交
1772
                candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
1773 1774 1775 1776
              else
                candidate
              end
            end
1777
          end
1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789

          def set_member_mappings_for_resource
            member do
              get :edit if parent_resource.actions.include?(:edit)
              get :show if parent_resource.actions.include?(:show)
              if parent_resource.actions.include?(:update)
                patch :update
                put   :update
              end
              delete :destroy if parent_resource.actions.include?(:destroy)
            end
          end
J
Joshua Peek 已提交
1790
      end
J
Joshua Peek 已提交
1791

1792
      # Routing Concerns allow you to declare common routes that can be reused
1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811
      # 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
1812
      module Concerns
1813
        # Define a routing concern using a name.
1814
        #
1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836
        # 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]
1837 1838
        #   end
        #
1839 1840 1841
        # Or, using a callable object, you might implement something more
        # specific to your application, which would be out of place in your
        # routes file.
1842
        #
1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853
        #   # 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]
1854 1855 1856
        #     end
        #   end
        #
1857 1858 1859 1860 1861 1862 1863 1864
        #   # routes.rb
        #   concern :purchasable, Purchasable.new(returnable: true)
        #
        #   resources :toys, concerns: :purchasable
        #   resources :electronics, concerns: :purchasable
        #   resources :pets do
        #     concerns :purchasable, returnable: false
        #   end
1865
        #
1866 1867 1868
        # 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>.
1869
        def concern(name, callable = nil, &block)
1870 1871
          callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
          @concerns[name] = callable
1872 1873
        end

1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884
        # 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
1885 1886 1887
        def concerns(*args)
          options = args.extract_options!
          args.flatten.each do |name|
1888
            if concern = @concerns[name]
1889
              concern.call(self, options)
1890 1891 1892 1893 1894 1895 1896
            else
              raise ArgumentError, "No concern named #{name} was found!"
            end
          end
        end
      end

A
Aaron Patterson 已提交
1897
      class Scope # :nodoc:
1898 1899 1900 1901
        OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
                   :controller, :action, :path_names, :constraints,
                   :shallow, :blocks, :defaults, :options]

A
Aaron Patterson 已提交
1902 1903 1904 1905 1906 1907 1908
        attr_reader :parent

        def initialize(hash, parent = {})
          @hash = hash
          @parent = parent
        end

1909 1910 1911 1912
        def options
          OPTIONS
        end

A
Aaron Patterson 已提交
1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925
        def new(hash)
          self.class.new hash, self
        end

        def [](key)
          @hash.fetch(key) { @parent[key] }
        end

        def []=(k,v)
          @hash[k] = v
        end
      end

1926 1927
      def initialize(set) #:nodoc:
        @set = set
A
Aaron Patterson 已提交
1928
        @scope = Scope.new({ :path_names => @set.resources_path_names })
1929
        @concerns = {}
1930
        @nesting = []
1931 1932
      end

1933 1934
      include Base
      include HttpHelpers
1935
      include Redirection
1936
      include Scoping
1937
      include Concerns
1938
      include Resources
J
Joshua Peek 已提交
1939 1940
    end
  end
J
Joshua Peek 已提交
1941
end