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

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

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

22
        def initialize(app, constraints, dispatcher_p)
23 24 25 26
          # Unwrap Constraints objects.  I don't actually think it's possible
          # to pass a Constraints object to this constructor, but there were
          # multiple places that kept testing children of this object.  I
          # *think* they were just being defensive, but I have no idea.
27
          if app.is_a?(self.class)
28 29 30 31
            constraints += app.constraints
            app = app.app
          end

32
          @dispatcher = dispatcher_p
33

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

37 38
        def dispatcher?; @dispatcher; end

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

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

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

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

62
      class Mapping #:nodoc:
63
        IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
64
        ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
65
        WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
66

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

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

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

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

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

90
        private
91

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

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

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

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

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

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

228
            if to.respond_to?(:call)
229
              Constraints.new(to, blocks, false)
230
            else
231
              if blocks.any?
232
                Constraints.new(dispatcher, blocks, true)
233 234 235
              else
                dispatcher
              end
236
            end
237 238
          end

239
          def default_controller_and_action
240
            if to.respond_to?(:call)
241 242
              { }
            else
243 244 245 246 247
              controller = default_controller
              action     = default_action

              case to
              when Symbol
248
                action = to.to_s
249 250 251 252
              when /#/
                controller, action = to.split('#')
              when String
                controller = to
253
              end
J
Joshua Peek 已提交
254

255 256 257 258 259 260
              if @scope[:module] && !controller.is_a?(Regexp)
                if controller =~ %r{\A/}
                  controller = controller[1..-1]
                else
                  controller = [@scope[:module], controller].compact.join("/").presence
                end
261
              end
262

263 264
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
265

266 267
              check_action! action
              check_controller! controller
268

A
Aaron Patterson 已提交
269
              hash = {}
A
Aaron Patterson 已提交
270 271
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
272
              hash
273 274
            end
          end
275

276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
          def check_action!(action)
            if action.blank? && segment_keys.exclude?(:action)
              message = "Missing :action key on routes definition, please check your routes."
              raise ArgumentError, message
            end
          end

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

            if controller.blank? && segment_keys.exclude?(:controller)
              message = "Missing :controller key on routes definition, please check your routes."
              raise ArgumentError, message
            end

            if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
              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
          end

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

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

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

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

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

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

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

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

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

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

350
      module Base
351 352
        # You can specify what Rails should route "/" to with the root method:
        #
A
AvnerCohen 已提交
353
        #   root to: 'pages#main'
354
        #
355
        # For options, see +match+, as +root+ uses it internally.
356
        #
357 358 359 360
        # You can also pass a string which will expand
        #
        #   root 'pages#main'
        #
361 362 363
        # You should put the root route at the top of <tt>config/routes.rb</tt>,
        # because this means it will be matched first. As this is the most popular route
        # of most Rails applications, this is beneficial.
364
        def root(options = {})
365
          match '/', { :as => :root, :via => :get }.merge!(options)
366
        end
367

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

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

547
            options = app
548
            app, path = options.find { |k, _| k.respond_to?(:call) }
549 550 551 552 553
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

P
Pratik Naik 已提交
554
          options[:as]  ||= app_name(app)
555
          target_as       = name_for_action(options[:as], path)
P
Pratik Naik 已提交
556
          options[:via] ||= :all
557

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

560
          define_generate_prefix(app, target_as)
561 562 563
          self
        end

564 565 566 567
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
568

569 570 571 572 573 574
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

575 576 577 578 579
        # Query if the following named route was already defined.
        def has_named_route?(name)
          @set.named_routes.routes[name.to_sym]
        end

580 581 582
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
583 584 585 586 587

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
588
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
589
            end
590 591 592
          end

          def define_generate_prefix(app, name)
593
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
594 595

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

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

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

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

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

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

        private
654
          def map_method(method, args, &block)
655
            options = args.extract_options!
656
            options[:via] = method
657
            match(*args, options, &block)
658 659 660 661
            self
          end
      end

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

751
          options[:path] = args.flatten.join('/') if args.any?
752
          options[:constraints] ||= {}
753

754
          unless nested_scope?
755 756
            options[:shallow_path] ||= options[:path] if options.key?(:path)
            options[:shallow_prefix] ||= options[:as] if options.key?(:as)
757 758
          end

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

769 770 771 772 773 774 775 776 777 778
          SCOPE_OPTIONS.each do |option|
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
              value = options.delete(option)
            end

            if value
779 780 781
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
782 783 784 785 786
          end

          yield
          self
        ensure
787
          @scope.merge!(recover)
788 789
        end

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

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

          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 }
850
        end
851

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

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

920
        private
J
José Valim 已提交
921
          def merge_path_scope(parent, child) #:nodoc:
922
            Mapper.normalize_path("#{parent}/#{child}")
923 924
          end

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

J
José Valim 已提交
929
          def merge_as_scope(parent, child) #:nodoc:
930
            parent ? "#{parent}_#{child}" : child
931 932
          end

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

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

J
José Valim 已提交
941
          def merge_controller_scope(parent, child) #:nodoc:
942
            child
943 944
          end

945 946 947 948
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

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

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

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

J
José Valim 已提交
961
          def merge_blocks_scope(parent, child) #:nodoc:
962 963 964
            merged = parent ? parent.dup : []
            merged << child if child
            merged
965 966
          end

J
José Valim 已提交
967
          def merge_options_scope(parent, child) #:nodoc:
968
            (parent || {}).except(*override_keys(child)).merge!(child)
969
          end
970

J
José Valim 已提交
971
          def merge_shallow_scope(parent, child) #:nodoc:
972 973
            child ? true : false
          end
974

J
José Valim 已提交
975
          def override_keys(child) #:nodoc:
976 977
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
978 979
      end

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

1028
        class Resource #:nodoc:
1029
          attr_reader :controller, :path, :options, :param
1030 1031

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

1041
          def default_actions
1042
            [:index, :create, :new, :show, :update, :destroy, :edit]
1043 1044
          end

1045
          def actions
1046
            if only = @options[:only]
1047
              Array(only).map(&:to_sym)
1048
            elsif except = @options[:except]
1049 1050 1051 1052 1053 1054
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

1055
          def name
1056
            @as || @name
1057 1058
          end

1059
          def plural
1060
            @plural ||= name.to_s
1061 1062 1063
          end

          def singular
1064
            @singular ||= name.to_s.singularize
1065 1066
          end

1067
          alias :member_name :singular
1068

1069
          # Checks for uncountable plurals, and appends "_index" if the plural
1070
          # and singular form are the same.
1071
          def collection_name
1072
            singular == plural ? "#{plural}_index" : plural
1073 1074
          end

1075
          def resource_scope
1076
            { :controller => controller }
1077 1078
          end

1079
          alias :collection_scope :path
1080 1081

          def member_scope
1082
            "#{path}/:#{param}"
1083 1084
          end

1085 1086
          alias :shallow_scope :member_scope

1087
          def new_scope(new_path)
1088
            "#{path}/#{new_path}"
1089 1090
          end

1091 1092 1093 1094
          def nested_param
            :"#{singular}_#{param}"
          end

1095
          def nested_scope
1096
            "#{path}/:#{nested_param}"
1097
          end
1098

1099 1100 1101 1102 1103 1104 1105
          def shallow=(value)
            @shallow = value
          end

          def shallow?
            @shallow
          end
1106 1107 1108
        end

        class SingletonResource < Resource #:nodoc:
1109
          def initialize(entities, options)
1110
            super
1111
            @as         = nil
1112 1113
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1114 1115
          end

1116 1117 1118 1119
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

1120 1121
          def plural
            @plural ||= name.to_s.pluralize
1122 1123
          end

1124 1125
          def singular
            @singular ||= name.to_s
1126
          end
1127 1128 1129 1130 1131 1132

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1133 1134
        end

1135 1136 1137 1138
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

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

1163
          if apply_common_behavior_for(:resource, resources, options, &block)
1164 1165 1166
            return self
          end

1167
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1168
            yield if block_given?
1169

1170 1171
            concerns(options[:concerns]) if options[:concerns]

1172
            collection do
1173
              post :create
1174
            end if parent_resource.actions.include?(:create)
1175

1176
            new do
1177
              get :new
1178
            end if parent_resource.actions.include?(:new)
1179

1180
            set_member_mappings_for_resource
1181 1182
          end

J
Joshua Peek 已提交
1183
          self
1184 1185
        end

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

1321
          if apply_common_behavior_for(:resources, resources, options, &block)
1322 1323 1324
            return self
          end

1325
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1326
            yield if block_given?
J
Joshua Peek 已提交
1327

1328 1329
            concerns(options[:concerns]) if options[:concerns]

1330
            collection do
1331 1332
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1333
            end
1334

1335
            new do
1336
              get :new
1337
            end if parent_resource.actions.include?(:new)
1338

1339
            set_member_mappings_for_resource
1340 1341
          end

J
Joshua Peek 已提交
1342
          self
1343 1344
        end

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

1362 1363 1364 1365
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1366
          end
1367
        end
J
Joshua Peek 已提交
1368

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

1385
          with_scope_level(:member) do
1386 1387 1388 1389
            if shallow?
              shallow_scope(parent_resource.member_scope) { yield }
            else
              scope(parent_resource.member_scope) { yield }
1390
            end
1391 1392 1393 1394 1395 1396 1397
          end
        end

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

1399 1400 1401 1402
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1403
          end
J
Joshua Peek 已提交
1404 1405
        end

1406
        def nested
1407 1408
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1409 1410 1411
          end

          with_scope_level(:nested) do
1412
            if shallow? && shallow_nesting_depth > 1
1413
              shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1414
            else
1415
              scope(parent_resource.nested_scope, nested_options) { yield }
1416 1417 1418 1419
            end
          end
        end

1420
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1421
        def namespace(path, options = {})
1422
          if resource_scope?
1423 1424 1425 1426 1427 1428
            nested { super }
          else
            super
          end
        end

1429
        def shallow
1430
          scope(:shallow => true) do
1431 1432 1433 1434
            yield
          end
        end

1435 1436 1437 1438
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

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

1454 1455
          options[:anchor] = true unless options.key?(:anchor)

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

1460 1461 1462 1463
          if @scope[:controller] && @scope[:action]
            options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
          end

1464 1465 1466
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
1467 1468 1469 1470

            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')
1471
              route_options[:to].tr!("-", "_")
1472 1473
            end

1474 1475
            decomposed_match(_path, route_options)
          end
1476 1477
          self
        end
1478

1479 1480 1481 1482
        def using_match_shorthand?(path, options)
          path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
        end

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

1498
        def add_route(action, options) # :nodoc:
1499
          path = path_for_action(action, options.delete(:path))
1500
          action = action.to_s.dup
1501

1502
          if action =~ /^[\w\-\/]+$/
1503
            options[:action] ||= action.tr('-', '_') unless action.include?("/")
1504
          else
1505 1506 1507
            action = nil
          end

1508
          if !options.fetch(:as, true)
1509 1510 1511
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1512
          end
J
Joshua Peek 已提交
1513

1514
          mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1515 1516
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1517 1518
        end

1519 1520 1521 1522 1523 1524 1525 1526 1527
        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

1528
          if @scope[:scope_level] == :resources
1529 1530
            with_scope_level(:root) do
              scope(parent_resource.path) do
1531 1532 1533 1534 1535 1536
                super(options)
              end
            end
          else
            super(options)
          end
1537 1538
        end

1539
        protected
1540

1541
          def parent_resource #:nodoc:
1542 1543 1544
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1545
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1546 1547 1548 1549 1550
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1551 1552 1553 1554 1555 1556 1557
            if options.delete(:shallow)
              shallow do
                send(method, resources.pop, options, &block)
              end
              return true
            end

1558 1559 1560 1561 1562
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1563
            options.keys.each do |k|
1564 1565 1566
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1567 1568 1569
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1570 1571 1572 1573 1574
                send(method, resources.pop, options, &block)
              end
              return true
            end

1575 1576 1577 1578
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1579 1580 1581
            false
          end

J
José Valim 已提交
1582
          def action_options?(options) #:nodoc:
1583 1584 1585
            options[:only] || options[:except]
          end

J
José Valim 已提交
1586
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1587
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1588 1589
          end

J
José Valim 已提交
1590
          def scope_action_options #:nodoc:
1591 1592 1593
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1594
          def resource_scope? #:nodoc:
1595
            RESOURCE_SCOPES.include? @scope[:scope_level]
1596 1597
          end

J
José Valim 已提交
1598
          def resource_method_scope? #:nodoc:
1599
            RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1600 1601
          end

1602 1603 1604 1605
          def nested_scope? #:nodoc:
            @scope[:scope_level] == :nested
          end

1606
          def with_exclusive_scope
1607
            begin
1608 1609
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1610

1611 1612 1613
              with_scope_level(:exclusive) do
                yield
              end
1614
            ensure
1615
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1616 1617 1618
            end
          end

1619
          def with_scope_level(kind)
J
Joshua Peek 已提交
1620 1621 1622 1623 1624
            old, @scope[:scope_level] = @scope[:scope_level], kind
            yield
          ensure
            @scope[:scope_level] = old
          end
1625

1626
          def resource_scope(kind, resource) #:nodoc:
1627
            resource.shallow = @scope[:shallow]
1628
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1629
            @nesting.push(resource)
1630 1631 1632

            with_scope_level(kind) do
              scope(parent_resource.resource_scope) { yield }
1633
            end
1634
          ensure
1635
            @nesting.pop
1636
            @scope[:scope_level_resource] = old_resource
1637 1638
          end

J
José Valim 已提交
1639
          def nested_options #:nodoc:
1640 1641
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1642 1643
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1644 1645

            options
1646 1647
          end

1648 1649 1650 1651
          def nesting_depth #:nodoc:
            @nesting.size
          end

1652 1653 1654 1655
          def shallow_nesting_depth #:nodoc:
            @nesting.select(&:shallow?).size
          end

1656 1657
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1658 1659
          end

1660 1661
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1662 1663
          end

J
José Valim 已提交
1664
          def canonical_action?(action, flag) #:nodoc:
1665
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1666 1667
          end

1668 1669 1670 1671 1672 1673 1674
          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
1675 1676
          end

J
José Valim 已提交
1677
          def path_for_action(action, path) #:nodoc:
1678
            if canonical_action?(action, path.blank?)
1679
              @scope[:path].to_s
1680
            else
1681
              "#{@scope[:path]}/#{action_path(action, path)}"
1682 1683 1684
            end
          end

J
José Valim 已提交
1685
          def action_path(name, path = nil) #:nodoc:
1686
            name = name.to_sym if name.is_a?(String)
1687
            path || @scope[:path_names][name] || name.to_s
1688 1689
          end

J
José Valim 已提交
1690
          def prefix_name_for_action(as, action) #:nodoc:
1691
            if as
1692
              prefix = as
1693
            elsif !canonical_action?(action, @scope[:scope_level])
1694
              prefix = action
1695
            end
1696
            prefix.to_s.tr('-', '_') if prefix
1697 1698
          end

J
José Valim 已提交
1699
          def name_for_action(as, action) #:nodoc:
1700
            prefix = prefix_name_for_action(as, action)
1701
            prefix = Mapper.normalize_name(prefix) if prefix
1702 1703 1704
            name_prefix = @scope[:as]

            if parent_resource
1705
              return nil unless as || action
1706

1707 1708
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1709
            end
1710

1711
            name = case @scope[:scope_level]
1712
            when :nested
1713
              [name_prefix, prefix]
1714
            when :collection
1715
              [prefix, name_prefix, collection_name]
1716
            when :new
1717 1718
              [prefix, :new, name_prefix, member_name]
            when :member
1719
              [prefix, name_prefix, member_name]
1720 1721
            when :root
              [name_prefix, collection_name, prefix]
1722
            else
1723
              [name_prefix, member_name, prefix]
1724
            end
1725

1726 1727 1728 1729 1730 1731 1732 1733 1734 1735
            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
1736
          end
1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748

          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 已提交
1749
      end
J
Joshua Peek 已提交
1750

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

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

1856 1857 1858
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1859
        @concerns = {}
1860
        @nesting = []
1861 1862
      end

1863 1864
      include Base
      include HttpHelpers
1865
      include Redirection
1866
      include Scoping
1867
      include Concerns
1868
      include Resources
J
Joshua Peek 已提交
1869 1870
    end
  end
J
Joshua Peek 已提交
1871
end