mapper.rb 65.3 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 15
      URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]

16
      class Constraints < Endpoint #:nodoc:
17
        attr_reader :app, :constraints
18

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

29
          @dispatcher = dispatcher_p
30

31
          @app, @constraints, = app, constraints
32 33
        end

34 35
        def dispatcher?; @dispatcher; end

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

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

          if dispatcher?
            @app.serve req
          else
            @app.call req.env
          end
51
        end
52 53 54

        private
          def constraint_args(constraint, request)
55
            constraint.arity == 1 ? [request] : [request.path_parameters, request]
56
          end
57 58
      end

59
      class Mapping #:nodoc:
60
        ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
61

A
Aaron Patterson 已提交
62
        attr_reader :requirements, :conditions, :defaults
63
        attr_reader :to, :default_controller, :default_action, :as, :anchor
64

65
        def self.build(scope, set, path, as, options)
66 67 68 69 70 71 72
          options = scope[:options].merge(options) if scope[:options]

          options.delete :only
          options.delete :except
          options.delete :shallow_path
          options.delete :shallow_prefix
          options.delete :shallow
73

74 75
          defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}

76
          new scope, set, path, defaults, as, options
77 78
        end

79
        def initialize(scope, set, path, defaults, as, options)
80 81
          @requirements, @conditions = {}, {}
          @defaults = defaults
82
          @set = set
83

84
          @to                 = options.delete :to
85 86
          @default_controller = options.delete(:controller) || scope[:controller]
          @default_action     = options.delete(:action) || scope[:action]
87
          @as                 = as
88
          @anchor             = options.delete :anchor
89

A
Aaron Patterson 已提交
90
          formatted = options.delete :format
91
          via = Array(options.delete(:via) { [] })
92
          options_constraints = options.delete :constraints
93

94
          path = normalize_path! path, formatted
95
          ast  = path_ast path
A
Aaron Patterson 已提交
96
          path_params = path_params ast
97

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

A
Aaron Patterson 已提交
100

101 102
          split_constraints(path_params, scope[:constraints]) if scope[:constraints]
          constraints = constraints(options, path_params)
103

104 105
          split_constraints path_params, constraints

106 107
          @blocks = blocks(options_constraints, scope[:blocks])

108 109 110 111 112 113 114 115 116 117
          if options_constraints.is_a?(Hash)
            split_constraints path_params, options_constraints
            options_constraints.each do |key, default|
              if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
                @defaults[key] ||= default
              end
            end
          end

          normalize_format!(formatted)
118 119 120 121

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

122
          add_request_method(via, @conditions)
123
          normalize_defaults!(options)
124
        end
J
Joshua Peek 已提交
125

126
        def to_route
127
          [ app(@blocks), conditions, requirements, defaults, as, anchor ]
128
        end
J
Joshua Peek 已提交
129

130
        private
131

132 133
          def normalize_path!(path, format)
            path = Mapper.normalize_path(path)
134

135 136 137 138 139 140
            if format == true
              "#{path}.:format"
            elsif optional_format?(path, format)
              "#{path}(.:format)"
            else
              path
141 142 143
            end
          end

144 145
          def optional_format?(path, format)
            format != false && !path.include?(':format') && !path.end_with?('/')
146 147
          end

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

157
            if path_params.include?(:controller)
158
              raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
159 160 161 162 163

              # 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' }
164
              options[:controller] ||= /.+?/
165
            end
166

A
Aaron Patterson 已提交
167 168 169
            if to.respond_to? :call
              options
            else
170 171 172 173 174 175 176
              to_endpoint = split_to to
              controller  = to_endpoint[0] || default_controller
              action      = to_endpoint[1] || default_action

              controller = add_controller_module(controller, modyoule)

              options.merge! check_controller_and_action(path_params, controller, action)
A
Aaron Patterson 已提交
177
            end
178 179
          end

180
          def split_constraints(path_params, constraints)
181 182 183 184 185 186 187
            constraints.each_pair do |key, requirement|
              if path_params.include?(key) || key == :controller
                verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
                @requirements[key] = requirement
              else
                @conditions[key] = requirement
              end
188
            end
189
          end
190

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

Y
Yves Senn 已提交
203 204 205 206 207 208 209 210 211 212
          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

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

221
          def verify_callable_constraint(callable_constraint)
222 223 224
            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
225 226
          end

227
          def add_request_method(via, conditions)
228 229 230 231 232 233 234 235 236
            return if via == [:all]

            if via.empty?
              msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
                    "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
                    "If you want to expose your action to GET, use `get` in the router:\n" \
                    "  Instead of: match \"controller#action\"\n" \
                    "  Do: get \"controller#action\""
              raise ArgumentError, msg
237
            end
238 239

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

242
          def app(blocks)
243
            if to.respond_to?(:call)
244
              Constraints.new(to, blocks, false)
B
Bruno Sutic 已提交
245 246
            elsif blocks.any?
              Constraints.new(dispatcher(defaults), blocks, true)
247
            else
B
Bruno Sutic 已提交
248
              dispatcher(defaults)
249
            end
250 251
          end

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

A
Aaron Patterson 已提交
258 259
                raise ArgumentError, message
              }
260
            end
261

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

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

279
          def split_to(to)
280 281
            if to =~ /#/
              to.split('#')
282 283
            else
              []
284
            end
285
          end
286

287
          def add_controller_module(controller, modyoule)
288 289
            if modyoule && !controller.is_a?(Regexp)
              if controller =~ %r{\A/}
290
                controller[1..-1]
291
              else
292
                [modyoule, controller].compact.join("/")
293
              end
294 295
            else
              controller
296 297 298
            end
          end

A
Aaron Patterson 已提交
299
          def translate_controller(controller)
300 301
            return controller if Regexp === controller
            return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
302

303
            yield
304 305
          end

306
          def blocks(options_constraints, scope_blocks)
307 308
            if options_constraints && !options_constraints.is_a?(Hash)
              verify_callable_constraint(options_constraints)
309
              [options_constraints]
310
            else
311
              scope_blocks || []
312 313
            end
          end
J
Joshua Peek 已提交
314

315 316
          def constraints(options, path_params)
            constraints = {}
317
            required_defaults = []
318
            options.each_pair do |key, option|
A
Aaron Patterson 已提交
319 320 321 322
              if Regexp === option
                constraints[key] = option
              else
                required_defaults << key unless path_params.include?(key)
323
              end
324
            end
325
            @conditions[:required_defaults] = required_defaults
326
            constraints
327
          end
J
Joshua Peek 已提交
328

A
Aaron Patterson 已提交
329 330
          def path_params(ast)
            ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
331
          end
332

333 334 335
          def path_ast(path)
            parser = Journey::Parser.new
            parser.parse path
336 337
          end

338 339
          def dispatcher(defaults)
            @set.dispatcher defaults
340 341
          end
      end
342

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

352
      def self.normalize_name(name)
353
        normalize_path(name)[1..-1].tr("/", "_")
354 355
      end

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

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

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

559
            options = app
560
            app, path = options.find { |k, _| k.respond_to?(:call) }
561 562 563 564 565
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

566 567
          rails_app = rails_app? app
          options[:as] ||= app_name(app, rails_app)
A
Aaron Patterson 已提交
568

569
          target_as       = name_for_action(options[:as], path)
P
Pratik Naik 已提交
570
          options[:via] ||= :all
571

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

574
          define_generate_prefix(app, target_as) if rails_app
575 576 577
          self
        end

578 579 580 581
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
582

583 584 585 586 587 588
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

589 590 591 592 593
        # Query if the following named route was already defined.
        def has_named_route?(name)
          @set.named_routes.routes[name.to_sym]
        end

594
        private
A
Aaron Patterson 已提交
595 596
          def rails_app?(app)
            app.is_a?(Class) && app < Rails::Railtie
597 598
          end

599 600
          def app_name(app, rails_app)
            if rails_app
601
              app.railtie_name
602 603
            elsif app.is_a?(Class)
              class_name = app.name
604 605 606 607
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
            end
          end

608
          def define_generate_prefix(app, name)
609
            _route = @set.named_routes.get name
P
Piotr Sarnacki 已提交
610 611
            _routes = @set
            app.routes.define_mounted_helper(name)
612
            app.routes.extend Module.new {
A
Aaron Patterson 已提交
613
              def optimize_routes_generation?; false; end
614
              define_method :find_script_name do |options|
615 616 617 618 619 620 621
                if options.key? :script_name
                  super(options)
                else
                  prefix_options = options.slice(*_route.segment_keys)
                  # we must actually delete prefix segment keys to avoid passing them to next url_for
                  _route.segment_keys.each { |k| options.delete(k) }
                  _routes.url_helpers.send("#{name}_path", prefix_options)
622
                end
623
              end
624
            }
625
          end
626 627 628
      end

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

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

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

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

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

        private
670
          def map_method(method, args, &block)
671
            options = args.extract_options!
672
            options[:via] = method
673
            match(*args, options, &block)
674 675 676 677
            self
          end
      end

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

767
          options[:path] = args.flatten.join('/') if args.any?
768
          options[:constraints] ||= {}
769

770
          unless nested_scope?
771 772
            options[:shallow_path] ||= options[:path] if options.key?(:path)
            options[:shallow_prefix] ||= options[:as] if options.key?(:as)
773 774
          end

775
          if options[:constraints].is_a?(Hash)
776 777 778 779 780
            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)
781 782
          else
            block, options[:constraints] = options[:constraints], {}
783 784
          end

785
          @scope.options.each do |option|
786 787 788 789 790 791 792 793 794
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
              value = options.delete(option)
            end

            if value
A
Aaron Patterson 已提交
795
              scope[option] = send("merge_#{option}_scope", @scope[option], value)
796
            end
797 798
          end

A
Aaron Patterson 已提交
799
          @scope = @scope.new scope
800 801 802
          yield
          self
        ensure
A
Aaron Patterson 已提交
803
          @scope = @scope.parent
804 805
        end

806 807 808
        # Scopes routes to a specific controller
        #
        #   controller "food" do
809
        #     match "bacon", action: :bacon, via: :get
810
        #   end
811 812 813
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
814 815
        end

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

          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 }
866
        end
867

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

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

936
        private
J
José Valim 已提交
937
          def merge_path_scope(parent, child) #:nodoc:
938
            Mapper.normalize_path("#{parent}/#{child}")
939 940
          end

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

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

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

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

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

961 962 963 964
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

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

J
José Valim 已提交
969
          def merge_constraints_scope(parent, child) #:nodoc:
970 971 972
            merge_options_scope(parent, child)
          end

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

J
José Valim 已提交
977
          def merge_blocks_scope(parent, child) #:nodoc:
978 979 980
            merged = parent ? parent.dup : []
            merged << child if child
            merged
981 982
          end

J
José Valim 已提交
983
          def merge_options_scope(parent, child) #:nodoc:
984
            (parent || {}).except(*override_keys(child)).merge!(child)
985
          end
986

J
José Valim 已提交
987
          def merge_shallow_scope(parent, child) #:nodoc:
988 989
            child ? true : false
          end
990

J
José Valim 已提交
991
          def override_keys(child) #:nodoc:
992 993
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
994 995
      end

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

1042
        class Resource #:nodoc:
1043
          attr_reader :controller, :path, :options, :param
1044 1045

          def initialize(entities, options = {})
1046
            @name       = entities.to_s
1047 1048 1049
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
1050
            @param      = (options[:param] || :id).to_sym
1051
            @options    = options
1052
            @shallow    = false
1053 1054
          end

1055
          def default_actions
1056
            [:index, :create, :new, :show, :update, :destroy, :edit]
1057 1058
          end

1059
          def actions
1060
            if only = @options[:only]
1061
              Array(only).map(&:to_sym)
1062
            elsif except = @options[:except]
1063 1064 1065 1066 1067 1068
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

1069
          def name
1070
            @as || @name
1071 1072
          end

1073
          def plural
1074
            @plural ||= name.to_s
1075 1076 1077
          end

          def singular
1078
            @singular ||= name.to_s.singularize
1079 1080
          end

1081
          alias :member_name :singular
1082

1083
          # Checks for uncountable plurals, and appends "_index" if the plural
1084
          # and singular form are the same.
1085
          def collection_name
1086
            singular == plural ? "#{plural}_index" : plural
1087 1088
          end

1089
          def resource_scope
1090
            { :controller => controller }
1091 1092
          end

1093
          alias :collection_scope :path
1094 1095

          def member_scope
1096
            "#{path}/:#{param}"
1097 1098
          end

1099 1100
          alias :shallow_scope :member_scope

1101
          def new_scope(new_path)
1102
            "#{path}/#{new_path}"
1103 1104
          end

1105 1106 1107 1108
          def nested_param
            :"#{singular}_#{param}"
          end

1109
          def nested_scope
1110
            "#{path}/:#{nested_param}"
1111
          end
1112

1113 1114 1115 1116 1117 1118 1119
          def shallow=(value)
            @shallow = value
          end

          def shallow?
            @shallow
          end
1120 1121 1122
        end

        class SingletonResource < Resource #:nodoc:
1123
          def initialize(entities, options)
1124
            super
1125
            @as         = nil
1126 1127
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1128 1129
          end

1130 1131 1132 1133
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

1134 1135
          def plural
            @plural ||= name.to_s.pluralize
1136 1137
          end

1138 1139
          def singular
            @singular ||= name.to_s
1140
          end
1141 1142 1143 1144 1145 1146

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1147 1148
        end

1149 1150 1151 1152
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

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

1177
          if apply_common_behavior_for(:resource, resources, options, &block)
1178 1179 1180
            return self
          end

1181
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1182
            yield if block_given?
1183

1184 1185
            concerns(options[:concerns]) if options[:concerns]

1186
            collection do
1187
              post :create
1188
            end if parent_resource.actions.include?(:create)
1189

1190
            new do
1191
              get :new
1192
            end if parent_resource.actions.include?(:new)
1193

1194
            set_member_mappings_for_resource
1195 1196
          end

J
Joshua Peek 已提交
1197
          self
1198 1199
        end

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

1335
          if apply_common_behavior_for(:resources, resources, options, &block)
1336 1337 1338
            return self
          end

1339
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1340
            yield if block_given?
J
Joshua Peek 已提交
1341

1342 1343
            concerns(options[:concerns]) if options[:concerns]

1344
            collection do
1345 1346
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1347
            end
1348

1349
            new do
1350
              get :new
1351
            end if parent_resource.actions.include?(:new)
1352

1353
            set_member_mappings_for_resource
1354 1355
          end

J
Joshua Peek 已提交
1356
          self
1357 1358
        end

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

1376 1377 1378 1379
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1380
          end
1381
        end
J
Joshua Peek 已提交
1382

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

1399
          with_scope_level(:member) do
1400 1401 1402 1403
            if shallow?
              shallow_scope(parent_resource.member_scope) { yield }
            else
              scope(parent_resource.member_scope) { yield }
1404
            end
1405 1406 1407 1408 1409 1410 1411
          end
        end

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

1413 1414 1415 1416
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1417
          end
J
Joshua Peek 已提交
1418 1419
        end

1420
        def nested
1421 1422
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1423 1424 1425
          end

          with_scope_level(:nested) do
1426
            if shallow? && shallow_nesting_depth >= 1
1427
              shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1428
            else
1429
              scope(parent_resource.nested_scope, nested_options) { yield }
1430 1431 1432 1433
            end
          end
        end

1434
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1435
        def namespace(path, options = {})
1436
          if resource_scope?
1437 1438 1439 1440 1441 1442
            nested { super }
          else
            super
          end
        end

1443
        def shallow
1444
          scope(:shallow => true) do
1445 1446 1447 1448
            yield
          end
        end

1449 1450 1451 1452
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

1453 1454 1455 1456 1457 1458
        # Matches a url pattern to one or more routes.
        # For more information, see match[rdoc-ref:Base#match].
        #
        #   match 'path' => 'controller#action', via: patch
        #   match 'path', to: 'controller#action', via: :post
        #   match 'path', 'otherpath', on: :member, via: :get
1459 1460 1461
        def match(path, *rest)
          if rest.empty? && Hash === path
            options  = path
1462
            path, to = options.find { |name, _value| name.is_a?(String) }
1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476

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

1477
            options.delete(path)
1478 1479 1480 1481 1482 1483
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1484 1485
          options[:anchor] = true unless options.key?(:anchor)

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

1490 1491 1492 1493
          if @scope[:controller] && @scope[:action]
            options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
          end

1494 1495 1496
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
1497 1498 1499 1500

            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')
1501
              route_options[:to].tr!("-", "_")
1502 1503
            end

1504 1505
            decomposed_match(_path, route_options)
          end
1506 1507
          self
        end
1508

1509
        def using_match_shorthand?(path, options)
1510
          path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$}
1511 1512
        end

1513
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1514 1515
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1516
          else
1517
            case @scope.scope_level
A
Aaron Patterson 已提交
1518 1519 1520 1521 1522 1523 1524
            when :resources
              nested { decomposed_match(path, options) }
            when :resource
              member { decomposed_match(path, options) }
            else
              add_route(path, options)
            end
J
Joshua Peek 已提交
1525
          end
1526
        end
J
Joshua Peek 已提交
1527

1528
        def add_route(action, options) # :nodoc:
1529
          path = path_for_action(action, options.delete(:path))
1530 1531
          raise ArgumentError, "path is required" if path.blank?

1532
          action = action.to_s.dup
1533

1534
          if action =~ /^[\w\-\/]+$/
1535
            options[:action] ||= action.tr('-', '_') unless action.include?("/")
1536
          else
1537 1538 1539
            action = nil
          end

1540 1541 1542 1543 1544
          as = if !options.fetch(:as, true) # if it's set to nil or false
                 options.delete(:as)
               else
                 name_for_action(options.delete(:as), action)
               end
J
Joshua Peek 已提交
1545

1546
          mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
1547 1548
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1549 1550
        end

1551 1552 1553 1554 1555 1556 1557 1558 1559
        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

A
Aaron Patterson 已提交
1560
          if @scope.resources?
1561 1562
            with_scope_level(:root) do
              scope(parent_resource.path) do
1563 1564 1565 1566 1567 1568
                super(options)
              end
            end
          else
            super(options)
          end
1569 1570
        end

1571
        protected
1572

1573
          def parent_resource #:nodoc:
1574 1575 1576
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1577
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1578 1579 1580 1581 1582
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1583 1584 1585 1586 1587 1588 1589
            if options.delete(:shallow)
              shallow do
                send(method, resources.pop, options, &block)
              end
              return true
            end

1590 1591 1592 1593 1594
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1595
            options.keys.each do |k|
1596 1597 1598
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1599 1600 1601
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1602 1603 1604 1605 1606
                send(method, resources.pop, options, &block)
              end
              return true
            end

1607 1608 1609 1610
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1611 1612 1613
            false
          end

J
José Valim 已提交
1614
          def action_options?(options) #:nodoc:
1615 1616 1617
            options[:only] || options[:except]
          end

J
José Valim 已提交
1618
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1619
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1620 1621
          end

J
José Valim 已提交
1622
          def scope_action_options #:nodoc:
1623 1624 1625
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1626
          def resource_scope? #:nodoc:
1627
            @scope.resource_scope?
1628 1629
          end

1630 1631
          def resource_method_scope? #:nodoc:
            @scope.resource_method_scope?
1632 1633
          end

1634
          def nested_scope? #:nodoc:
1635
            @scope.nested?
1636 1637
          end

1638
          def with_exclusive_scope
1639
            begin
A
Aaron Patterson 已提交
1640
              @scope = @scope.new(:as => nil, :path => nil)
1641

1642 1643 1644
              with_scope_level(:exclusive) do
                yield
              end
1645
            ensure
A
Aaron Patterson 已提交
1646
              @scope = @scope.parent
1647 1648 1649
            end
          end

1650
          def with_scope_level(kind)
1651
            @scope = @scope.new_level(kind)
J
Joshua Peek 已提交
1652 1653
            yield
          ensure
A
Aaron Patterson 已提交
1654
            @scope = @scope.parent
J
Joshua Peek 已提交
1655
          end
1656

1657
          def resource_scope(kind, resource) #:nodoc:
1658
            resource.shallow = @scope[:shallow]
A
Aaron Patterson 已提交
1659
            @scope = @scope.new(:scope_level_resource => resource)
1660
            @nesting.push(resource)
1661 1662 1663

            with_scope_level(kind) do
              scope(parent_resource.resource_scope) { yield }
1664
            end
1665
          ensure
1666
            @nesting.pop
A
Aaron Patterson 已提交
1667
            @scope = @scope.parent
1668 1669
          end

J
José Valim 已提交
1670
          def nested_options #:nodoc:
1671 1672
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1673 1674
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1675 1676

            options
1677 1678
          end

1679 1680 1681 1682
          def nesting_depth #:nodoc:
            @nesting.size
          end

1683
          def shallow_nesting_depth #:nodoc:
1684
            @nesting.count(&:shallow?)
1685 1686
          end

1687 1688
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1689 1690
          end

1691 1692
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1693 1694
          end

1695 1696
          def canonical_action?(action) #:nodoc:
            resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1697 1698
          end

1699
          def shallow_scope(path, options = {}) #:nodoc:
A
Aaron Patterson 已提交
1700 1701 1702
            scope = { :as   => @scope[:shallow_prefix],
                      :path => @scope[:shallow_path] }
            @scope = @scope.new scope
1703 1704 1705

            scope(path, options) { yield }
          ensure
A
Aaron Patterson 已提交
1706
            @scope = @scope.parent
1707 1708
          end

J
José Valim 已提交
1709
          def path_for_action(action, path) #:nodoc:
1710
            if path.blank? && canonical_action?(action)
1711
              @scope[:path].to_s
1712
            else
1713
              "#{@scope[:path]}/#{action_path(action, path)}"
1714 1715 1716
            end
          end

J
José Valim 已提交
1717
          def action_path(name, path = nil) #:nodoc:
1718
            name = name.to_sym if name.is_a?(String)
1719
            path || @scope[:path_names][name] || name.to_s
1720 1721
          end

1722
          def prefix_name_for_action(as, action) #:nodoc:
1723
            if as
1724
              prefix = as
1725
            elsif !canonical_action?(action)
1726
              prefix = action
1727
            end
A
Aaron Patterson 已提交
1728

A
Aaron Patterson 已提交
1729
            if prefix && prefix != '/' && !prefix.empty?
A
Aaron Patterson 已提交
1730 1731
              Mapper.normalize_name prefix.to_s.tr('-', '_')
            end
1732 1733
          end

J
José Valim 已提交
1734
          def name_for_action(as, action) #:nodoc:
1735
            prefix = prefix_name_for_action(as, action)
1736 1737 1738
            name_prefix = @scope[:as]

            if parent_resource
1739
              return nil unless as || action
1740

1741 1742
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1743
            end
1744

R
rono23 已提交
1745 1746
            action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
            candidate = action_name.select(&:present?).join('_')
1747

R
rono23 已提交
1748
            unless candidate.empty?
1749 1750 1751 1752
              # 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?
A
Aaron Patterson 已提交
1753
                candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
1754 1755 1756 1757
              else
                candidate
              end
            end
1758
          end
1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770

          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 已提交
1771
      end
J
Joshua Peek 已提交
1772

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

1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865
        # 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
1866 1867 1868
        def concerns(*args)
          options = args.extract_options!
          args.flatten.each do |name|
1869
            if concern = @concerns[name]
1870
              concern.call(self, options)
1871 1872 1873 1874 1875 1876 1877
            else
              raise ArgumentError, "No concern named #{name} was found!"
            end
          end
        end
      end

A
Aaron Patterson 已提交
1878
      class Scope # :nodoc:
1879 1880 1881 1882
        OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
                   :controller, :action, :path_names, :constraints,
                   :shallow, :blocks, :defaults, :options]

1883
        RESOURCE_SCOPES = [:resource, :resources]
1884
        RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1885

A
Aaron Patterson 已提交
1886
        attr_reader :parent, :scope_level
A
Aaron Patterson 已提交
1887

1888
        def initialize(hash, parent = {}, scope_level = nil)
A
Aaron Patterson 已提交
1889 1890
          @hash = hash
          @parent = parent
1891
          @scope_level = scope_level
A
Aaron Patterson 已提交
1892 1893
        end

1894 1895 1896 1897
        def nested?
          scope_level == :nested
        end

A
Aaron Patterson 已提交
1898 1899 1900 1901
        def resources?
          scope_level == :resources
        end

1902 1903 1904 1905
        def resource_method_scope?
          RESOURCE_METHOD_SCOPES.include? scope_level
        end

1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922
        def action_name(name_prefix, prefix, collection_name, member_name)
          case scope_level
          when :nested
            [name_prefix, prefix]
          when :collection
            [prefix, name_prefix, collection_name]
          when :new
            [prefix, :new, name_prefix, member_name]
          when :member
            [prefix, name_prefix, member_name]
          when :root
            [name_prefix, collection_name, prefix]
          else
            [name_prefix, member_name, prefix]
          end
        end

1923 1924 1925 1926
        def resource_scope?
          RESOURCE_SCOPES.include? scope_level
        end

1927 1928 1929 1930
        def options
          OPTIONS
        end

A
Aaron Patterson 已提交
1931
        def new(hash)
1932
          self.class.new hash, self, scope_level
A
Aaron Patterson 已提交
1933 1934
        end

1935
        def new_level(level)
1936 1937 1938 1939 1940
          self.class.new(self, self, level)
        end

        def fetch(key, &block)
          @hash.fetch(key, &block)
1941 1942
        end

A
Aaron Patterson 已提交
1943 1944 1945 1946 1947 1948 1949 1950 1951
        def [](key)
          @hash.fetch(key) { @parent[key] }
        end

        def []=(k,v)
          @hash[k] = v
        end
      end

1952 1953
      def initialize(set) #:nodoc:
        @set = set
A
Aaron Patterson 已提交
1954
        @scope = Scope.new({ :path_names => @set.resources_path_names })
1955
        @concerns = {}
1956
        @nesting = []
1957 1958
      end

1959 1960
      include Base
      include HttpHelpers
1961
      include Redirection
1962
      include Scoping
1963
      include Concerns
1964
      include Resources
J
Joshua Peek 已提交
1965 1966
    end
  end
J
Joshua Peek 已提交
1967
end