mapper.rb 65.3 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
        RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1050

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

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

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

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

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

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

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

1090
          alias :member_name :singular
1091

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

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

1102
          alias :collection_scope :path
1103 1104

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

1108 1109
          alias :shallow_scope :member_scope

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

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

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

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

          def shallow?
            @shallow
          end
1129 1130 1131
        end

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

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

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

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

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1156 1157
        end

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

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

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

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

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

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

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

1203
            set_member_mappings_for_resource
1204 1205
          end

J
Joshua Peek 已提交
1206
          self
1207 1208
        end

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

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

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

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

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

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

1362
            set_member_mappings_for_resource
1363 1364
          end

J
Joshua Peek 已提交
1365
          self
1366 1367
        end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            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')
1507
              route_options[:to].tr!("-", "_")
1508 1509
            end

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

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

1519
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1520 1521
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1522
          else
1523
            case @scope.scope_level
A
Aaron Patterson 已提交
1524 1525 1526 1527 1528 1529 1530
            when :resources
              nested { decomposed_match(path, options) }
            when :resource
              member { decomposed_match(path, options) }
            else
              add_route(path, options)
            end
J
Joshua Peek 已提交
1531
          end
1532
        end
J
Joshua Peek 已提交
1533

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

1538
          action = action.to_s.dup
1539

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

1546 1547 1548 1549 1550
          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 已提交
1551

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

1557 1558 1559 1560 1561 1562 1563 1564 1565
        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

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

1577
        protected
1578

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

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

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

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

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

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

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

1617 1618 1619
            false
          end

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

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

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

J
José Valim 已提交
1632
          def resource_scope? #:nodoc:
1633
            @scope.resource_scope?
1634 1635
          end

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

1640
          def nested_scope? #:nodoc:
1641
            @scope.nested?
1642 1643
          end

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

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

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

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

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

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

            options
1683 1684
          end

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

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

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

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

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

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

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

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

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

A
Aaron Patterson 已提交
1728
          def prefix_name_for_action(as, action, scope_level) #:nodoc:
1729
            if as
1730
              prefix = as
A
Aaron Patterson 已提交
1731
            elsif !canonical_action?(action, scope_level)
1732
              prefix = action
1733
            end
A
Aaron Patterson 已提交
1734

A
Aaron Patterson 已提交
1735
            if prefix && prefix != '/' && !prefix.empty?
A
Aaron Patterson 已提交
1736 1737
              Mapper.normalize_name prefix.to_s.tr('-', '_')
            end
1738 1739
          end

J
José Valim 已提交
1740
          def name_for_action(as, action) #:nodoc:
1741
            scope_level = @scope.scope_level
A
Aaron Patterson 已提交
1742
            prefix = prefix_name_for_action(as, action, scope_level)
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

A
Aaron Patterson 已提交
1752
            name = case 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

A
Aaron Patterson 已提交
1767
            if candidate = name.compact.join("_").presence
1768 1769 1770 1771
              # 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]

1902 1903
        RESOURCE_SCOPES = [:resource, :resources]

A
Aaron Patterson 已提交
1904 1905 1906 1907 1908 1909 1910
        attr_reader :parent

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

1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922
        def scope_level
          self[:scope_level]
        end

        def nested?
          scope_level == :nested
        end

        def resource_scope?
          RESOURCE_SCOPES.include? scope_level
        end

1923 1924 1925 1926
        def options
          OPTIONS
        end

A
Aaron Patterson 已提交
1927 1928 1929 1930
        def new(hash)
          self.class.new hash, self
        end

1931 1932 1933 1934
        def new_level(level)
          new(:scope_level => level)
        end

A
Aaron Patterson 已提交
1935 1936 1937 1938 1939 1940 1941 1942 1943
        def [](key)
          @hash.fetch(key) { @parent[key] }
        end

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

1944 1945
      def initialize(set) #:nodoc:
        @set = set
A
Aaron Patterson 已提交
1946
        @scope = Scope.new({ :path_names => @set.resources_path_names })
1947
        @concerns = {}
1948
        @nesting = []
1949 1950
      end

1951 1952
      include Base
      include HttpHelpers
1953
      include Redirection
1954
      include Scoping
1955
      include Concerns
1956
      include Resources
J
Joshua Peek 已提交
1957 1958
    end
  end
J
Joshua Peek 已提交
1959
end