mapper.rb 64.6 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
      URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
16
      SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
17 18
                       :controller, :action, :path_names, :constraints,
                       :shallow, :blocks, :defaults, :options]
19

20
      class Constraints < Endpoint #:nodoc:
21
        attr_reader :app, :constraints
22

23
        def initialize(app, constraints, dispatcher_p)
24 25 26 27
          # 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.
28
          if app.is_a?(self.class)
29 30 31 32
            constraints += app.constraints
            app = app.app
          end

33
          @dispatcher = dispatcher_p
34

35
          @app, @constraints, = app, constraints
36 37
        end

38 39
        def dispatcher?; @dispatcher; end

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

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

          if dispatcher?
            @app.serve req
          else
            @app.call req.env
          end
55
        end
56 57 58

        private
          def constraint_args(constraint, request)
59
            constraint.arity == 1 ? [request] : [request.path_parameters, request]
60
          end
61 62
      end

63
      class Mapping #:nodoc:
64
        ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
65

A
Aaron Patterson 已提交
66
        attr_reader :requirements, :conditions, :defaults
67
        attr_reader :to, :default_controller, :default_action, :as, :anchor
68

69
        def self.build(scope, set, path, options)
70 71 72 73 74 75 76
          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
77

78 79
          defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}

80
          new scope, set, path, defaults, options
81 82
        end

83
        def initialize(scope, set, path, defaults, options)
84 85
          @requirements, @conditions = {}, {}
          @defaults = defaults
86
          @set = set
87

88
          @to                 = options.delete :to
89 90
          @default_controller = options.delete(:controller) || scope[:controller]
          @default_action     = options.delete(:action) || scope[:action]
91 92
          @as                 = options.delete :as
          @anchor             = options.delete :anchor
93

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

98
          path = normalize_path! path, formatted
99
          ast  = path_ast path
A
Aaron Patterson 已提交
100
          path_params = path_params ast
101

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

A
Aaron Patterson 已提交
104

105 106
          split_constraints(path_params, scope[:constraints]) if scope[:constraints]
          constraints = constraints(options, path_params)
107

108 109
          split_constraints path_params, constraints

110 111
          @blocks = blocks(options_constraints, scope[:blocks])

112 113 114 115 116 117 118 119 120 121
          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)
122 123 124 125

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

126
          add_request_method(via, @conditions)
127
          normalize_defaults!(options)
128
        end
J
Joshua Peek 已提交
129

130
        def to_route
131
          [ app(@blocks), conditions, requirements, defaults, as, anchor ]
132
        end
J
Joshua Peek 已提交
133

134
        private
135

136 137
          def normalize_path!(path, format)
            path = Mapper.normalize_path(path)
138

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

148 149
          def optional_format?(path, format)
            format != false && !path.include?(':format') && !path.end_with?('/')
150 151
          end

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

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

              # 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' }
168
              options[:controller] ||= /.+?/
169
            end
170

A
Aaron Patterson 已提交
171 172 173
            if to.respond_to? :call
              options
            else
174 175 176 177 178 179 180
              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 已提交
181
            end
182 183
          end

184
          def split_constraints(path_params, constraints)
185 186 187 188 189 190 191
            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
192
            end
193
          end
194

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

Y
Yves Senn 已提交
207 208 209 210 211 212 213 214 215 216
          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

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

225
          def verify_callable_constraint(callable_constraint)
226 227 228
            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
229 230
          end

231
          def add_request_method(via, conditions)
232 233 234 235 236 237 238 239 240
            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
241
            end
242 243

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

246
          def app(blocks)
247 248
            return to if Redirect === to

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

260
          def check_controller_and_action(path_params, controller, action)
261
            hash = check_part(:controller, controller, path_params, {}) do |part|
A
Aaron Patterson 已提交
262
              translate_controller(part) {
263 264
                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"
265

A
Aaron Patterson 已提交
266 267
                raise ArgumentError, message
              }
268
            end
269

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

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

287
          def split_to(to)
288
            case to
289 290
            when Symbol
              ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated.  Please change \"to: :#{to}\" to \"action: :#{to}\""
291 292
              [nil, to.to_s]
            when /#/    then to.split('#')
293 294
            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}\""
295 296 297
              [to, nil]
            else
              []
298
            end
299
          end
300

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

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

317
            yield
318 319
          end

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

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

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

347 348 349
          def path_ast(path)
            parser = Journey::Parser.new
            parser.parse path
350 351
          end

352 353
          def dispatcher(defaults)
            @set.dispatcher defaults
354 355
          end
      end
356

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

366
      def self.normalize_name(name)
367
        normalize_path(name)[1..-1].tr("/", "_")
368 369
      end

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

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

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

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

          raise "A rack application must be specified" unless path

A
Aaron Patterson 已提交
580 581 582 583 584 585 586 587 588
          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

589
          target_as       = name_for_action(options[:as], path)
P
Pratik Naik 已提交
590
          options[:via] ||= :all
591

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

A
Aaron Patterson 已提交
594
          define_generate_prefix(app, target_as) if rails_app
595 596 597
          self
        end

598 599 600 601
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
602

603 604 605 606 607 608
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

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

614
        private
A
Aaron Patterson 已提交
615 616
          def rails_app?(app)
            app.is_a?(Class) && app < Rails::Railtie
617 618 619 620
          end

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

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

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

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

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

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

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

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

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

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

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

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

            if value
804 805 806
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
807 808 809 810 811
          end

          yield
          self
        ensure
812
          @scope.merge!(recover)
813 814
        end

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

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

          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 }
875
        end
876

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1092
          alias :member_name :singular
1093

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

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

1104
          alias :collection_scope :path
1105 1106

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

1110 1111
          alias :shallow_scope :member_scope

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

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

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

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

          def shallow?
            @shallow
          end
1131 1132 1133
        end

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

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

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

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

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1158 1159
        end

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

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

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

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

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

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

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

1205
            set_member_mappings_for_resource
1206 1207
          end

J
Joshua Peek 已提交
1208
          self
1209 1210
        end

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

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

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

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

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

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

1364
            set_member_mappings_for_resource
1365 1366
          end

J
Joshua Peek 已提交
1367
          self
1368 1369
        end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1540
          action = action.to_s.dup
1541

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

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

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

1559 1560 1561 1562 1563 1564 1565 1566 1567
        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

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

1579
        protected
1580

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

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

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

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

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

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

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

1619 1620 1621
            false
          end

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

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

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

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

J
José Valim 已提交
1638
          def resource_method_scope? #:nodoc:
1639
            RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1640 1641
          end

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

1646
          def with_exclusive_scope
1647
            begin
1648 1649
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1650

1651 1652 1653
              with_scope_level(:exclusive) do
                yield
              end
1654
            ensure
1655
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1656 1657 1658
            end
          end

1659
          def with_scope_level(kind)
J
Joshua Peek 已提交
1660 1661 1662 1663 1664
            old, @scope[:scope_level] = @scope[:scope_level], kind
            yield
          ensure
            @scope[:scope_level] = old
          end
1665

1666
          def resource_scope(kind, resource) #:nodoc:
1667
            resource.shallow = @scope[:shallow]
1668
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1669
            @nesting.push(resource)
1670 1671 1672

            with_scope_level(kind) do
              scope(parent_resource.resource_scope) { yield }
1673
            end
1674
          ensure
1675
            @nesting.pop
1676
            @scope[:scope_level_resource] = old_resource
1677 1678
          end

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

            options
1686 1687
          end

1688 1689 1690 1691
          def nesting_depth #:nodoc:
            @nesting.size
          end

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

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

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

J
José Valim 已提交
1704
          def canonical_action?(action, flag) #:nodoc:
1705
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1706 1707
          end

1708 1709 1710 1711 1712 1713 1714
          def shallow_scope(path, options = {}) #:nodoc:
            old_name_prefix, old_path = @scope[:as], @scope[:path]
            @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path]

            scope(path, options) { yield }
          ensure
            @scope[:as], @scope[:path] = old_name_prefix, old_path
1715 1716
          end

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

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

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

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

            if parent_resource
1745
              return nil unless as || action
1746

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

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

1766 1767 1768 1769 1770 1771 1772 1773 1774 1775
            if candidate = name.select(&:present?).join("_").presence
              # If a name was not explicitly given, we check if it is valid
              # and return nil in case it isn't. Otherwise, we pass the invalid name
              # forward so the underlying router engine treats it and raises an exception.
              if as.nil?
                candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
              else
                candidate
              end
            end
1776
          end
1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788

          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 已提交
1789
      end
J
Joshua Peek 已提交
1790

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

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

1896 1897 1898
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1899
        @concerns = {}
1900
        @nesting = []
1901 1902
      end

1903 1904
      include Base
      include HttpHelpers
1905
      include Redirection
1906
      include Scoping
1907
      include Concerns
1908
      include Resources
J
Joshua Peek 已提交
1909 1910
    end
  end
J
Joshua Peek 已提交
1911
end