mapper.rb 64.8 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, 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, options
78 79
        end

80
        def initialize(scope, set, path, defaults, 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 89
          @as                 = options.delete :as
          @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 617
          end

          def define_generate_prefix(app, name)
            _route = @set.named_routes.routes[name.to_sym]
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 623
              define_method :find_script_name do |options|
                super(options) || begin
P
Piotr Sarnacki 已提交
624
                prefix_options = options.slice(*_route.segment_keys)
625 626
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
627
                _routes.url_helpers.send("#{name}_path", prefix_options)
628
                end
629
              end
630
            }
631
          end
632 633 634
      end

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

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

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

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

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

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

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

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

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

781
          if options[:constraints].is_a?(Hash)
782 783 784 785 786
            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)
787 788
          else
            block, options[:constraints] = options[:constraints], {}
789 790
          end

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

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

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

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

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

          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 }
872
        end
873

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

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

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

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

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

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

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

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

967 968 969 970
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1089
          alias :member_name :singular
1090

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

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

1101
          alias :collection_scope :path
1102 1103

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

1107 1108
          alias :shallow_scope :member_scope

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

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

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

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

          def shallow?
            @shallow
          end
1128 1129 1130
        end

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

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

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

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

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1155 1156
        end

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

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

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

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

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

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

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

1202
            set_member_mappings_for_resource
1203 1204
          end

J
Joshua Peek 已提交
1205
          self
1206 1207
        end

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

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

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

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

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

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

1361
            set_member_mappings_for_resource
1362 1363
          end

J
Joshua Peek 已提交
1364
          self
1365 1366
        end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1537
          action = action.to_s.dup
1538

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

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

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

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

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

1576
        protected
1577

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

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

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

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

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

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

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

1616 1617 1618
            false
          end

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

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

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

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

J
José Valim 已提交
1635
          def resource_method_scope? #:nodoc:
1636
            RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1637 1638
          end

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

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

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

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

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

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

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

            options
1682 1683
          end

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

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

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

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

J
José Valim 已提交
1700
          def canonical_action?(action, flag) #:nodoc:
1701
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1702 1703
          end

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

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

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

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

J
José Valim 已提交
1727
          def prefix_name_for_action(as, action) #:nodoc:
1728
            if as
1729
              prefix = as
1730
            elsif !canonical_action?(action, @scope[:scope_level])
1731
              prefix = action
1732
            end
1733
            prefix.to_s.tr('-', '_') if prefix
1734 1735
          end

J
José Valim 已提交
1736
          def name_for_action(as, action) #:nodoc:
1737
            prefix = prefix_name_for_action(as, action)
1738
            prefix = Mapper.normalize_name(prefix) if prefix
1739 1740 1741
            name_prefix = @scope[:as]

            if parent_resource
1742
              return nil unless as || action
1743

1744 1745
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1746
            end
1747

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

1763 1764 1765 1766 1767
            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?
1768
                candidate unless @set.named_routes.key?(candidate) || candidate !~ /\A[_a-z]/i
1769 1770 1771 1772
              else
                candidate
              end
            end
1773
          end
1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785

          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 已提交
1786
      end
J
Joshua Peek 已提交
1787

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

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

A
Aaron Patterson 已提交
1893
      class Scope # :nodoc:
1894 1895 1896 1897
        OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
                   :controller, :action, :path_names, :constraints,
                   :shallow, :blocks, :defaults, :options]

A
Aaron Patterson 已提交
1898 1899 1900 1901 1902 1903 1904
        attr_reader :parent

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

1905 1906 1907 1908
        def options
          OPTIONS
        end

A
Aaron Patterson 已提交
1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921
        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

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

1929 1930
      include Base
      include HttpHelpers
1931
      include Redirection
1932
      include Scoping
1933
      include Concerns
1934
      include Resources
J
Joshua Peek 已提交
1935 1936
    end
  end
J
Joshua Peek 已提交
1937
end