mapper.rb 63.7 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

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

18
      class Constraints #:nodoc:
19
        attr_reader :app, :constraints
20

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

31 32 33 34 35
          # Unwrap any constraints so we can see what's inside for route generation.
          # This allows the formatter to skip over any mounted applications or redirects
          # that shouldn't be matched when using a url_for without a route name.
          @dispatcher  = app.is_a?(Routing::RouteSet::Dispatcher)

36
          @app, @constraints, @request = app, constraints, request
37 38
        end

39 40
        def dispatcher?; @dispatcher; end

41
        def matches?(env)
42
          req = @request.new(env)
43

44 45 46
          @constraints.all? do |constraint|
            (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
              (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
G
Gosha Arinich 已提交
47
          end
48 49
        ensure
          req.reset_parameters
50 51 52 53
        end

        def call(env)
          matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
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 120 121 122 123 124 125 126
            end

            if path_without_format.match(':controller')
              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

130
            options.merge!(default_controller_and_action)
131 132 133 134 135
          end

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

140
            if options[:format] == true
141
              @requirements[:format] ||= /.+/
142 143 144 145 146
            elsif Regexp === options[:format]
              @requirements[:format] = options[:format]
            elsif String === options[:format]
              @requirements[:format] = Regexp.compile(options[:format])
            end
147
          end
148

Y
Yves Senn 已提交
149 150 151 152 153 154 155 156 157 158
          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

159 160 161
          def normalize_defaults!
            @defaults.merge!(scope[:defaults]) if scope[:defaults]
            @defaults.merge!(options[:defaults]) if options[:defaults]
162

163
            options.each do |key, default|
A
Akshay Vishnoi 已提交
164 165 166
              unless Regexp === default || IGNORE_OPTIONS.include?(key)
                @defaults[key] = default
              end
167 168
            end

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

179 180 181 182
            if Regexp === options[:format]
              @defaults[:format] = nil
            elsif String === options[:format]
              @defaults[:format] = options[:format]
183
            end
184
          end
185

186
          def verify_callable_constraint(callable_constraint)
187 188 189
            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
190 191
          end

192
          def normalize_conditions!
193
            @conditions[:path_info] = path
194

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

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

209 210 211 212
            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" \
213 214
                    "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" \
215 216 217
                    "  Instead of: match \"controller#action\"\n" \
                    "  Do: get \"controller#action\""
              raise msg
218
            end
219

220
            if via = options[:via]
221
              @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase }
222 223 224
            end
          end

225
          def app
226 227
            endpoint = to.respond_to?(:call) ? to : dispatcher

228 229 230
            if blocks.any?
              Constraints.new(endpoint, blocks, @set.request_class)
            else
231
              Constraints.new(endpoint, blocks, @set.request_class)
232
            end
233 234
          end

235
          def default_controller_and_action
236
            if to.respond_to?(:call)
237 238
              { }
            else
239
              if to.is_a?(String)
240
                controller, action = to.split('#')
241 242
              elsif to.is_a?(Symbol)
                action = to.to_s
243
              end
J
Joshua Peek 已提交
244

245 246
              controller ||= default_controller
              action     ||= default_action
247

248 249 250 251 252 253
              if @scope[:module] && !controller.is_a?(Regexp)
                if controller =~ %r{\A/}
                  controller = controller[1..-1]
                else
                  controller = [@scope[:module], controller].compact.join("/").presence
                end
254
              end
255

256 257 258 259
              if controller.is_a?(String) && controller =~ %r{\A/}
                raise ArgumentError, "controller name should not start with a slash"
              end

260 261
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
262

263
              if controller.blank? && segment_keys.exclude?(:controller)
264 265
                message = "Missing :controller key on routes definition, please check your routes."
                raise ArgumentError, message
266
              end
J
Joshua Peek 已提交
267

268
              if action.blank? && segment_keys.exclude?(:action)
269 270
                message = "Missing :action key on routes definition, please check your routes."
                raise ArgumentError, message
271
              end
J
Joshua Peek 已提交
272

273
              if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
274 275 276 277 278
                message = "'#{controller}' 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"
                raise ArgumentError, message
              end

A
Aaron Patterson 已提交
279
              hash = {}
A
Aaron Patterson 已提交
280 281
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
282
              hash
283 284
            end
          end
285

286
          def blocks
287 288
            if options[:constraints].present? && !options[:constraints].is_a?(Hash)
              [options[:constraints]]
289
            else
290
              scope[:blocks] || []
291 292
            end
          end
J
Joshua Peek 已提交
293

294
          def constraints
295 296
            @constraints ||= {}.tap do |constraints|
              constraints.merge!(scope[:constraints]) if scope[:constraints]
297

298 299 300 301 302
              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)
303
            end
304
          end
J
Joshua Peek 已提交
305

306
          def segment_keys
307 308 309 310 311 312
            @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
          end

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

314 315 316 317 318
          def strexp
            Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
          end

          def dispatcher
319
            Routing::RouteSet::Dispatcher.new(defaults)
320 321
          end
      end
322

323
      # Invokes Journey::Router::Utils.normalize_path and ensure that
324 325
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
326
      def self.normalize_path(path)
327
        path = Journey::Router::Utils.normalize_path(path)
328
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
329 330 331
        path
      end

332
      def self.normalize_name(name)
333
        normalize_path(name)[1..-1].tr("/", "_")
334 335
      end

336
      module Base
337 338
        # You can specify what Rails should route "/" to with the root method:
        #
A
AvnerCohen 已提交
339
        #   root to: 'pages#main'
340
        #
341
        # For options, see +match+, as +root+ uses it internally.
342
        #
343 344 345 346
        # You can also pass a string which will expand
        #
        #   root 'pages#main'
        #
347 348 349
        # 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.
350
        def root(options = {})
351
          match '/', { :as => :root, :via => :get }.merge!(options)
352
        end
353

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

506 507
        # Mount a Rack-based application to be used within the application.
        #
A
AvnerCohen 已提交
508
        #   mount SomeRackApp, at: "some_route"
509 510 511
        #
        # Alternatively:
        #
R
Ryan Bigg 已提交
512
        #   mount(SomeRackApp => "some_route")
513
        #
514 515
        # For options, see +match+, as +mount+ uses it internally.
        #
516 517 518 519 520
        # 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 已提交
521
        #   mount(SomeRackApp => "some_route", as: "exciting")
522 523 524
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
525 526 527 528
        def mount(app, options = nil)
          if options
            path = options.delete(:at)
          else
529 530 531 532
            unless Hash === app
              raise ArgumentError, "must be called with mount point"
            end

533
            options = app
534
            app, path = options.find { |k, _| k.respond_to?(:call) }
535 536 537 538 539
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

P
Pratik Naik 已提交
540
          options[:as]  ||= app_name(app)
541
          target_as       = name_for_action(options[:as], path)
P
Pratik Naik 已提交
542
          options[:via] ||= :all
543

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

546
          define_generate_prefix(app, target_as)
547 548 549
          self
        end

550 551 552 553
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
554

555 556 557 558 559 560
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

561 562 563 564 565
        # Query if the following named route was already defined.
        def has_named_route?(name)
          @set.named_routes.routes[name.to_sym]
        end

566 567 568
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
569 570 571 572 573

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
574
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
575
            end
576 577 578
          end

          def define_generate_prefix(app, name)
579
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
580 581

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
582 583
            _routes = @set
            app.routes.define_mounted_helper(name)
584 585
            app.routes.extend Module.new {
              def mounted?; true; end
586 587
              define_method :find_script_name do |options|
                super(options) || begin
P
Piotr Sarnacki 已提交
588
                prefix_options = options.slice(*_route.segment_keys)
589 590
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
591
                _routes.url_helpers.send("#{name}_path", prefix_options)
592
                end
593
              end
594
            }
595
          end
596 597 598
      end

      module HttpHelpers
599
        # Define a route that only recognizes HTTP GET.
C
Cesar Carruitero 已提交
600
        # For supported arguments, see match[rdoc-ref:Base#match]
601
        #
A
AvnerCohen 已提交
602
        #   get 'bacon', to: 'food#bacon'
603
        def get(*args, &block)
604
          map_method(:get, args, &block)
605 606
        end

607
        # Define a route that only recognizes HTTP POST.
C
Cesar Carruitero 已提交
608
        # For supported arguments, see match[rdoc-ref:Base#match]
609
        #
A
AvnerCohen 已提交
610
        #   post 'bacon', to: 'food#bacon'
611
        def post(*args, &block)
612
          map_method(:post, args, &block)
613 614
        end

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

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

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

        private
640
          def map_method(method, args, &block)
641
            options = args.extract_options!
642
            options[:via] = method
643
            match(*args, options, &block)
644 645 646 647
            self
          end
      end

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

737
          options[:path] = args.flatten.join('/') if args.any?
738
          options[:constraints] ||= {}
739

740
          unless nested_scope?
741 742
            options[:shallow_path] ||= options[:path] if options.key?(:path)
            options[:shallow_prefix] ||= options[:as] if options.key?(:as)
743 744
          end

745
          if options[:constraints].is_a?(Hash)
746 747 748 749 750
            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)
751 752
          else
            block, options[:constraints] = options[:constraints], {}
753 754
          end

755 756 757 758 759 760 761 762 763 764
          SCOPE_OPTIONS.each do |option|
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
              value = options.delete(option)
            end

            if value
765 766 767
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
768 769 770 771 772
          end

          yield
          self
        ensure
773
          @scope.merge!(recover)
774 775
        end

776 777 778
        # Scopes routes to a specific controller
        #
        #   controller "food" do
A
AvnerCohen 已提交
779
        #     match "bacon", action: "bacon"
780
        #   end
781 782 783
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
784 785
        end

786 787 788 789 790 791 792 793
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
794 795 796 797 798
        #       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
799
        #        admin_post PATCH/PUT /admin/posts/:id(.:format)      admin/posts#update
800
        #        admin_post DELETE    /admin/posts/:id(.:format)      admin/posts#destroy
801
        #
802
        # === Options
803
        #
804 805
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
        # options all default to the name of the namespace.
806
        #
807 808
        # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
        # <tt>Resources#resources</tt>.
809
        #
810
        #   # accessible through /sekret/posts rather than /admin/posts
A
AvnerCohen 已提交
811
        #   namespace :admin, path: "sekret" do
812 813
        #     resources :posts
        #   end
814
        #
S
Sebastian Martinez 已提交
815
        #   # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
816
        #   namespace :admin, module: "sekret" do
817 818
        #     resources :posts
        #   end
819
        #
S
Sebastian Martinez 已提交
820
        #   # generates +sekret_posts_path+ rather than +admin_posts_path+
A
AvnerCohen 已提交
821
        #   namespace :admin, as: "sekret" do
822 823
        #     resources :posts
        #   end
824
        def namespace(path, options = {})
825
          path = path.to_s
826 827 828 829 830 831 832 833 834 835

          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 }
836
        end
837

R
Ryan Bigg 已提交
838 839 840 841
        # === 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 已提交
842
        #   constraints(id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
843 844 845 846 847
        #     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.
848
        #
R
R.T. Lechow 已提交
849
        # You may use this to also restrict other parameters:
R
Ryan Bigg 已提交
850 851
        #
        #   resources :posts do
A
AvnerCohen 已提交
852
        #     constraints(post_id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
853 854
        #       resources :comments
        #     end
J
James Miller 已提交
855
        #   end
R
Ryan Bigg 已提交
856 857 858 859 860
        #
        # === Restricting based on IP
        #
        # Routes can also be constrained to an IP or a certain range of IP addresses:
        #
A
AvnerCohen 已提交
861
        #   constraints(ip: /192\.168\.\d+\.\d+/) do
R
Ryan Bigg 已提交
862 863 864 865 866 867 868 869
        #     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 已提交
870
        # Requests to routes can be constrained based on specific criteria:
R
Ryan Bigg 已提交
871 872 873 874 875 876 877 878 879 880
        #
        #    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
881
        #      def self.matches?(request)
R
Ryan Bigg 已提交
882 883 884 885 886 887 888 889 890 891 892
        #        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
893 894 895 896
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
897
        # Allows you to set default parameters for a route, such as this:
A
AvnerCohen 已提交
898 899
        #   defaults id: 'home' do
        #     match 'scoped_pages/(:id)', to: 'pages#show'
900
        #   end
R
Ryan Bigg 已提交
901
        # Using this, the +:id+ parameter here will default to 'home'.
902 903 904 905
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

906
        private
J
José Valim 已提交
907
          def merge_path_scope(parent, child) #:nodoc:
908
            Mapper.normalize_path("#{parent}/#{child}")
909 910
          end

J
José Valim 已提交
911
          def merge_shallow_path_scope(parent, child) #:nodoc:
912 913 914
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
915
          def merge_as_scope(parent, child) #:nodoc:
916
            parent ? "#{parent}_#{child}" : child
917 918
          end

J
José Valim 已提交
919
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
920 921 922
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
923
          def merge_module_scope(parent, child) #:nodoc:
924 925 926
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
927
          def merge_controller_scope(parent, child) #:nodoc:
928
            child
929 930
          end

931 932 933 934
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

J
José Valim 已提交
935
          def merge_path_names_scope(parent, child) #:nodoc:
936 937 938
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
939
          def merge_constraints_scope(parent, child) #:nodoc:
940 941 942
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
943
          def merge_defaults_scope(parent, child) #:nodoc:
944 945 946
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
947
          def merge_blocks_scope(parent, child) #:nodoc:
948 949 950
            merged = parent ? parent.dup : []
            merged << child if child
            merged
951 952
          end

J
José Valim 已提交
953
          def merge_options_scope(parent, child) #:nodoc:
954
            (parent || {}).except(*override_keys(child)).merge!(child)
955
          end
956

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

J
José Valim 已提交
961
          def override_keys(child) #:nodoc:
962 963
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
964 965
      end

966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989
      # 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 已提交
990 991
      # <tt>app/controllers/admin</tt> directory, and you can group them together
      # in your router:
992 993 994 995 996
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
S
Sebastian Martinez 已提交
997 998
      # 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
999 1000
      # overrides this restriction, e.g:
      #
A
AvnerCohen 已提交
1001
      #   resources :articles, id: /[^\/]+/
1002
      #
S
Sebastian Martinez 已提交
1003
      # This allows any character other than a slash as part of your +:id+.
1004
      #
J
Joshua Peek 已提交
1005
      module Resources
1006 1007
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
1008
        VALID_ON_OPTIONS  = [:new, :collection, :member]
1009
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except, :param, :concerns]
1010
        CANONICAL_ACTIONS = %w(index create new show update destroy)
1011 1012
        RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
        RESOURCE_SCOPES = [:resource, :resources]
1013

1014
        class Resource #:nodoc:
1015
          attr_reader :controller, :path, :options, :param
1016 1017

          def initialize(entities, options = {})
1018
            @name       = entities.to_s
1019 1020 1021
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
1022
            @param      = (options[:param] || :id).to_sym
1023
            @options    = options
1024
            @shallow    = false
1025 1026
          end

1027
          def default_actions
1028
            [:index, :create, :new, :show, :update, :destroy, :edit]
1029 1030
          end

1031
          def actions
1032
            if only = @options[:only]
1033
              Array(only).map(&:to_sym)
1034
            elsif except = @options[:except]
1035 1036 1037 1038 1039 1040
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

1041
          def name
1042
            @as || @name
1043 1044
          end

1045
          def plural
1046
            @plural ||= name.to_s
1047 1048 1049
          end

          def singular
1050
            @singular ||= name.to_s.singularize
1051 1052
          end

1053
          alias :member_name :singular
1054

1055
          # Checks for uncountable plurals, and appends "_index" if the plural
1056
          # and singular form are the same.
1057
          def collection_name
1058
            singular == plural ? "#{plural}_index" : plural
1059 1060
          end

1061
          def resource_scope
1062
            { :controller => controller }
1063 1064
          end

1065
          alias :collection_scope :path
1066 1067

          def member_scope
1068
            "#{path}/:#{param}"
1069 1070
          end

1071 1072
          alias :shallow_scope :member_scope

1073
          def new_scope(new_path)
1074
            "#{path}/#{new_path}"
1075 1076
          end

1077 1078 1079 1080
          def nested_param
            :"#{singular}_#{param}"
          end

1081
          def nested_scope
1082
            "#{path}/:#{nested_param}"
1083
          end
1084

1085 1086 1087 1088 1089 1090 1091
          def shallow=(value)
            @shallow = value
          end

          def shallow?
            @shallow
          end
1092 1093 1094
        end

        class SingletonResource < Resource #:nodoc:
1095
          def initialize(entities, options)
1096
            super
1097
            @as         = nil
1098 1099
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1100 1101
          end

1102 1103 1104 1105
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

1106 1107
          def plural
            @plural ||= name.to_s.pluralize
1108 1109
          end

1110 1111
          def singular
            @singular ||= name.to_s
1112
          end
1113 1114 1115 1116 1117 1118

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1119 1120
        end

1121 1122 1123 1124
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

1125 1126 1127 1128 1129 1130
        # 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:
        #
1131
        #   resource :profile
1132 1133
        #
        # creates six different routes in your application, all mapping to
1134
        # the +Profiles+ controller (note that the controller is named after
1135 1136
        # the plural):
        #
1137 1138 1139 1140 1141 1142
        #   GET       /profile/new
        #   POST      /profile
        #   GET       /profile
        #   GET       /profile/edit
        #   PATCH/PUT /profile
        #   DELETE    /profile
1143
        #
1144
        # === Options
1145
        # Takes same options as +resources+.
J
Joshua Peek 已提交
1146
        def resource(*resources, &block)
1147
          options = resources.extract_options!.dup
J
Joshua Peek 已提交
1148

1149
          if apply_common_behavior_for(:resource, resources, options, &block)
1150 1151 1152
            return self
          end

1153
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1154
            yield if block_given?
1155

1156 1157
            concerns(options[:concerns]) if options[:concerns]

1158
            collection do
1159
              post :create
1160
            end if parent_resource.actions.include?(:create)
1161

1162
            new do
1163
              get :new
1164
            end if parent_resource.actions.include?(:new)
1165

1166
            set_member_mappings_for_resource
1167 1168
          end

J
Joshua Peek 已提交
1169
          self
1170 1171
        end

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

1307
          if apply_common_behavior_for(:resources, resources, options, &block)
1308 1309 1310
            return self
          end

1311
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1312
            yield if block_given?
J
Joshua Peek 已提交
1313

1314 1315
            concerns(options[:concerns]) if options[:concerns]

1316
            collection do
1317 1318
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1319
            end
1320

1321
            new do
1322
              get :new
1323
            end if parent_resource.actions.include?(:new)
1324

1325
            set_member_mappings_for_resource
1326 1327
          end

J
Joshua Peek 已提交
1328
          self
1329 1330
        end

1331 1332 1333 1334 1335 1336 1337 1338 1339
        # 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 已提交
1340
        # with GET, and route to the search action of +PhotosController+. It will also
1341 1342
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1343
        def collection
1344 1345
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1346 1347
          end

1348 1349 1350 1351
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1352
          end
1353
        end
J
Joshua Peek 已提交
1354

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

1371
          with_scope_level(:member) do
1372 1373 1374 1375
            if shallow?
              shallow_scope(parent_resource.member_scope) { yield }
            else
              scope(parent_resource.member_scope) { yield }
1376
            end
1377 1378 1379 1380 1381 1382 1383
          end
        end

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

1385 1386 1387 1388
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1389
          end
J
Joshua Peek 已提交
1390 1391
        end

1392
        def nested
1393 1394
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1395 1396 1397
          end

          with_scope_level(:nested) do
1398
            if shallow? && shallow_nesting_depth > 1
1399
              shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1400
            else
1401
              scope(parent_resource.nested_scope, nested_options) { yield }
1402 1403 1404 1405
            end
          end
        end

1406
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1407
        def namespace(path, options = {})
1408
          if resource_scope?
1409 1410 1411 1412 1413 1414
            nested { super }
          else
            super
          end
        end

1415
        def shallow
1416
          scope(:shallow => true) do
1417 1418 1419 1420
            yield
          end
        end

1421 1422 1423 1424
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

1425
        # match 'path' => 'controller#action'
R
Rafael Mendonça França 已提交
1426
        # match 'path', to: 'controller#action'
1427
        # match 'path', 'otherpath', on: :member, via: :get
1428 1429 1430
        def match(path, *rest)
          if rest.empty? && Hash === path
            options  = path
1431
            path, to = options.find { |name, _value| name.is_a?(String) }
1432 1433
            options[:to] = to
            options.delete(path)
1434 1435 1436 1437 1438 1439
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1440 1441
          options[:anchor] = true unless options.key?(:anchor)

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

1446 1447 1448 1449
          if @scope[:controller] && @scope[:action]
            options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
          end

1450 1451 1452
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
1453 1454 1455 1456

            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')
1457
              route_options[:to].tr!("-", "_")
1458 1459
            end

1460 1461
            decomposed_match(_path, route_options)
          end
1462 1463
          self
        end
1464

1465 1466 1467 1468
        def using_match_shorthand?(path, options)
          path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
        end

1469
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1470 1471
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1472
          else
A
Aaron Patterson 已提交
1473 1474 1475 1476 1477 1478 1479 1480
            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 已提交
1481
          end
1482
        end
J
Joshua Peek 已提交
1483

1484
        def add_route(action, options) # :nodoc:
1485
          path = path_for_action(action, options.delete(:path))
1486
          action = action.to_s.dup
1487

1488
          if action =~ /^[\w\-\/]+$/
1489
            options[:action] ||= action.tr('-', '_') unless action.include?("/")
1490
          else
1491 1492 1493
            action = nil
          end

1494
          if !options.fetch(:as, true)
1495 1496 1497
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1498
          end
J
Joshua Peek 已提交
1499

1500
          mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1501 1502
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1503 1504
        end

1505 1506 1507 1508 1509 1510 1511 1512 1513
        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

1514
          if @scope[:scope_level] == :resources
1515 1516
            with_scope_level(:root) do
              scope(parent_resource.path) do
1517 1518 1519 1520 1521 1522
                super(options)
              end
            end
          else
            super(options)
          end
1523 1524
        end

1525
        protected
1526

1527
          def parent_resource #:nodoc:
1528 1529 1530
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1531
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1532 1533 1534 1535 1536
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1537 1538 1539 1540 1541 1542 1543
            if options.delete(:shallow)
              shallow do
                send(method, resources.pop, options, &block)
              end
              return true
            end

1544 1545 1546 1547 1548
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1549
            options.keys.each do |k|
1550 1551 1552
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1553 1554 1555
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1556 1557 1558 1559 1560
                send(method, resources.pop, options, &block)
              end
              return true
            end

1561 1562 1563 1564
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1565 1566 1567
            false
          end

J
José Valim 已提交
1568
          def action_options?(options) #:nodoc:
1569 1570 1571
            options[:only] || options[:except]
          end

J
José Valim 已提交
1572
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1573
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1574 1575
          end

J
José Valim 已提交
1576
          def scope_action_options #:nodoc:
1577 1578 1579
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1580
          def resource_scope? #:nodoc:
1581
            RESOURCE_SCOPES.include? @scope[:scope_level]
1582 1583
          end

J
José Valim 已提交
1584
          def resource_method_scope? #:nodoc:
1585
            RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1586 1587
          end

1588 1589 1590 1591
          def nested_scope? #:nodoc:
            @scope[:scope_level] == :nested
          end

1592
          def with_exclusive_scope
1593
            begin
1594 1595
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1596

1597 1598 1599
              with_scope_level(:exclusive) do
                yield
              end
1600
            ensure
1601
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1602 1603 1604
            end
          end

1605
          def with_scope_level(kind)
J
Joshua Peek 已提交
1606 1607 1608 1609 1610
            old, @scope[:scope_level] = @scope[:scope_level], kind
            yield
          ensure
            @scope[:scope_level] = old
          end
1611

1612
          def resource_scope(kind, resource) #:nodoc:
1613
            resource.shallow = @scope[:shallow]
1614
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1615
            @nesting.push(resource)
1616 1617 1618

            with_scope_level(kind) do
              scope(parent_resource.resource_scope) { yield }
1619
            end
1620
          ensure
1621
            @nesting.pop
1622
            @scope[:scope_level_resource] = old_resource
1623 1624
          end

J
José Valim 已提交
1625
          def nested_options #:nodoc:
1626 1627
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1628 1629
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1630 1631

            options
1632 1633
          end

1634 1635 1636 1637
          def nesting_depth #:nodoc:
            @nesting.size
          end

1638 1639 1640 1641
          def shallow_nesting_depth #:nodoc:
            @nesting.select(&:shallow?).size
          end

1642 1643
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1644 1645
          end

1646 1647
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1648 1649
          end

J
José Valim 已提交
1650
          def canonical_action?(action, flag) #:nodoc:
1651
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1652 1653
          end

1654 1655 1656 1657 1658 1659 1660
          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
1661 1662
          end

J
José Valim 已提交
1663
          def path_for_action(action, path) #:nodoc:
1664
            if canonical_action?(action, path.blank?)
1665
              @scope[:path].to_s
1666
            else
1667
              "#{@scope[:path]}/#{action_path(action, path)}"
1668 1669 1670
            end
          end

J
José Valim 已提交
1671
          def action_path(name, path = nil) #:nodoc:
1672
            name = name.to_sym if name.is_a?(String)
1673
            path || @scope[:path_names][name] || name.to_s
1674 1675
          end

J
José Valim 已提交
1676
          def prefix_name_for_action(as, action) #:nodoc:
1677
            if as
1678
              prefix = as
1679
            elsif !canonical_action?(action, @scope[:scope_level])
1680
              prefix = action
1681
            end
1682
            prefix.to_s.tr('-', '_') if prefix
1683 1684
          end

J
José Valim 已提交
1685
          def name_for_action(as, action) #:nodoc:
1686
            prefix = prefix_name_for_action(as, action)
1687
            prefix = Mapper.normalize_name(prefix) if prefix
1688 1689 1690
            name_prefix = @scope[:as]

            if parent_resource
1691
              return nil unless as || action
1692

1693 1694
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1695
            end
1696

1697
            name = case @scope[:scope_level]
1698
            when :nested
1699
              [name_prefix, prefix]
1700
            when :collection
1701
              [prefix, name_prefix, collection_name]
1702
            when :new
1703 1704
              [prefix, :new, name_prefix, member_name]
            when :member
1705
              [prefix, name_prefix, member_name]
1706 1707
            when :root
              [name_prefix, collection_name, prefix]
1708
            else
1709
              [name_prefix, member_name, prefix]
1710
            end
1711

1712 1713 1714 1715 1716 1717 1718 1719 1720 1721
            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
1722
          end
1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734

          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 已提交
1735
      end
J
Joshua Peek 已提交
1736

1737
      # Routing Concerns allow you to declare common routes that can be reused
1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756
      # 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
1757
      module Concerns
1758
        # Define a routing concern using a name.
1759
        #
1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781
        # 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]
1782 1783
        #   end
        #
1784 1785 1786
        # Or, using a callable object, you might implement something more
        # specific to your application, which would be out of place in your
        # routes file.
1787
        #
1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798
        #   # 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]
1799 1800 1801
        #     end
        #   end
        #
1802 1803 1804 1805 1806 1807 1808 1809
        #   # routes.rb
        #   concern :purchasable, Purchasable.new(returnable: true)
        #
        #   resources :toys, concerns: :purchasable
        #   resources :electronics, concerns: :purchasable
        #   resources :pets do
        #     concerns :purchasable, returnable: false
        #   end
1810
        #
1811 1812 1813
        # 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>.
1814
        def concern(name, callable = nil, &block)
1815 1816
          callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
          @concerns[name] = callable
1817 1818
        end

1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829
        # 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
1830 1831 1832
        def concerns(*args)
          options = args.extract_options!
          args.flatten.each do |name|
1833
            if concern = @concerns[name]
1834
              concern.call(self, options)
1835 1836 1837 1838 1839 1840 1841
            else
              raise ArgumentError, "No concern named #{name} was found!"
            end
          end
        end
      end

1842 1843 1844
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1845
        @concerns = {}
1846
        @nesting = []
1847 1848
      end

1849 1850
      include Base
      include HttpHelpers
1851
      include Redirection
1852
      include Scoping
1853
      include Concerns
1854
      include Resources
J
Joshua Peek 已提交
1855 1856
    end
  end
J
Joshua Peek 已提交
1857
end