mapper.rb 63.9 KB
Newer Older
1
require 'active_support/core_ext/hash/except'
B
Bogdan Gusiev 已提交
2
require 'active_support/core_ext/hash/reverse_merge'
3
require 'active_support/core_ext/hash/slice'
S
Santiago Pastorino 已提交
4
require 'active_support/core_ext/enumerable'
5
require 'active_support/core_ext/array/extract_options'
6
require 'active_support/core_ext/module/remove_method'
7
require 'active_support/inflector'
8
require 'action_dispatch/routing/redirection'
9
require 'action_dispatch/routing/endpoint'
10

J
Joshua Peek 已提交
11 12
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
13
    class Mapper
14
      URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
15
      SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
16 17
                       :controller, :action, :path_names, :constraints,
                       :shallow, :blocks, :defaults, :options]
18

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

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

32
          @dispatcher = dispatcher_p
33

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

37 38
        def dispatcher?; @dispatcher; end

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

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

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

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

62
      class Mapping #:nodoc:
A
Aaron Patterson 已提交
63
        IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
64
        ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
65

66
        attr_reader :scope, :options, :requirements, :conditions, :defaults
67
        attr_reader :to, :default_controller, :default_action
68

69 70
        def initialize(scope, path, options)
          @scope = scope
71
          @requirements, @conditions, @defaults = {}, {}, {}
72

73 74 75 76 77
          options = scope[:options].merge(options) if scope[:options]
          @to                 = options[:to]
          @default_controller = options[:controller] || scope[:controller]
          @default_action     = options[:action] || scope[:action]

A
Aaron Patterson 已提交
78
          formatted = options.delete :format
79 80

          path = normalize_path! path, formatted
81
          ast  = path_ast path
A
Aaron Patterson 已提交
82
          path_params = path_params ast
83 84
          @options = normalize_options!(options, formatted, path_params, ast)
          normalize_requirements!(path_params, formatted)
85
          normalize_conditions!(path_params, path, ast)
86
          normalize_defaults!(formatted)
87
        end
J
Joshua Peek 已提交
88

89
        def to_route
90
          [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
91
        end
J
Joshua Peek 已提交
92

93
        private
94

95 96
          def normalize_path!(path, format)
            path = Mapper.normalize_path(path)
97

98 99 100 101 102 103
            if format == true
              "#{path}.:format"
            elsif optional_format?(path, format)
              "#{path}(.:format)"
            else
              path
104 105 106
            end
          end

107 108
          def optional_format?(path, format)
            format != false && !path.include?(':format') && !path.end_with?('/')
109 110
          end

111
          def normalize_options!(options, formatted, path_params, path_ast)
112 113
            # Add a constraint for wildcard route to make it non-greedy and match the
            # optional format part of the route by default
114
            if formatted != false
A
Aaron Patterson 已提交
115 116 117
              path_ast.grep(Journey::Nodes::Star) do |node|
                options[node.name.to_sym] ||= /.+?/
              end
118 119
            end

120
            if path_params.include?(:controller)
121 122 123 124 125 126
              raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]

              # Add a default constraint for :controller path segments that matches namespaced
              # controllers with default routes like :controller/:action/:id(.:format), e.g:
              # GET /admin/products/show/1
              # => { controller: 'admin/products', action: 'show', id: '1' }
127
              options[:controller] ||= /.+?/
128
            end
129

A
Aaron Patterson 已提交
130 131 132
            if to.respond_to? :call
              options
            else
133
              options.merge!(default_controller_and_action(path_params))
A
Aaron Patterson 已提交
134
            end
135 136
          end

137
          def normalize_requirements!(path_params, formatted)
138
            constraints.each do |key, requirement|
139
              next unless path_params.include?(key) || key == :controller
Y
Yves Senn 已提交
140
              verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
141
              @requirements[key] = requirement
142
            end
143

144
            if formatted == true
145
              @requirements[:format] ||= /.+/
146 147 148 149
            elsif Regexp === formatted
              @requirements[:format] = formatted
            elsif String === formatted
              @requirements[:format] = Regexp.compile(formatted)
150
            end
151
          end
152

Y
Yves Senn 已提交
153 154 155 156 157 158 159 160 161 162
          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

163
          def normalize_defaults!(formatted)
164 165
            @defaults.merge!(scope[:defaults]) if scope[:defaults]
            @defaults.merge!(options[:defaults]) if options[:defaults]
166

167
            options.each do |key, default|
A
Akshay Vishnoi 已提交
168 169 170
              unless Regexp === default || IGNORE_OPTIONS.include?(key)
                @defaults[key] = default
              end
171 172
            end

173 174
            if options[:constraints].is_a?(Hash)
              options[:constraints].each do |key, default|
A
Akshay Vishnoi 已提交
175 176 177
                if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
                  @defaults[key] ||= default
                end
178
              end
179 180
            elsif options[:constraints]
              verify_callable_constraint(options[:constraints])
181 182
            end

183
            if Regexp === formatted
184
              @defaults[:format] = nil
185 186
            elsif String === formatted
              @defaults[:format] = formatted
187
            end
188
          end
189

190
          def verify_callable_constraint(callable_constraint)
191 192 193
            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
194 195
          end

196
          def normalize_conditions!(path_params, path, ast)
197
            @conditions[:path_info] = path
198
            @conditions[:parsed_path_info] = ast
199

200
            constraints.each do |key, condition|
201
              unless path_params.include?(key) || key == :controller
A
Akshay Vishnoi 已提交
202 203
                @conditions[key] = condition
              end
204
            end
J
Joshua Peek 已提交
205

206
            required_defaults = []
207
            options.each do |key, required_default|
208
              unless path_params.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default
209
                required_defaults << key
A
Akshay Vishnoi 已提交
210
              end
211
            end
212
            @conditions[:required_defaults] = required_defaults
213

214 215 216 217
            via_all = options.delete(:via) if options[:via] == :all

            if !via_all && options[:via].blank?
              msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
218 219
                    "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" \
220 221 222
                    "  Instead of: match \"controller#action\"\n" \
                    "  Do: get \"controller#action\""
              raise msg
223
            end
224

225
            if via = options[:via]
226
              @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase }
227 228 229
            end
          end

230
          def app
231 232
            return to if Redirect === to

233
            if to.respond_to?(:call)
234
              Constraints.new(to, blocks, false)
235
            else
236
              if blocks.any?
237
                Constraints.new(dispatcher, blocks, true)
238 239 240
              else
                dispatcher
              end
241
            end
242 243
          end

244
          def default_controller_and_action(path_params)
A
Aaron Patterson 已提交
245
            controller, action = get_controller_and_action(default_controller,
246 247 248 249
              default_action,
              to,
              @scope[:module]
            )
A
Aaron Patterson 已提交
250

251
            hash = check_part(:controller, controller, path_params, {}) do |part|
A
Aaron Patterson 已提交
252
              translate_controller(part) {
253 254
                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"
255

A
Aaron Patterson 已提交
256 257
                raise ArgumentError, message
              }
258
            end
259

260
            check_part(:action, action, path_params, hash) { |part|
A
Aaron Patterson 已提交
261
              part.is_a?(Regexp) ? part : part.to_s
262
            }
263
          end
264

265
          def check_part(name, part, path_params, hash)
266
            if part
A
Aaron Patterson 已提交
267
              hash[name] = yield(part)
268
            else
269
              unless path_params.include?(name)
270
                message = "Missing :#{name} key on routes definition, please check your routes."
271 272
                raise ArgumentError, message
              end
273
            end
274
            hash
275
          end
276

277 278
          def get_controller_and_action(controller, action, to, modyoule)
            case to
A
Aaron Patterson 已提交
279 280 281
            when Symbol then action = to.to_s
            when /#/    then controller, action = to.split('#')
            when String then controller = to
282 283 284 285 286 287 288 289 290 291 292 293
            end

            if modyoule && !controller.is_a?(Regexp)
              if controller =~ %r{\A/}
                controller = controller[1..-1]
              else
                controller = [modyoule, controller].compact.join("/")
              end
            end
            [controller, action]
          end

A
Aaron Patterson 已提交
294
          def translate_controller(controller)
295 296
            return controller if Regexp === controller
            return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
297

298
            yield
299 300
          end

301
          def blocks
302 303
            if options[:constraints].present? && !options[:constraints].is_a?(Hash)
              [options[:constraints]]
304
            else
305
              scope[:blocks] || []
306 307
            end
          end
J
Joshua Peek 已提交
308

309
          def constraints
310 311
            @constraints ||= {}.tap do |constraints|
              constraints.merge!(scope[:constraints]) if scope[:constraints]
312

313 314 315 316 317
              options.except(*IGNORE_OPTIONS).each do |key, option|
                constraints[key] = option if Regexp === option
              end

              constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash)
318
            end
319
          end
J
Joshua Peek 已提交
320

A
Aaron Patterson 已提交
321 322
          def path_params(ast)
            ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
323
          end
324

325 326 327
          def path_ast(path)
            parser = Journey::Parser.new
            parser.parse path
328 329 330
          end

          def dispatcher
331
            Routing::RouteSet::Dispatcher.new(defaults)
332 333
          end
      end
334

335
      # Invokes Journey::Router::Utils.normalize_path and ensure that
336 337
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
338
      def self.normalize_path(path)
339
        path = Journey::Router::Utils.normalize_path(path)
340
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
341 342 343
        path
      end

344
      def self.normalize_name(name)
345
        normalize_path(name)[1..-1].tr("/", "_")
346 347
      end

348
      module Base
349 350
        # You can specify what Rails should route "/" to with the root method:
        #
A
AvnerCohen 已提交
351
        #   root to: 'pages#main'
352
        #
353
        # For options, see +match+, as +root+ uses it internally.
354
        #
355 356 357 358
        # You can also pass a string which will expand
        #
        #   root 'pages#main'
        #
359 360 361
        # 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.
362
        def root(options = {})
363
          match '/', { :as => :root, :via => :get }.merge!(options)
364
        end
365

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

524 525
        # Mount a Rack-based application to be used within the application.
        #
A
AvnerCohen 已提交
526
        #   mount SomeRackApp, at: "some_route"
527 528 529
        #
        # Alternatively:
        #
R
Ryan Bigg 已提交
530
        #   mount(SomeRackApp => "some_route")
531
        #
532 533
        # For options, see +match+, as +mount+ uses it internally.
        #
534 535 536 537 538
        # 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 已提交
539
        #   mount(SomeRackApp => "some_route", as: "exciting")
540 541 542
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
543 544 545 546
        def mount(app, options = nil)
          if options
            path = options.delete(:at)
          else
547 548 549 550
            unless Hash === app
              raise ArgumentError, "must be called with mount point"
            end

551
            options = app
552
            app, path = options.find { |k, _| k.respond_to?(:call) }
553 554 555 556 557
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

P
Pratik Naik 已提交
558
          options[:as]  ||= app_name(app)
559
          target_as       = name_for_action(options[:as], path)
P
Pratik Naik 已提交
560
          options[:via] ||= :all
561

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

564
          define_generate_prefix(app, target_as)
565 566 567
          self
        end

568 569 570 571
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
572

573 574 575 576 577 578
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

579 580 581 582 583
        # Query if the following named route was already defined.
        def has_named_route?(name)
          @set.named_routes.routes[name.to_sym]
        end

584 585 586
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
587 588 589 590 591

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
592
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
593
            end
594 595 596
          end

          def define_generate_prefix(app, name)
597
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
598 599

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
600 601
            _routes = @set
            app.routes.define_mounted_helper(name)
602 603
            app.routes.extend Module.new {
              def mounted?; true; end
604 605
              define_method :find_script_name do |options|
                super(options) || begin
P
Piotr Sarnacki 已提交
606
                prefix_options = options.slice(*_route.segment_keys)
607 608
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
609
                _routes.url_helpers.send("#{name}_path", prefix_options)
610
                end
611
              end
612
            }
613
          end
614 615 616
      end

      module HttpHelpers
617
        # Define a route that only recognizes HTTP GET.
C
Cesar Carruitero 已提交
618
        # For supported arguments, see match[rdoc-ref:Base#match]
619
        #
A
AvnerCohen 已提交
620
        #   get 'bacon', to: 'food#bacon'
621
        def get(*args, &block)
622
          map_method(:get, args, &block)
623 624
        end

625
        # Define a route that only recognizes HTTP POST.
C
Cesar Carruitero 已提交
626
        # For supported arguments, see match[rdoc-ref:Base#match]
627
        #
A
AvnerCohen 已提交
628
        #   post 'bacon', to: 'food#bacon'
629
        def post(*args, &block)
630
          map_method(:post, args, &block)
631 632
        end

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

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

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

        private
658
          def map_method(method, args, &block)
659
            options = args.extract_options!
660
            options[:via] = method
661
            match(*args, options, &block)
662 663 664 665
            self
          end
      end

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

755
          options[:path] = args.flatten.join('/') if args.any?
756
          options[:constraints] ||= {}
757

758
          unless nested_scope?
759 760
            options[:shallow_path] ||= options[:path] if options.key?(:path)
            options[:shallow_prefix] ||= options[:as] if options.key?(:as)
761 762
          end

763
          if options[:constraints].is_a?(Hash)
764 765 766 767 768
            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)
769 770
          else
            block, options[:constraints] = options[:constraints], {}
771 772
          end

773 774 775 776 777 778 779 780 781 782
          SCOPE_OPTIONS.each do |option|
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
              value = options.delete(option)
            end

            if value
783 784 785
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
786 787 788 789 790
          end

          yield
          self
        ensure
791
          @scope.merge!(recover)
792 793
        end

794 795 796
        # Scopes routes to a specific controller
        #
        #   controller "food" do
A
AvnerCohen 已提交
797
        #     match "bacon", action: "bacon"
798
        #   end
799 800 801
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
802 803
        end

804 805 806 807 808 809 810 811
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
812 813 814 815 816
        #       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
817
        #        admin_post PATCH/PUT /admin/posts/:id(.:format)      admin/posts#update
818
        #        admin_post DELETE    /admin/posts/:id(.:format)      admin/posts#destroy
819
        #
820
        # === Options
821
        #
822 823
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
        # options all default to the name of the namespace.
824
        #
825 826
        # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
        # <tt>Resources#resources</tt>.
827
        #
828
        #   # accessible through /sekret/posts rather than /admin/posts
A
AvnerCohen 已提交
829
        #   namespace :admin, path: "sekret" do
830 831
        #     resources :posts
        #   end
832
        #
S
Sebastian Martinez 已提交
833
        #   # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
834
        #   namespace :admin, module: "sekret" do
835 836
        #     resources :posts
        #   end
837
        #
S
Sebastian Martinez 已提交
838
        #   # generates +sekret_posts_path+ rather than +admin_posts_path+
A
AvnerCohen 已提交
839
        #   namespace :admin, as: "sekret" do
840 841
        #     resources :posts
        #   end
842
        def namespace(path, options = {})
843
          path = path.to_s
844 845 846 847 848 849 850 851 852 853

          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 }
854
        end
855

R
Ryan Bigg 已提交
856 857 858 859
        # === 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 已提交
860
        #   constraints(id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
861 862 863 864 865
        #     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.
866
        #
R
R.T. Lechow 已提交
867
        # You may use this to also restrict other parameters:
R
Ryan Bigg 已提交
868 869
        #
        #   resources :posts do
A
AvnerCohen 已提交
870
        #     constraints(post_id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
871 872
        #       resources :comments
        #     end
J
James Miller 已提交
873
        #   end
R
Ryan Bigg 已提交
874 875 876 877 878
        #
        # === Restricting based on IP
        #
        # Routes can also be constrained to an IP or a certain range of IP addresses:
        #
A
AvnerCohen 已提交
879
        #   constraints(ip: /192\.168\.\d+\.\d+/) do
R
Ryan Bigg 已提交
880 881 882 883 884 885 886 887
        #     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 已提交
888
        # Requests to routes can be constrained based on specific criteria:
R
Ryan Bigg 已提交
889 890 891 892 893 894 895 896 897 898
        #
        #    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
899
        #      def self.matches?(request)
R
Ryan Bigg 已提交
900 901 902 903 904 905 906 907 908 909 910
        #        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
911 912 913 914
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
915
        # Allows you to set default parameters for a route, such as this:
A
AvnerCohen 已提交
916 917
        #   defaults id: 'home' do
        #     match 'scoped_pages/(:id)', to: 'pages#show'
918
        #   end
R
Ryan Bigg 已提交
919
        # Using this, the +:id+ parameter here will default to 'home'.
920 921 922 923
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

924
        private
J
José Valim 已提交
925
          def merge_path_scope(parent, child) #:nodoc:
926
            Mapper.normalize_path("#{parent}/#{child}")
927 928
          end

J
José Valim 已提交
929
          def merge_shallow_path_scope(parent, child) #:nodoc:
930 931 932
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
933
          def merge_as_scope(parent, child) #:nodoc:
934
            parent ? "#{parent}_#{child}" : child
935 936
          end

J
José Valim 已提交
937
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
938 939 940
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
941
          def merge_module_scope(parent, child) #:nodoc:
942 943 944
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
945
          def merge_controller_scope(parent, child) #:nodoc:
946
            child
947 948
          end

949 950 951 952
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

J
José Valim 已提交
953
          def merge_path_names_scope(parent, child) #:nodoc:
954 955 956
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
957
          def merge_constraints_scope(parent, child) #:nodoc:
958 959 960
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
961
          def merge_defaults_scope(parent, child) #:nodoc:
962 963 964
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
965
          def merge_blocks_scope(parent, child) #:nodoc:
966 967 968
            merged = parent ? parent.dup : []
            merged << child if child
            merged
969 970
          end

J
José Valim 已提交
971
          def merge_options_scope(parent, child) #:nodoc:
972
            (parent || {}).except(*override_keys(child)).merge!(child)
973
          end
974

J
José Valim 已提交
975
          def merge_shallow_scope(parent, child) #:nodoc:
976 977
            child ? true : false
          end
978

J
José Valim 已提交
979
          def override_keys(child) #:nodoc:
980 981
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
982 983
      end

984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007
      # 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 已提交
1008 1009
      # <tt>app/controllers/admin</tt> directory, and you can group them together
      # in your router:
1010 1011 1012 1013 1014
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
S
Sebastian Martinez 已提交
1015 1016
      # 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
1017 1018
      # overrides this restriction, e.g:
      #
A
AvnerCohen 已提交
1019
      #   resources :articles, id: /[^\/]+/
1020
      #
S
Sebastian Martinez 已提交
1021
      # This allows any character other than a slash as part of your +:id+.
1022
      #
J
Joshua Peek 已提交
1023
      module Resources
1024 1025
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
1026
        VALID_ON_OPTIONS  = [:new, :collection, :member]
1027
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except, :param, :concerns]
1028
        CANONICAL_ACTIONS = %w(index create new show update destroy)
1029 1030
        RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
        RESOURCE_SCOPES = [:resource, :resources]
1031

1032
        class Resource #:nodoc:
1033
          attr_reader :controller, :path, :options, :param
1034 1035

          def initialize(entities, options = {})
1036
            @name       = entities.to_s
1037 1038 1039
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
1040
            @param      = (options[:param] || :id).to_sym
1041
            @options    = options
1042
            @shallow    = false
1043 1044
          end

1045
          def default_actions
1046
            [:index, :create, :new, :show, :update, :destroy, :edit]
1047 1048
          end

1049
          def actions
1050
            if only = @options[:only]
1051
              Array(only).map(&:to_sym)
1052
            elsif except = @options[:except]
1053 1054 1055 1056 1057 1058
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

1059
          def name
1060
            @as || @name
1061 1062
          end

1063
          def plural
1064
            @plural ||= name.to_s
1065 1066 1067
          end

          def singular
1068
            @singular ||= name.to_s.singularize
1069 1070
          end

1071
          alias :member_name :singular
1072

1073
          # Checks for uncountable plurals, and appends "_index" if the plural
1074
          # and singular form are the same.
1075
          def collection_name
1076
            singular == plural ? "#{plural}_index" : plural
1077 1078
          end

1079
          def resource_scope
1080
            { :controller => controller }
1081 1082
          end

1083
          alias :collection_scope :path
1084 1085

          def member_scope
1086
            "#{path}/:#{param}"
1087 1088
          end

1089 1090
          alias :shallow_scope :member_scope

1091
          def new_scope(new_path)
1092
            "#{path}/#{new_path}"
1093 1094
          end

1095 1096 1097 1098
          def nested_param
            :"#{singular}_#{param}"
          end

1099
          def nested_scope
1100
            "#{path}/:#{nested_param}"
1101
          end
1102

1103 1104 1105 1106 1107 1108 1109
          def shallow=(value)
            @shallow = value
          end

          def shallow?
            @shallow
          end
1110 1111 1112
        end

        class SingletonResource < Resource #:nodoc:
1113
          def initialize(entities, options)
1114
            super
1115
            @as         = nil
1116 1117
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1118 1119
          end

1120 1121 1122 1123
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

1124 1125
          def plural
            @plural ||= name.to_s.pluralize
1126 1127
          end

1128 1129
          def singular
            @singular ||= name.to_s
1130
          end
1131 1132 1133 1134 1135 1136

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1137 1138
        end

1139 1140 1141 1142
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

1143 1144 1145 1146 1147 1148
        # 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:
        #
1149
        #   resource :profile
1150 1151
        #
        # creates six different routes in your application, all mapping to
1152
        # the +Profiles+ controller (note that the controller is named after
1153 1154
        # the plural):
        #
1155 1156 1157 1158 1159 1160
        #   GET       /profile/new
        #   POST      /profile
        #   GET       /profile
        #   GET       /profile/edit
        #   PATCH/PUT /profile
        #   DELETE    /profile
1161
        #
1162
        # === Options
1163
        # Takes same options as +resources+.
J
Joshua Peek 已提交
1164
        def resource(*resources, &block)
1165
          options = resources.extract_options!.dup
J
Joshua Peek 已提交
1166

1167
          if apply_common_behavior_for(:resource, resources, options, &block)
1168 1169 1170
            return self
          end

1171
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1172
            yield if block_given?
1173

1174 1175
            concerns(options[:concerns]) if options[:concerns]

1176
            collection do
1177
              post :create
1178
            end if parent_resource.actions.include?(:create)
1179

1180
            new do
1181
              get :new
1182
            end if parent_resource.actions.include?(:new)
1183

1184
            set_member_mappings_for_resource
1185 1186
          end

J
Joshua Peek 已提交
1187
          self
1188 1189
        end

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

1325
          if apply_common_behavior_for(:resources, resources, options, &block)
1326 1327 1328
            return self
          end

1329
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1330
            yield if block_given?
J
Joshua Peek 已提交
1331

1332 1333
            concerns(options[:concerns]) if options[:concerns]

1334
            collection do
1335 1336
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1337
            end
1338

1339
            new do
1340
              get :new
1341
            end if parent_resource.actions.include?(:new)
1342

1343
            set_member_mappings_for_resource
1344 1345
          end

J
Joshua Peek 已提交
1346
          self
1347 1348
        end

1349 1350 1351 1352 1353 1354 1355 1356 1357
        # 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 已提交
1358
        # with GET, and route to the search action of +PhotosController+. It will also
1359 1360
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1361
        def collection
1362 1363
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1364 1365
          end

1366 1367 1368 1369
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1370
          end
1371
        end
J
Joshua Peek 已提交
1372

1373 1374 1375 1376 1377 1378 1379 1380 1381
        # 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 已提交
1382
        # preview action of +PhotosController+. It will also create the
1383
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1384
        def member
1385 1386
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1387
          end
J
Joshua Peek 已提交
1388

1389
          with_scope_level(:member) do
1390 1391 1392 1393
            if shallow?
              shallow_scope(parent_resource.member_scope) { yield }
            else
              scope(parent_resource.member_scope) { yield }
1394
            end
1395 1396 1397 1398 1399 1400 1401
          end
        end

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

1403 1404 1405 1406
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1407
          end
J
Joshua Peek 已提交
1408 1409
        end

1410
        def nested
1411 1412
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1413 1414 1415
          end

          with_scope_level(:nested) do
1416
            if shallow? && shallow_nesting_depth > 1
1417
              shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1418
            else
1419
              scope(parent_resource.nested_scope, nested_options) { yield }
1420 1421 1422 1423
            end
          end
        end

1424
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1425
        def namespace(path, options = {})
1426
          if resource_scope?
1427 1428 1429 1430 1431 1432
            nested { super }
          else
            super
          end
        end

1433
        def shallow
1434
          scope(:shallow => true) do
1435 1436 1437 1438
            yield
          end
        end

1439 1440 1441 1442
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

1443
        # match 'path' => 'controller#action'
R
Rafael Mendonça França 已提交
1444
        # match 'path', to: 'controller#action'
1445
        # match 'path', 'otherpath', on: :member, via: :get
1446 1447 1448
        def match(path, *rest)
          if rest.empty? && Hash === path
            options  = path
1449
            path, to = options.find { |name, _value| name.is_a?(String) }
1450 1451
            options[:to] = to
            options.delete(path)
1452 1453 1454 1455 1456 1457
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1458 1459
          options[:anchor] = true unless options.key?(:anchor)

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

1464 1465 1466 1467
          if @scope[:controller] && @scope[:action]
            options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
          end

1468 1469 1470
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
1471 1472 1473 1474

            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')
1475
              route_options[:to].tr!("-", "_")
1476 1477
            end

1478 1479
            decomposed_match(_path, route_options)
          end
1480 1481
          self
        end
1482

1483 1484 1485 1486
        def using_match_shorthand?(path, options)
          path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
        end

1487
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1488 1489
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1490
          else
A
Aaron Patterson 已提交
1491 1492 1493 1494 1495 1496 1497 1498
            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 已提交
1499
          end
1500
        end
J
Joshua Peek 已提交
1501

1502
        def add_route(action, options) # :nodoc:
1503
          path = path_for_action(action, options.delete(:path))
1504 1505
          raise ArgumentError, "path is required" if path.blank?

1506
          action = action.to_s.dup
1507

1508
          if action =~ /^[\w\-\/]+$/
1509
            options[:action] ||= action.tr('-', '_') unless action.include?("/")
1510
          else
1511 1512 1513
            action = nil
          end

1514
          if !options.fetch(:as, true)
1515 1516 1517
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1518
          end
J
Joshua Peek 已提交
1519

1520
          mapping = Mapping.new(@scope, URI.parser.escape(path), options)
1521 1522
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1523 1524
        end

1525 1526 1527 1528 1529 1530 1531 1532 1533
        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

1534
          if @scope[:scope_level] == :resources
1535 1536
            with_scope_level(:root) do
              scope(parent_resource.path) do
1537 1538 1539 1540 1541 1542
                super(options)
              end
            end
          else
            super(options)
          end
1543 1544
        end

1545
        protected
1546

1547
          def parent_resource #:nodoc:
1548 1549 1550
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1551
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1552 1553 1554 1555 1556
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1557 1558 1559 1560 1561 1562 1563
            if options.delete(:shallow)
              shallow do
                send(method, resources.pop, options, &block)
              end
              return true
            end

1564 1565 1566 1567 1568
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1569
            options.keys.each do |k|
1570 1571 1572
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1573 1574 1575
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1576 1577 1578 1579 1580
                send(method, resources.pop, options, &block)
              end
              return true
            end

1581 1582 1583 1584
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1585 1586 1587
            false
          end

J
José Valim 已提交
1588
          def action_options?(options) #:nodoc:
1589 1590 1591
            options[:only] || options[:except]
          end

J
José Valim 已提交
1592
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1593
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1594 1595
          end

J
José Valim 已提交
1596
          def scope_action_options #:nodoc:
1597 1598 1599
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1600
          def resource_scope? #:nodoc:
1601
            RESOURCE_SCOPES.include? @scope[:scope_level]
1602 1603
          end

J
José Valim 已提交
1604
          def resource_method_scope? #:nodoc:
1605
            RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1606 1607
          end

1608 1609 1610 1611
          def nested_scope? #:nodoc:
            @scope[:scope_level] == :nested
          end

1612
          def with_exclusive_scope
1613
            begin
1614 1615
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1616

1617 1618 1619
              with_scope_level(:exclusive) do
                yield
              end
1620
            ensure
1621
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1622 1623 1624
            end
          end

1625
          def with_scope_level(kind)
J
Joshua Peek 已提交
1626 1627 1628 1629 1630
            old, @scope[:scope_level] = @scope[:scope_level], kind
            yield
          ensure
            @scope[:scope_level] = old
          end
1631

1632
          def resource_scope(kind, resource) #:nodoc:
1633
            resource.shallow = @scope[:shallow]
1634
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1635
            @nesting.push(resource)
1636 1637 1638

            with_scope_level(kind) do
              scope(parent_resource.resource_scope) { yield }
1639
            end
1640
          ensure
1641
            @nesting.pop
1642
            @scope[:scope_level_resource] = old_resource
1643 1644
          end

J
José Valim 已提交
1645
          def nested_options #:nodoc:
1646 1647
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1648 1649
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1650 1651

            options
1652 1653
          end

1654 1655 1656 1657
          def nesting_depth #:nodoc:
            @nesting.size
          end

1658 1659 1660 1661
          def shallow_nesting_depth #:nodoc:
            @nesting.select(&:shallow?).size
          end

1662 1663
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1664 1665
          end

1666 1667
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1668 1669
          end

J
José Valim 已提交
1670
          def canonical_action?(action, flag) #:nodoc:
1671
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1672 1673
          end

1674 1675 1676 1677 1678 1679 1680
          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
1681 1682
          end

J
José Valim 已提交
1683
          def path_for_action(action, path) #:nodoc:
1684
            if canonical_action?(action, path.blank?)
1685
              @scope[:path].to_s
1686
            else
1687
              "#{@scope[:path]}/#{action_path(action, path)}"
1688 1689 1690
            end
          end

J
José Valim 已提交
1691
          def action_path(name, path = nil) #:nodoc:
1692
            name = name.to_sym if name.is_a?(String)
1693
            path || @scope[:path_names][name] || name.to_s
1694 1695
          end

J
José Valim 已提交
1696
          def prefix_name_for_action(as, action) #:nodoc:
1697
            if as
1698
              prefix = as
1699
            elsif !canonical_action?(action, @scope[:scope_level])
1700
              prefix = action
1701
            end
1702
            prefix.to_s.tr('-', '_') if prefix
1703 1704
          end

J
José Valim 已提交
1705
          def name_for_action(as, action) #:nodoc:
1706
            prefix = prefix_name_for_action(as, action)
1707
            prefix = Mapper.normalize_name(prefix) if prefix
1708 1709 1710
            name_prefix = @scope[:as]

            if parent_resource
1711
              return nil unless as || action
1712

1713 1714
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1715
            end
1716

1717
            name = case @scope[:scope_level]
1718
            when :nested
1719
              [name_prefix, prefix]
1720
            when :collection
1721
              [prefix, name_prefix, collection_name]
1722
            when :new
1723 1724
              [prefix, :new, name_prefix, member_name]
            when :member
1725
              [prefix, name_prefix, member_name]
1726 1727
            when :root
              [name_prefix, collection_name, prefix]
1728
            else
1729
              [name_prefix, member_name, prefix]
1730
            end
1731

1732 1733 1734 1735 1736 1737 1738 1739 1740 1741
            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
1742
          end
1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754

          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 已提交
1755
      end
J
Joshua Peek 已提交
1756

1757
      # Routing Concerns allow you to declare common routes that can be reused
1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776
      # 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
1777
      module Concerns
1778
        # Define a routing concern using a name.
1779
        #
1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801
        # 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]
1802 1803
        #   end
        #
1804 1805 1806
        # Or, using a callable object, you might implement something more
        # specific to your application, which would be out of place in your
        # routes file.
1807
        #
1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818
        #   # 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]
1819 1820 1821
        #     end
        #   end
        #
1822 1823 1824 1825 1826 1827 1828 1829
        #   # routes.rb
        #   concern :purchasable, Purchasable.new(returnable: true)
        #
        #   resources :toys, concerns: :purchasable
        #   resources :electronics, concerns: :purchasable
        #   resources :pets do
        #     concerns :purchasable, returnable: false
        #   end
1830
        #
1831 1832 1833
        # 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>.
1834
        def concern(name, callable = nil, &block)
1835 1836
          callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
          @concerns[name] = callable
1837 1838
        end

1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849
        # 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
1850 1851 1852
        def concerns(*args)
          options = args.extract_options!
          args.flatten.each do |name|
1853
            if concern = @concerns[name]
1854
              concern.call(self, options)
1855 1856 1857 1858 1859 1860 1861
            else
              raise ArgumentError, "No concern named #{name} was found!"
            end
          end
        end
      end

1862 1863 1864
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1865
        @concerns = {}
1866
        @nesting = []
1867 1868
      end

1869 1870
      include Base
      include HttpHelpers
1871
      include Redirection
1872
      include Scoping
1873
      include Concerns
1874
      include Resources
J
Joshua Peek 已提交
1875 1876
    end
  end
J
Joshua Peek 已提交
1877
end