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

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 = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
64
        ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
65
        WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
66

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

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

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

          @options = normalize_options!(options)
80
          normalize_path!
81 82
          normalize_requirements!
          normalize_conditions!
83
          normalize_defaults!
84
        end
J
Joshua Peek 已提交
85

86
        def to_route
87
          [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
88
        end
J
Joshua Peek 已提交
89

90
        private
91

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
          def normalize_path!
            raise ArgumentError, "path is required" if @path.blank?
            @path = Mapper.normalize_path(@path)

            if required_format?
              @path = "#{@path}.:format"
            elsif optional_format?
              @path = "#{@path}(.:format)"
            end
          end

          def required_format?
            options[:format] == true
          end

          def optional_format?
            options[:format] != false && !path.include?(':format') && !path.end_with?('/')
          end

111
          def normalize_options!(options)
112 113 114 115
            path_without_format = path.sub(/\(\.:format\)$/, '')

            # Add a constraint for wildcard route to make it non-greedy and match the
            # optional format part of the route by default
116 117
            if path_without_format.match(WILDCARD_PATH) && options[:format] != false
              options[$1.to_sym] ||= /.+?/
118 119
            end

120
            if path_pattern.names.map(&:to_sym).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 133 134
            if to.respond_to? :call
              options
            else
              options.merge!(default_controller_and_action)
            end
135 136 137 138 139
          end

          def normalize_requirements!
            constraints.each do |key, requirement|
              next unless segment_keys.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 options[:format] == true
145
              @requirements[:format] ||= /.+/
146 147 148 149 150
            elsif Regexp === options[:format]
              @requirements[:format] = options[:format]
            elsif String === options[:format]
              @requirements[:format] = Regexp.compile(options[:format])
            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 164 165
          def normalize_defaults!
            @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 184 185 186
            if Regexp === options[:format]
              @defaults[:format] = nil
            elsif String === options[:format]
              @defaults[:format] = options[:format]
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!
197
            @conditions[:path_info] = path
198

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

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

213 214 215 216
            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" \
217 218
                    "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" \
219 220 221
                    "  Instead of: match \"controller#action\"\n" \
                    "  Do: get \"controller#action\""
              raise msg
222
            end
223

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

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

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

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

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

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

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

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

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

297
            yield
298 299
          end

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

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

312 313 314 315 316
              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)
317
            end
318
          end
J
Joshua Peek 已提交
319

320
          def segment_keys
321 322 323 324 325 326
            @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
          end

          def path_pattern
            Journey::Path::Pattern.new(strexp)
          end
327

328 329 330 331 332
          def strexp
            Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
          end

          def dispatcher
333
            Routing::RouteSet::Dispatcher.new(defaults)
334 335
          end
      end
336

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

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

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

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

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

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

          raise "A rack application must be specified" unless path

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

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

566
          define_generate_prefix(app, target_as)
567 568 569
          self
        end

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

575 576 577 578 579 580
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          yield
          self
        ensure
793
          @scope.merge!(recover)
794 795
        end

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

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

          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 }
856
        end
857

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

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

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

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

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

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

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

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

951 952 953 954
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

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

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

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

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

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

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

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

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

1034
        class Resource #:nodoc:
1035
          attr_reader :controller, :path, :options, :param
1036 1037

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

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

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

1061
          def name
1062
            @as || @name
1063 1064
          end

1065
          def plural
1066
            @plural ||= name.to_s
1067 1068 1069
          end

          def singular
1070
            @singular ||= name.to_s.singularize
1071 1072
          end

1073
          alias :member_name :singular
1074

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

1081
          def resource_scope
1082
            { :controller => controller }
1083 1084
          end

1085
          alias :collection_scope :path
1086 1087

          def member_scope
1088
            "#{path}/:#{param}"
1089 1090
          end

1091 1092
          alias :shallow_scope :member_scope

1093
          def new_scope(new_path)
1094
            "#{path}/#{new_path}"
1095 1096
          end

1097 1098 1099 1100
          def nested_param
            :"#{singular}_#{param}"
          end

1101
          def nested_scope
1102
            "#{path}/:#{nested_param}"
1103
          end
1104

1105 1106 1107 1108 1109 1110 1111
          def shallow=(value)
            @shallow = value
          end

          def shallow?
            @shallow
          end
1112 1113 1114
        end

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

1122 1123 1124 1125
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

1126 1127
          def plural
            @plural ||= name.to_s.pluralize
1128 1129
          end

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

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1139 1140
        end

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

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

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

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

1176 1177
            concerns(options[:concerns]) if options[:concerns]

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

1182
            new do
1183
              get :new
1184
            end if parent_resource.actions.include?(:new)
1185

1186
            set_member_mappings_for_resource
1187 1188
          end

J
Joshua Peek 已提交
1189
          self
1190 1191
        end

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

1327
          if apply_common_behavior_for(:resources, resources, options, &block)
1328 1329 1330
            return self
          end

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

1334 1335
            concerns(options[:concerns]) if options[:concerns]

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

1341
            new do
1342
              get :new
1343
            end if parent_resource.actions.include?(:new)
1344

1345
            set_member_mappings_for_resource
1346 1347
          end

J
Joshua Peek 已提交
1348
          self
1349 1350
        end

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

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

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

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

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

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

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

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

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

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

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

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

1460 1461
          options[:anchor] = true unless options.key?(:anchor)

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

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

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

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

1480 1481
            decomposed_match(_path, route_options)
          end
1482 1483
          self
        end
1484

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

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

1504
        def add_route(action, options) # :nodoc:
1505
          path = path_for_action(action, options.delete(:path))
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(@set, @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