mapper.rb 64.0 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:
63
        IGNORE_OPTIONS = [:via, :on, :constraints, :defaults, :only, :except, :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, :as, :anchor
68

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

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

A
Aaron Patterson 已提交
80
          formatted = options.delete :format
81 82

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

91
        def to_route
92
          [ app, conditions, requirements, defaults, as, anchor ]
93
        end
J
Joshua Peek 已提交
94

95
        private
96

97 98
          def normalize_path!(path, format)
            path = Mapper.normalize_path(path)
99

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

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

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

122
            if path_params.include?(:controller)
123 124 125 126 127 128
              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' }
129
              options[:controller] ||= /.+?/
130
            end
131

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

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

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

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

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

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

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

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

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

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

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

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

A
Aaron Patterson 已提交
216
            via = Array(options[:via]).compact
217

A
Aaron Patterson 已提交
218
            if via == [:all]
219 220 221 222 223 224 225 226 227 228
              options.delete(:via)
            else
              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
              end
229

230
              @conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
231 232 233
            end
          end

234
          def app
235 236
            return to if Redirect === to

237
            if to.respond_to?(:call)
238
              Constraints.new(to, blocks, false)
239
            else
240
              if blocks.any?
241
                Constraints.new(dispatcher, blocks, true)
242 243 244
              else
                dispatcher
              end
245
            end
246 247
          end

248
          def default_controller_and_action(path_params)
A
Aaron Patterson 已提交
249
            controller, action = get_controller_and_action(default_controller,
250 251 252 253
              default_action,
              to,
              @scope[:module]
            )
A
Aaron Patterson 已提交
254

255
            hash = check_part(:controller, controller, path_params, {}) do |part|
A
Aaron Patterson 已提交
256
              translate_controller(part) {
257 258
                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"
259

A
Aaron Patterson 已提交
260 261
                raise ArgumentError, message
              }
262
            end
263

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

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

281 282
          def get_controller_and_action(controller, action, to, modyoule)
            case to
A
Aaron Patterson 已提交
283 284 285
            when Symbol then action = to.to_s
            when /#/    then controller, action = to.split('#')
            when String then controller = to
286 287 288 289 290 291 292 293 294 295 296 297
            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 已提交
298
          def translate_controller(controller)
299 300
            return controller if Regexp === controller
            return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
301

302
            yield
303 304
          end

305
          def blocks
306 307
            if options[:constraints].present? && !options[:constraints].is_a?(Hash)
              [options[:constraints]]
308
            else
309
              scope[:blocks] || []
310 311
            end
          end
J
Joshua Peek 已提交
312

313
          def constraints
314 315
            @constraints ||= {}.tap do |constraints|
              constraints.merge!(scope[:constraints]) if scope[:constraints]
316

317 318 319 320 321
              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)
322
            end
323
          end
J
Joshua Peek 已提交
324

A
Aaron Patterson 已提交
325 326
          def path_params(ast)
            ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
327
          end
328

329 330 331
          def path_ast(path)
            parser = Journey::Parser.new
            parser.parse path
332 333 334
          end

          def dispatcher
335
            Routing::RouteSet::Dispatcher.new(defaults)
336 337
          end
      end
338

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

348
      def self.normalize_name(name)
349
        normalize_path(name)[1..-1].tr("/", "_")
350 351
      end

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

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

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

555
            options = app
556
            app, path = options.find { |k, _| k.respond_to?(:call) }
557 558 559 560 561
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

P
Pratik Naik 已提交
562
          options[:as]  ||= app_name(app)
563
          target_as       = name_for_action(options[:as], path)
P
Pratik Naik 已提交
564
          options[:via] ||= :all
565

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

568
          define_generate_prefix(app, target_as)
569 570 571
          self
        end

572 573 574 575
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
576

577 578 579 580 581 582
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

583 584 585 586 587
        # Query if the following named route was already defined.
        def has_named_route?(name)
          @set.named_routes.routes[name.to_sym]
        end

588 589 590
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
591 592 593 594 595

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
596
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
597
            end
598 599 600
          end

          def define_generate_prefix(app, name)
601
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
602 603

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

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

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

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

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

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

        private
662
          def map_method(method, args, &block)
663
            options = args.extract_options!
664
            options[:via] = method
665
            match(*args, options, &block)
666 667 668 669
            self
          end
      end

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

759
          options[:path] = args.flatten.join('/') if args.any?
760
          options[:constraints] ||= {}
761

762
          unless nested_scope?
763 764
            options[:shallow_path] ||= options[:path] if options.key?(:path)
            options[:shallow_prefix] ||= options[:as] if options.key?(:as)
765 766
          end

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

777 778 779 780 781 782 783 784 785 786
          SCOPE_OPTIONS.each do |option|
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
              value = options.delete(option)
            end

            if value
787 788 789
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
790 791 792 793 794
          end

          yield
          self
        ensure
795
          @scope.merge!(recover)
796 797
        end

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

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

          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 }
858
        end
859

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

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

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

J
José Valim 已提交
933
          def merge_shallow_path_scope(parent, child) #:nodoc:
934 935 936
            Mapper.normalize_path("#{parent}/#{child}")
          end

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

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

J
José Valim 已提交
945
          def merge_module_scope(parent, child) #:nodoc:
946 947 948
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
949
          def merge_controller_scope(parent, child) #:nodoc:
950
            child
951 952
          end

953 954 955 956
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

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

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

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

J
José Valim 已提交
969
          def merge_blocks_scope(parent, child) #:nodoc:
970 971 972
            merged = parent ? parent.dup : []
            merged << child if child
            merged
973 974
          end

J
José Valim 已提交
975
          def merge_options_scope(parent, child) #:nodoc:
976
            (parent || {}).except(*override_keys(child)).merge!(child)
977
          end
978

J
José Valim 已提交
979
          def merge_shallow_scope(parent, child) #:nodoc:
980 981
            child ? true : false
          end
982

J
José Valim 已提交
983
          def override_keys(child) #:nodoc:
984 985
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
986 987
      end

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

1036
        class Resource #:nodoc:
1037
          attr_reader :controller, :path, :options, :param
1038 1039

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

1049
          def default_actions
1050
            [:index, :create, :new, :show, :update, :destroy, :edit]
1051 1052
          end

1053
          def actions
1054
            if only = @options[:only]
1055
              Array(only).map(&:to_sym)
1056
            elsif except = @options[:except]
1057 1058 1059 1060 1061 1062
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

1063
          def name
1064
            @as || @name
1065 1066
          end

1067
          def plural
1068
            @plural ||= name.to_s
1069 1070 1071
          end

          def singular
1072
            @singular ||= name.to_s.singularize
1073 1074
          end

1075
          alias :member_name :singular
1076

1077
          # Checks for uncountable plurals, and appends "_index" if the plural
1078
          # and singular form are the same.
1079
          def collection_name
1080
            singular == plural ? "#{plural}_index" : plural
1081 1082
          end

1083
          def resource_scope
1084
            { :controller => controller }
1085 1086
          end

1087
          alias :collection_scope :path
1088 1089

          def member_scope
1090
            "#{path}/:#{param}"
1091 1092
          end

1093 1094
          alias :shallow_scope :member_scope

1095
          def new_scope(new_path)
1096
            "#{path}/#{new_path}"
1097 1098
          end

1099 1100 1101 1102
          def nested_param
            :"#{singular}_#{param}"
          end

1103
          def nested_scope
1104
            "#{path}/:#{nested_param}"
1105
          end
1106

1107 1108 1109 1110 1111 1112 1113
          def shallow=(value)
            @shallow = value
          end

          def shallow?
            @shallow
          end
1114 1115 1116
        end

        class SingletonResource < Resource #:nodoc:
1117
          def initialize(entities, options)
1118
            super
1119
            @as         = nil
1120 1121
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1122 1123
          end

1124 1125 1126 1127
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

1128 1129
          def plural
            @plural ||= name.to_s.pluralize
1130 1131
          end

1132 1133
          def singular
            @singular ||= name.to_s
1134
          end
1135 1136 1137 1138 1139 1140

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1141 1142
        end

1143 1144 1145 1146
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

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

1171
          if apply_common_behavior_for(:resource, resources, options, &block)
1172 1173 1174
            return self
          end

1175
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1176
            yield if block_given?
1177

1178 1179
            concerns(options[:concerns]) if options[:concerns]

1180
            collection do
1181
              post :create
1182
            end if parent_resource.actions.include?(:create)
1183

1184
            new do
1185
              get :new
1186
            end if parent_resource.actions.include?(:new)
1187

1188
            set_member_mappings_for_resource
1189 1190
          end

J
Joshua Peek 已提交
1191
          self
1192 1193
        end

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

1329
          if apply_common_behavior_for(:resources, resources, options, &block)
1330 1331 1332
            return self
          end

1333
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1334
            yield if block_given?
J
Joshua Peek 已提交
1335

1336 1337
            concerns(options[:concerns]) if options[:concerns]

1338
            collection do
1339 1340
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1341
            end
1342

1343
            new do
1344
              get :new
1345
            end if parent_resource.actions.include?(:new)
1346

1347
            set_member_mappings_for_resource
1348 1349
          end

J
Joshua Peek 已提交
1350
          self
1351 1352
        end

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

1370 1371 1372 1373
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1374
          end
1375
        end
J
Joshua Peek 已提交
1376

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

1393
          with_scope_level(:member) do
1394 1395 1396 1397
            if shallow?
              shallow_scope(parent_resource.member_scope) { yield }
            else
              scope(parent_resource.member_scope) { yield }
1398
            end
1399 1400 1401 1402 1403 1404 1405
          end
        end

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

1407 1408 1409 1410
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1411
          end
J
Joshua Peek 已提交
1412 1413
        end

1414
        def nested
1415 1416
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1417 1418 1419
          end

          with_scope_level(:nested) do
1420
            if shallow? && shallow_nesting_depth > 1
1421
              shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1422
            else
1423
              scope(parent_resource.nested_scope, nested_options) { yield }
1424 1425 1426 1427
            end
          end
        end

1428
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1429
        def namespace(path, options = {})
1430
          if resource_scope?
1431 1432 1433 1434 1435 1436
            nested { super }
          else
            super
          end
        end

1437
        def shallow
1438
          scope(:shallow => true) do
1439 1440 1441 1442
            yield
          end
        end

1443 1444 1445 1446
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

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

1462 1463
          options[:anchor] = true unless options.key?(:anchor)

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

1468 1469 1470 1471
          if @scope[:controller] && @scope[:action]
            options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
          end

1472 1473 1474
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
1475 1476 1477 1478

            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')
1479
              route_options[:to].tr!("-", "_")
1480 1481
            end

1482 1483
            decomposed_match(_path, route_options)
          end
1484 1485
          self
        end
1486

1487 1488 1489 1490
        def using_match_shorthand?(path, options)
          path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
        end

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

1506
        def add_route(action, options) # :nodoc:
1507
          path = path_for_action(action, options.delete(:path))
1508 1509
          raise ArgumentError, "path is required" if path.blank?

1510
          action = action.to_s.dup
1511

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

1518
          if !options.fetch(:as, true)
1519 1520 1521
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1522
          end
J
Joshua Peek 已提交
1523

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

1529 1530 1531 1532 1533 1534 1535 1536 1537
        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

1538
          if @scope[:scope_level] == :resources
1539 1540
            with_scope_level(:root) do
              scope(parent_resource.path) do
1541 1542 1543 1544 1545 1546
                super(options)
              end
            end
          else
            super(options)
          end
1547 1548
        end

1549
        protected
1550

1551
          def parent_resource #:nodoc:
1552 1553 1554
            @scope[:scope_level_resource]
          end

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

1561 1562 1563 1564 1565 1566 1567
            if options.delete(:shallow)
              shallow do
                send(method, resources.pop, options, &block)
              end
              return true
            end

1568 1569 1570 1571 1572
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1573
            options.keys.each do |k|
1574 1575 1576
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1577 1578 1579
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1580 1581 1582 1583 1584
                send(method, resources.pop, options, &block)
              end
              return true
            end

1585 1586 1587 1588
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1589 1590 1591
            false
          end

J
José Valim 已提交
1592
          def action_options?(options) #:nodoc:
1593 1594 1595
            options[:only] || options[:except]
          end

J
José Valim 已提交
1596
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1597
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1598 1599
          end

J
José Valim 已提交
1600
          def scope_action_options #:nodoc:
1601 1602 1603
            @scope[:options].slice(:only, :except)
          end

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

J
José Valim 已提交
1608
          def resource_method_scope? #:nodoc:
1609
            RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1610 1611
          end

1612 1613 1614 1615
          def nested_scope? #:nodoc:
            @scope[:scope_level] == :nested
          end

1616
          def with_exclusive_scope
1617
            begin
1618 1619
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1620

1621 1622 1623
              with_scope_level(:exclusive) do
                yield
              end
1624
            ensure
1625
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1626 1627 1628
            end
          end

1629
          def with_scope_level(kind)
J
Joshua Peek 已提交
1630 1631 1632 1633 1634
            old, @scope[:scope_level] = @scope[:scope_level], kind
            yield
          ensure
            @scope[:scope_level] = old
          end
1635

1636
          def resource_scope(kind, resource) #:nodoc:
1637
            resource.shallow = @scope[:shallow]
1638
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1639
            @nesting.push(resource)
1640 1641 1642

            with_scope_level(kind) do
              scope(parent_resource.resource_scope) { yield }
1643
            end
1644
          ensure
1645
            @nesting.pop
1646
            @scope[:scope_level_resource] = old_resource
1647 1648
          end

J
José Valim 已提交
1649
          def nested_options #:nodoc:
1650 1651
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1652 1653
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1654 1655

            options
1656 1657
          end

1658 1659 1660 1661
          def nesting_depth #:nodoc:
            @nesting.size
          end

1662 1663 1664 1665
          def shallow_nesting_depth #:nodoc:
            @nesting.select(&:shallow?).size
          end

1666 1667
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1668 1669
          end

1670 1671
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1672 1673
          end

J
José Valim 已提交
1674
          def canonical_action?(action, flag) #:nodoc:
1675
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1676 1677
          end

1678 1679 1680 1681 1682 1683 1684
          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
1685 1686
          end

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

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

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

J
José Valim 已提交
1709
          def name_for_action(as, action) #:nodoc:
1710
            prefix = prefix_name_for_action(as, action)
1711
            prefix = Mapper.normalize_name(prefix) if prefix
1712 1713 1714
            name_prefix = @scope[:as]

            if parent_resource
1715
              return nil unless as || action
1716

1717 1718
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1719
            end
1720

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

1736 1737 1738 1739 1740 1741 1742 1743 1744 1745
            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
1746
          end
1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758

          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 已提交
1759
      end
J
Joshua Peek 已提交
1760

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

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

1866 1867 1868
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1869
        @concerns = {}
1870
        @nesting = []
1871 1872
      end

1873 1874
      include Base
      include HttpHelpers
1875
      include Redirection
1876
      include Scoping
1877
      include Concerns
1878
      include Resources
J
Joshua Peek 已提交
1879 1880
    end
  end
J
Joshua Peek 已提交
1881
end