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

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

17
      class Constraints #:nodoc:
18
        def self.new(app, constraints, request = Rack::Request)
19
          if constraints.any?
20
            super(app, constraints, request)
21 22 23 24 25
          else
            app
          end
        end

26
        attr_reader :app, :constraints
27

28 29
        def initialize(app, constraints, request)
          @app, @constraints, @request = app, constraints, request
30 31
        end

32
        def matches?(env)
33
          req = @request.new(env)
34

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

        def call(env)
          matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
45
        end
46 47 48 49 50

        private
          def constraint_args(constraint, request)
            constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
          end
51 52
      end

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

58
        attr_reader :scope, :path, :options, :requirements, :conditions, :defaults
59

60 61 62
        def initialize(set, scope, path, options)
          @set, @scope, @path, @options = set, scope, path, options
          @requirements, @conditions, @defaults = {}, {}, {}
63

64
          normalize_options!
65
          normalize_path!
66 67
          normalize_requirements!
          normalize_conditions!
68
          normalize_defaults!
69
        end
J
Joshua Peek 已提交
70

71
        def to_route
72
          [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
73
        end
J
Joshua Peek 已提交
74

75
        private
76

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
          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

96
          def normalize_options!
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
            @options.reverse_merge!(scope[:options]) if scope[:options]
            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
            if path_without_format.match(WILDCARD_PATH) && @options[:format] != false
              @options[$1.to_sym] ||= /.+?/
            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' }
              @options[:controller] ||= /.+?/
            end
115

116
            @options.merge!(default_controller_and_action)
117 118 119 120 121
          end

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

126
            if options[:format] == true
127
              @requirements[:format] ||= /.+/
128 129 130 131 132
            elsif Regexp === options[:format]
              @requirements[:format] = options[:format]
            elsif String === options[:format]
              @requirements[:format] = Regexp.compile(options[:format])
            end
133
          end
134

Y
Yves Senn 已提交
135 136 137 138 139 140 141 142 143 144
          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

145 146 147
          def normalize_defaults!
            @defaults.merge!(scope[:defaults]) if scope[:defaults]
            @defaults.merge!(options[:defaults]) if options[:defaults]
148

149 150 151
            options.each do |key, default|
              next if Regexp === default || IGNORE_OPTIONS.include?(key)
              @defaults[key] = default
152 153
            end

154 155 156 157 158
            if options[:constraints].is_a?(Hash)
              options[:constraints].each do |key, default|
                next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
                @defaults[key] ||= default
              end
159 160
            end

161 162 163 164
            if Regexp === options[:format]
              @defaults[:format] = nil
            elsif String === options[:format]
              @defaults[:format] = options[:format]
165
            end
166
          end
167

168 169
          def normalize_conditions!
            @conditions.merge!(:path_info => path)
170

171 172 173 174
            constraints.each do |key, condition|
              next if segment_keys.include?(key) || key == :controller
              @conditions[key] = condition
            end
J
Joshua Peek 已提交
175

176 177 178 179 180 181 182
            @conditions[:required_defaults] = []
            options.each do |key, required_default|
              next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key)
              next if Regexp === required_default
              @conditions[:required_defaults] << key
            end

183 184 185 186
            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" \
187 188
                    "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" \
189 190 191
                    "  Instead of: match \"controller#action\"\n" \
                    "  Do: get \"controller#action\""
              raise msg
192
            end
193

194 195 196
            if via = options[:via]
              list = Array(via).map { |m| m.to_s.dasherize.upcase }
              @conditions.merge!(:request_method => list)
197 198 199
            end
          end

200 201 202 203
          def app
            Constraints.new(endpoint, blocks, @set.request_class)
          end

204
          def default_controller_and_action
205
            if to.respond_to?(:call)
206 207
              { }
            else
208
              if to.is_a?(String)
209
                controller, action = to.split('#')
210 211
              elsif to.is_a?(Symbol)
                action = to.to_s
212
              end
J
Joshua Peek 已提交
213

214 215
              controller ||= default_controller
              action     ||= default_action
216

217
              unless controller.is_a?(Regexp)
218 219
                controller = [@scope[:module], controller].compact.join("/").presence
              end
220

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

225 226
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
227

228
              if controller.blank? && segment_keys.exclude?(:controller)
229 230
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
231

232
              if action.blank? && segment_keys.exclude?(:action)
233 234
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
235

236
              if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
237 238 239 240 241
                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 已提交
242
              hash = {}
A
Aaron Patterson 已提交
243 244
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
245
              hash
246 247
            end
          end
248

249
          def blocks
250 251
            if options[:constraints].present? && !options[:constraints].is_a?(Hash)
              [options[:constraints]]
252
            else
253
              scope[:blocks] || []
254 255
            end
          end
J
Joshua Peek 已提交
256

257
          def constraints
258 259
            @constraints ||= {}.tap do |constraints|
              constraints.merge!(scope[:constraints]) if scope[:constraints]
260

261 262 263 264 265
              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)
266
            end
267
          end
J
Joshua Peek 已提交
268

269
          def segment_keys
270 271 272 273 274 275
            @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
          end

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

277 278 279 280 281 282 283 284 285 286
          def strexp
            Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
          end

          def endpoint
            to.respond_to?(:call) ? to : dispatcher
          end

          def dispatcher
            Routing::RouteSet::Dispatcher.new(:defaults => defaults)
287
          end
288

289
          def to
290
            options[:to]
291
          end
J
Joshua Peek 已提交
292

293
          def default_controller
294
            options[:controller] || scope[:controller]
295
          end
296 297

          def default_action
298
            options[:action] || scope[:action]
299
          end
300
      end
301

302
      # Invokes Journey::Router::Utils.normalize_path and ensure that
303 304
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
305
      def self.normalize_path(path)
306
        path = Journey::Router::Utils.normalize_path(path)
307
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
308 309 310
        path
      end

311
      def self.normalize_name(name)
312
        normalize_path(name)[1..-1].tr("/", "_")
313 314
      end

315
      module Base
316 317
        # You can specify what Rails should route "/" to with the root method:
        #
A
AvnerCohen 已提交
318
        #   root to: 'pages#main'
319
        #
320
        # For options, see +match+, as +root+ uses it internally.
321
        #
322 323 324 325
        # You can also pass a string which will expand
        #
        #   root 'pages#main'
        #
326 327 328
        # 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.
329
        def root(options = {})
330
          match '/', { :as => :root, :via => :get }.merge!(options)
331
        end
332

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

465 466
        # Mount a Rack-based application to be used within the application.
        #
A
AvnerCohen 已提交
467
        #   mount SomeRackApp, at: "some_route"
468 469 470
        #
        # Alternatively:
        #
R
Ryan Bigg 已提交
471
        #   mount(SomeRackApp => "some_route")
472
        #
473 474
        # For options, see +match+, as +mount+ uses it internally.
        #
475 476 477 478 479
        # 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 已提交
480
        #   mount(SomeRackApp => "some_route", as: "exciting")
481 482 483
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
484 485 486 487
        def mount(app, options = nil)
          if options
            path = options.delete(:at)
          else
488 489 490 491
            unless Hash === app
              raise ArgumentError, "must be called with mount point"
            end

492
            options = app
493
            app, path = options.find { |k, _| k.respond_to?(:call) }
494 495 496 497 498
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

P
Pratik Naik 已提交
499 500
          options[:as]  ||= app_name(app)
          options[:via] ||= :all
501

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

          define_generate_prefix(app, options[:as])
505 506 507
          self
        end

508 509 510 511
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
512

513 514 515 516 517 518
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

519 520 521 522 523
        # Query if the following named route was already defined.
        def has_named_route?(name)
          @set.named_routes.routes[name.to_sym]
        end

524 525 526
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
527 528 529 530 531

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
532
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
533
            end
534 535 536
          end

          def define_generate_prefix(app, name)
537
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
538 539

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
540 541
            _routes = @set
            app.routes.define_mounted_helper(name)
J
José Valim 已提交
542 543 544 545 546
            app.routes.singleton_class.class_eval do
              define_method :mounted? do
                true
              end

547
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
548
                prefix_options = options.slice(*_route.segment_keys)
549 550
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
551
                _routes.url_helpers.send("#{name}_path", prefix_options)
552 553 554
              end
            end
          end
555 556 557
      end

      module HttpHelpers
558
        # Define a route that only recognizes HTTP GET.
C
Cesar Carruitero 已提交
559
        # For supported arguments, see match[rdoc-ref:Base#match]
560
        #
A
AvnerCohen 已提交
561
        #   get 'bacon', to: 'food#bacon'
562
        def get(*args, &block)
563
          map_method(:get, args, &block)
564 565
        end

566
        # Define a route that only recognizes HTTP POST.
C
Cesar Carruitero 已提交
567
        # For supported arguments, see match[rdoc-ref:Base#match]
568
        #
A
AvnerCohen 已提交
569
        #   post 'bacon', to: 'food#bacon'
570
        def post(*args, &block)
571
          map_method(:post, args, &block)
572 573
        end

574
        # Define a route that only recognizes HTTP PATCH.
C
Cesar Carruitero 已提交
575
        # For supported arguments, see match[rdoc-ref:Base#match]
576
        #
A
AvnerCohen 已提交
577
        #   patch 'bacon', to: 'food#bacon'
578 579 580 581
        def patch(*args, &block)
          map_method(:patch, args, &block)
        end

582
        # Define a route that only recognizes HTTP PUT.
C
Cesar Carruitero 已提交
583
        # For supported arguments, see match[rdoc-ref:Base#match]
584
        #
A
AvnerCohen 已提交
585
        #   put 'bacon', to: 'food#bacon'
586
        def put(*args, &block)
587
          map_method(:put, args, &block)
588 589
        end

590
        # Define a route that only recognizes HTTP DELETE.
C
Cesar Carruitero 已提交
591
        # For supported arguments, see match[rdoc-ref:Base#match]
592
        #
A
AvnerCohen 已提交
593
        #   delete 'broccoli', to: 'food#broccoli'
594
        def delete(*args, &block)
595
          map_method(:delete, args, &block)
596 597 598
        end

        private
599
          def map_method(method, args, &block)
600
            options = args.extract_options!
601
            options[:via] = method
602
            match(*args, options, &block)
603 604 605 606
            self
          end
      end

607 608 609
      # 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 已提交
610 611
      # the <tt>app/controllers/admin</tt> directory, and you can group them
      # together in your router:
612 613 614 615
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
616
      #
617
      # This will create a number of routes for each of the posts and comments
S
Sebastian Martinez 已提交
618
      # controller. For <tt>Admin::PostsController</tt>, Rails will create:
619
      #
620 621 622 623 624
      #   GET       /admin/posts
      #   GET       /admin/posts/new
      #   POST      /admin/posts
      #   GET       /admin/posts/1
      #   GET       /admin/posts/1/edit
625
      #   PATCH/PUT /admin/posts/1
626
      #   DELETE    /admin/posts/1
627
      #
628
      # If you want to route /posts (without the prefix /admin) to
S
Sebastian Martinez 已提交
629
      # <tt>Admin::PostsController</tt>, you could use
630
      #
A
AvnerCohen 已提交
631
      #   scope module: "admin" do
632
      #     resources :posts
633 634 635
      #   end
      #
      # or, for a single case
636
      #
A
AvnerCohen 已提交
637
      #   resources :posts, module: "admin"
638
      #
S
Sebastian Martinez 已提交
639
      # If you want to route /admin/posts to +PostsController+
640
      # (without the Admin:: module prefix), you could use
641
      #
642
      #   scope "/admin" do
643
      #     resources :posts
644 645 646
      #   end
      #
      # or, for a single case
647
      #
A
AvnerCohen 已提交
648
      #   resources :posts, path: "/admin/posts"
649 650 651
      #
      # 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 已提交
652
      # +PostsController+:
653
      #
654 655 656 657 658
      #   GET       /admin/posts
      #   GET       /admin/posts/new
      #   POST      /admin/posts
      #   GET       /admin/posts/1
      #   GET       /admin/posts/1/edit
659
      #   PATCH/PUT /admin/posts/1
660
      #   DELETE    /admin/posts/1
661
      module Scoping
662
        # Scopes a set of routes to the given default options.
663 664 665
        #
        # Take the following route definition as an example:
        #
A
AvnerCohen 已提交
666
        #   scope path: ":account_id", as: "account" do
667 668 669 670
        #     resources :projects
        #   end
        #
        # This generates helpers such as +account_projects_path+, just like +resources+ does.
671 672
        # The difference here being that the routes generated are like /:account_id/projects,
        # rather than /accounts/:account_id/projects.
673
        #
674
        # === Options
675
        #
676
        # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
677
        #
S
Sebastian Martinez 已提交
678
        #   # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
679
        #   scope module: "admin" do
680 681
        #     resources :posts
        #   end
682
        #
683
        #   # prefix the posts resource's requests with '/admin'
A
AvnerCohen 已提交
684
        #   scope path: "/admin" do
685 686
        #     resources :posts
        #   end
687
        #
S
Sebastian Martinez 已提交
688
        #   # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
A
AvnerCohen 已提交
689
        #   scope as: "sekret" do
690 691
        #     resources :posts
        #   end
692
        def scope(*args)
693
          options = args.extract_options!.dup
694
          recover = {}
695

696
          options[:path] = args.flatten.join('/') if args.any?
697
          options[:constraints] ||= {}
698

699
          if options[:constraints].is_a?(Hash)
700 701 702 703 704
            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)
705 706
          else
            block, options[:constraints] = options[:constraints], {}
707 708
          end

709 710 711 712 713 714 715 716 717 718
          SCOPE_OPTIONS.each do |option|
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
              value = options.delete(option)
            end

            if value
719 720 721
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
722 723 724 725 726
          end

          yield
          self
        ensure
727
          @scope.merge!(recover)
728 729
        end

730 731 732
        # Scopes routes to a specific controller
        #
        #   controller "food" do
A
AvnerCohen 已提交
733
        #     match "bacon", action: "bacon"
734
        #   end
735 736 737
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
738 739
        end

740 741 742 743 744 745 746 747
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
748 749 750 751 752
        #       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
753
        #        admin_post PATCH/PUT /admin/posts/:id(.:format)      admin/posts#update
754
        #        admin_post DELETE    /admin/posts/:id(.:format)      admin/posts#destroy
755
        #
756
        # === Options
757
        #
758 759
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
        # options all default to the name of the namespace.
760
        #
761 762
        # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
        # <tt>Resources#resources</tt>.
763
        #
764
        #   # accessible through /sekret/posts rather than /admin/posts
A
AvnerCohen 已提交
765
        #   namespace :admin, path: "sekret" do
766 767
        #     resources :posts
        #   end
768
        #
S
Sebastian Martinez 已提交
769
        #   # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
770
        #   namespace :admin, module: "sekret" do
771 772
        #     resources :posts
        #   end
773
        #
S
Sebastian Martinez 已提交
774
        #   # generates +sekret_posts_path+ rather than +admin_posts_path+
A
AvnerCohen 已提交
775
        #   namespace :admin, as: "sekret" do
776 777
        #     resources :posts
        #   end
778
        def namespace(path, options = {})
779
          path = path.to_s
780 781 782
          options = { :path => path, :as => path, :module => path,
                      :shallow_path => path, :shallow_prefix => path }.merge!(options)
          scope(options) { yield }
783
        end
784

R
Ryan Bigg 已提交
785 786 787 788
        # === 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 已提交
789
        #   constraints(id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
790 791 792 793 794
        #     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.
795
        #
R
R.T. Lechow 已提交
796
        # You may use this to also restrict other parameters:
R
Ryan Bigg 已提交
797 798
        #
        #   resources :posts do
A
AvnerCohen 已提交
799
        #     constraints(post_id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
800 801
        #       resources :comments
        #     end
J
James Miller 已提交
802
        #   end
R
Ryan Bigg 已提交
803 804 805 806 807
        #
        # === Restricting based on IP
        #
        # Routes can also be constrained to an IP or a certain range of IP addresses:
        #
A
AvnerCohen 已提交
808
        #   constraints(ip: /192\.168\.\d+\.\d+/) do
R
Ryan Bigg 已提交
809 810 811 812 813 814 815 816
        #     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 已提交
817
        # Requests to routes can be constrained based on specific criteria:
R
Ryan Bigg 已提交
818 819 820 821 822 823 824 825 826 827
        #
        #    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
828
        #      def self.matches?(request)
R
Ryan Bigg 已提交
829 830 831 832 833 834 835 836 837 838 839
        #        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
840 841 842 843
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
844
        # Allows you to set default parameters for a route, such as this:
A
AvnerCohen 已提交
845 846
        #   defaults id: 'home' do
        #     match 'scoped_pages/(:id)', to: 'pages#show'
847
        #   end
R
Ryan Bigg 已提交
848
        # Using this, the +:id+ parameter here will default to 'home'.
849 850 851 852
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

853
        private
J
José Valim 已提交
854
          def merge_path_scope(parent, child) #:nodoc:
855
            Mapper.normalize_path("#{parent}/#{child}")
856 857
          end

J
José Valim 已提交
858
          def merge_shallow_path_scope(parent, child) #:nodoc:
859 860 861
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
862
          def merge_as_scope(parent, child) #:nodoc:
863
            parent ? "#{parent}_#{child}" : child
864 865
          end

J
José Valim 已提交
866
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
867 868 869
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
870
          def merge_module_scope(parent, child) #:nodoc:
871 872 873
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
874
          def merge_controller_scope(parent, child) #:nodoc:
875
            child
876 877
          end

878 879 880 881
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

J
José Valim 已提交
882
          def merge_path_names_scope(parent, child) #:nodoc:
883 884 885
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
886
          def merge_constraints_scope(parent, child) #:nodoc:
887 888 889
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
890
          def merge_defaults_scope(parent, child) #:nodoc:
891 892 893
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
894
          def merge_blocks_scope(parent, child) #:nodoc:
895 896 897
            merged = parent ? parent.dup : []
            merged << child if child
            merged
898 899
          end

J
José Valim 已提交
900
          def merge_options_scope(parent, child) #:nodoc:
901
            (parent || {}).except(*override_keys(child)).merge!(child)
902
          end
903

J
José Valim 已提交
904
          def merge_shallow_scope(parent, child) #:nodoc:
905 906
            child ? true : false
          end
907

J
José Valim 已提交
908
          def override_keys(child) #:nodoc:
909 910
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
911 912
      end

913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936
      # 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 已提交
937 938
      # <tt>app/controllers/admin</tt> directory, and you can group them together
      # in your router:
939 940 941 942 943
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
S
Sebastian Martinez 已提交
944 945
      # 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
946 947
      # overrides this restriction, e.g:
      #
A
AvnerCohen 已提交
948
      #   resources :articles, id: /[^\/]+/
949
      #
S
Sebastian Martinez 已提交
950
      # This allows any character other than a slash as part of your +:id+.
951
      #
J
Joshua Peek 已提交
952
      module Resources
953 954
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
955
        VALID_ON_OPTIONS  = [:new, :collection, :member]
956
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except, :param, :concerns]
957
        CANONICAL_ACTIONS = %w(index create new show update destroy)
958 959
        RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
        RESOURCE_SCOPES = [:resource, :resources]
960

961
        class Resource #:nodoc:
962
          attr_reader :controller, :path, :options, :param
963 964

          def initialize(entities, options = {})
965
            @name       = entities.to_s
966 967 968
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
969
            @param      = (options[:param] || :id).to_sym
970
            @options    = options
971 972
          end

973
          def default_actions
974
            [:index, :create, :new, :show, :update, :destroy, :edit]
975 976
          end

977
          def actions
978
            if only = @options[:only]
979
              Array(only).map(&:to_sym)
980
            elsif except = @options[:except]
981 982 983 984 985 986
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

987
          def name
988
            @as || @name
989 990
          end

991
          def plural
992
            @plural ||= name.to_s
993 994 995
          end

          def singular
996
            @singular ||= name.to_s.singularize
997 998
          end

999
          alias :member_name :singular
1000

1001
          # Checks for uncountable plurals, and appends "_index" if the plural
1002
          # and singular form are the same.
1003
          def collection_name
1004
            singular == plural ? "#{plural}_index" : plural
1005 1006
          end

1007
          def resource_scope
1008
            { :controller => controller }
1009 1010
          end

1011
          alias :collection_scope :path
1012 1013

          def member_scope
1014
            "#{path}/:#{param}"
1015 1016
          end

1017 1018
          alias :shallow_scope :member_scope

1019
          def new_scope(new_path)
1020
            "#{path}/#{new_path}"
1021 1022
          end

1023 1024 1025 1026
          def nested_param
            :"#{singular}_#{param}"
          end

1027
          def nested_scope
1028
            "#{path}/:#{nested_param}"
1029
          end
1030

1031 1032 1033
        end

        class SingletonResource < Resource #:nodoc:
1034
          def initialize(entities, options)
1035
            super
1036
            @as         = nil
1037 1038
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1039 1040
          end

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

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

1049 1050
          def singular
            @singular ||= name.to_s
1051
          end
1052 1053 1054 1055 1056 1057

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1058 1059
        end

1060 1061 1062 1063
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

1064 1065 1066 1067 1068 1069
        # 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:
        #
1070
        #   resource :profile
1071 1072
        #
        # creates six different routes in your application, all mapping to
1073
        # the +Profiles+ controller (note that the controller is named after
1074 1075
        # the plural):
        #
1076 1077 1078 1079 1080 1081
        #   GET       /profile/new
        #   POST      /profile
        #   GET       /profile
        #   GET       /profile/edit
        #   PATCH/PUT /profile
        #   DELETE    /profile
1082
        #
1083
        # === Options
1084
        # Takes same options as +resources+.
J
Joshua Peek 已提交
1085
        def resource(*resources, &block)
1086
          options = resources.extract_options!.dup
J
Joshua Peek 已提交
1087

1088
          if apply_common_behavior_for(:resource, resources, options, &block)
1089 1090 1091
            return self
          end

1092
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1093
            yield if block_given?
1094

1095 1096
            concerns(options[:concerns]) if options[:concerns]

1097
            collection do
1098
              post :create
1099
            end if parent_resource.actions.include?(:create)
1100

1101
            new do
1102
              get :new
1103
            end if parent_resource.actions.include?(:new)
1104

1105
            set_member_mappings_for_resource
1106 1107
          end

J
Joshua Peek 已提交
1108
          self
1109 1110
        end

1111 1112 1113 1114 1115 1116 1117 1118
        # 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 已提交
1119
        # the +Photos+ controller:
1120
        #
1121 1122 1123 1124 1125
        #   GET       /photos
        #   GET       /photos/new
        #   POST      /photos
        #   GET       /photos/:id
        #   GET       /photos/:id/edit
1126
        #   PATCH/PUT /photos/:id
1127
        #   DELETE    /photos/:id
1128
        #
1129 1130 1131 1132 1133 1134 1135 1136
        # Resources can also be nested infinitely by using this block syntax:
        #
        #   resources :photos do
        #     resources :comments
        #   end
        #
        # This generates the following comments routes:
        #
1137 1138 1139 1140 1141
        #   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
1142
        #   PATCH/PUT /photos/:photo_id/comments/:id
1143
        #   DELETE    /photos/:photo_id/comments/:id
1144
        #
1145
        # === Options
1146 1147
        # Takes same options as <tt>Base#match</tt> as well as:
        #
1148
        # [:path_names]
A
Aviv Ben-Yosef 已提交
1149 1150
        #   Allows you to change the segment component of the +edit+ and +new+ actions.
        #   Actions not specified are not changed.
1151
        #
A
AvnerCohen 已提交
1152
        #     resources :posts, path_names: { new: "brand_new" }
1153 1154
        #
        #   The above example will now change /posts/new to /posts/brand_new
1155
        #
1156 1157 1158
        # [:path]
        #   Allows you to change the path prefix for the resource.
        #
A
AvnerCohen 已提交
1159
        #     resources :posts, path: 'postings'
1160 1161 1162
        #
        #   The resource and all segments will now route to /postings instead of /posts
        #
1163 1164
        # [:only]
        #   Only generate routes for the given actions.
1165
        #
A
AvnerCohen 已提交
1166 1167
        #     resources :cows, only: :show
        #     resources :cows, only: [:show, :index]
1168
        #
1169 1170
        # [:except]
        #   Generate all routes except for the given actions.
1171
        #
A
AvnerCohen 已提交
1172 1173
        #     resources :cows, except: :show
        #     resources :cows, except: [:show, :index]
1174 1175 1176 1177 1178
        #
        # [:shallow]
        #   Generates shallow routes for nested resource(s). When placed on a parent resource,
        #   generates shallow routes for all nested resources.
        #
A
AvnerCohen 已提交
1179
        #     resources :posts, shallow: true do
1180 1181 1182 1183 1184 1185
        #       resources :comments
        #     end
        #
        #   Is the same as:
        #
        #     resources :posts do
A
AvnerCohen 已提交
1186
        #       resources :comments, except: [:show, :edit, :update, :destroy]
1187
        #     end
A
AvnerCohen 已提交
1188
        #     resources :comments, only: [:show, :edit, :update, :destroy]
1189 1190 1191 1192
        #
        #   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>.
1193 1194 1195 1196
        #
        # [:shallow_path]
        #   Prefixes nested shallow routes with the specified path.
        #
A
AvnerCohen 已提交
1197
        #     scope shallow_path: "sekret" do
1198
        #       resources :posts do
A
AvnerCohen 已提交
1199
        #         resources :comments, shallow: true
1200
        #       end
1201 1202 1203 1204
        #     end
        #
        #   The +comments+ resource here will have the following routes generated for it:
        #
1205 1206 1207 1208 1209
        #     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)
1210
        #     comment          PATCH/PUT /sekret/comments/:id(.:format)
1211
        #     comment          DELETE    /sekret/comments/:id(.:format)
1212
        #
1213 1214 1215
        # [:shallow_prefix]
        #   Prefixes nested shallow route names with specified prefix.
        #
A
AvnerCohen 已提交
1216
        #     scope shallow_prefix: "sekret" do
1217
        #       resources :posts do
A
AvnerCohen 已提交
1218
        #         resources :comments, shallow: true
1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231
        #       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)
        #
1232
        # [:format]
1233
        #   Allows you to specify the default value for optional +format+
V
Vijay Dev 已提交
1234
        #   segment or disable it by supplying +false+.
1235
        #
1236
        # === Examples
1237
        #
S
Sebastian Martinez 已提交
1238
        #   # routes call <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
1239
        #   resources :posts, module: "admin"
1240
        #
1241
        #   # resource actions are at /admin/posts.
A
AvnerCohen 已提交
1242
        #   resources :posts, path: "admin/posts"
J
Joshua Peek 已提交
1243
        def resources(*resources, &block)
1244
          options = resources.extract_options!.dup
1245

1246
          if apply_common_behavior_for(:resources, resources, options, &block)
1247 1248 1249
            return self
          end

1250
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1251
            yield if block_given?
J
Joshua Peek 已提交
1252

1253 1254
            concerns(options[:concerns]) if options[:concerns]

1255
            collection do
1256 1257
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1258
            end
1259

1260
            new do
1261
              get :new
1262
            end if parent_resource.actions.include?(:new)
1263

1264
            set_member_mappings_for_resource
1265 1266
          end

J
Joshua Peek 已提交
1267
          self
1268 1269
        end

1270 1271 1272 1273 1274 1275 1276 1277 1278
        # 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 已提交
1279
        # with GET, and route to the search action of +PhotosController+. It will also
1280 1281
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1282
        def collection
1283 1284
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1285 1286
          end

1287 1288 1289 1290
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1291
          end
1292
        end
J
Joshua Peek 已提交
1293

1294 1295 1296 1297 1298 1299 1300 1301 1302
        # 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 已提交
1303
        # preview action of +PhotosController+. It will also create the
1304
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1305
        def member
1306 1307
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1308
          end
J
Joshua Peek 已提交
1309

1310 1311 1312 1313
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1314 1315 1316 1317 1318 1319 1320
          end
        end

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

1322 1323 1324 1325
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1326
          end
J
Joshua Peek 已提交
1327 1328
        end

1329
        def nested
1330 1331
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1332 1333 1334
          end

          with_scope_level(:nested) do
1335
            if shallow?
1336
              with_exclusive_scope do
1337
                if @scope[:shallow_path].blank?
1338
                  scope(parent_resource.nested_scope, nested_options) { yield }
1339
                else
1340
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1341
                    scope(parent_resource.nested_scope, nested_options) { yield }
1342 1343 1344 1345
                  end
                end
              end
            else
1346
              scope(parent_resource.nested_scope, nested_options) { yield }
1347 1348 1349 1350
            end
          end
        end

1351
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1352
        def namespace(path, options = {})
1353
          if resource_scope?
1354 1355 1356 1357 1358 1359
            nested { super }
          else
            super
          end
        end

1360
        def shallow
1361
          scope(:shallow => true, :shallow_path => @scope[:path]) do
1362 1363 1364 1365
            yield
          end
        end

1366 1367 1368 1369
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

1370
        # match 'path' => 'controller#action'
R
Rafael Mendonça França 已提交
1371
        # match 'path', to: 'controller#action'
1372
        # match 'path', 'otherpath', on: :member, via: :get
1373 1374 1375
        def match(path, *rest)
          if rest.empty? && Hash === path
            options  = path
1376
            path, to = options.find { |name, _value| name.is_a?(String) }
1377 1378
            options[:to] = to
            options.delete(path)
1379 1380 1381 1382 1383 1384
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1385 1386
          options[:anchor] = true unless options.key?(:anchor)

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

1391 1392 1393 1394
          if @scope[:controller] && @scope[:action]
            options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
          end

1395 1396 1397
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
1398 1399 1400 1401 1402 1403

            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')
            end

1404 1405
            decomposed_match(_path, route_options)
          end
1406 1407
          self
        end
1408

1409 1410 1411 1412
        def using_match_shorthand?(path, options)
          path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
        end

1413
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1414 1415
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1416
          else
A
Aaron Patterson 已提交
1417 1418 1419 1420 1421 1422 1423 1424
            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 已提交
1425
          end
1426
        end
J
Joshua Peek 已提交
1427

1428
        def add_route(action, options) # :nodoc:
1429
          path = path_for_action(action, options.delete(:path))
1430
          action = action.to_s.dup
1431

1432 1433
          if action =~ /^[\w\/]+$/
            options[:action] ||= action unless action.include?("/")
1434
          else
1435 1436 1437
            action = nil
          end

1438
          if !options.fetch(:as, true)
1439 1440 1441
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1442
          end
J
Joshua Peek 已提交
1443

1444
          mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1445 1446
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1447 1448
        end

1449 1450 1451 1452 1453 1454 1455 1456 1457
        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

1458
          if @scope[:scope_level] == :resources
1459 1460
            with_scope_level(:root) do
              scope(parent_resource.path) do
1461 1462 1463 1464 1465 1466
                super(options)
              end
            end
          else
            super(options)
          end
1467 1468
        end

1469
        protected
1470

1471
          def parent_resource #:nodoc:
1472 1473 1474
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1475
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1476 1477 1478 1479 1480
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1481 1482 1483 1484 1485
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1486
            options.keys.each do |k|
1487 1488 1489
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1490 1491 1492
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1493 1494 1495 1496 1497
                send(method, resources.pop, options, &block)
              end
              return true
            end

1498 1499 1500 1501
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1502 1503 1504
            false
          end

J
José Valim 已提交
1505
          def action_options?(options) #:nodoc:
1506 1507 1508
            options[:only] || options[:except]
          end

J
José Valim 已提交
1509
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1510
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1511 1512
          end

J
José Valim 已提交
1513
          def scope_action_options #:nodoc:
1514 1515 1516
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1517
          def resource_scope? #:nodoc:
1518
            RESOURCE_SCOPES.include? @scope[:scope_level]
1519 1520
          end

J
José Valim 已提交
1521
          def resource_method_scope? #:nodoc:
1522
            RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1523 1524
          end

1525
          def with_exclusive_scope
1526
            begin
1527 1528
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1529

1530 1531 1532
              with_scope_level(:exclusive) do
                yield
              end
1533
            ensure
1534
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1535 1536 1537
            end
          end

1538
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1539
            old, @scope[:scope_level] = @scope[:scope_level], kind
1540
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1541 1542 1543
            yield
          ensure
            @scope[:scope_level] = old
1544
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1545
          end
1546

1547 1548
          def resource_scope(kind, resource) #:nodoc:
            with_scope_level(kind, resource) do
1549
              scope(parent_resource.resource_scope) do
1550 1551 1552 1553 1554
                yield
              end
            end
          end

J
José Valim 已提交
1555
          def nested_options #:nodoc:
1556 1557
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1558 1559
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1560 1561

            options
1562 1563
          end

1564 1565
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1566 1567
          end

1568 1569
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1570 1571
          end

J
José Valim 已提交
1572
          def canonical_action?(action, flag) #:nodoc:
1573
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1574 1575
          end

J
José Valim 已提交
1576
          def shallow_scoping? #:nodoc:
1577
            shallow? && @scope[:scope_level] == :member
1578 1579
          end

J
José Valim 已提交
1580
          def path_for_action(action, path) #:nodoc:
1581
            prefix = shallow_scoping? ?
1582
              "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path]
1583

1584
            if canonical_action?(action, path.blank?)
1585
              prefix.to_s
1586
            else
1587
              "#{prefix}/#{action_path(action, path)}"
1588 1589 1590
            end
          end

J
José Valim 已提交
1591
          def action_path(name, path = nil) #:nodoc:
1592
            name = name.to_sym if name.is_a?(String)
1593
            path || @scope[:path_names][name] || name.to_s
1594 1595
          end

J
José Valim 已提交
1596
          def prefix_name_for_action(as, action) #:nodoc:
1597
            if as
1598
              as.to_s
1599
            elsif !canonical_action?(action, @scope[:scope_level])
1600
              action.to_s
1601
            end
1602 1603
          end

J
José Valim 已提交
1604
          def name_for_action(as, action) #:nodoc:
1605
            prefix = prefix_name_for_action(as, action)
1606
            prefix = Mapper.normalize_name(prefix) if prefix
1607 1608 1609
            name_prefix = @scope[:as]

            if parent_resource
1610
              return nil unless as || action
1611

1612 1613
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1614
            end
1615

1616
            name = case @scope[:scope_level]
1617
            when :nested
1618
              [name_prefix, prefix]
1619
            when :collection
1620
              [prefix, name_prefix, collection_name]
1621
            when :new
1622 1623 1624 1625 1626
              [prefix, :new, name_prefix, member_name]
            when :member
              [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
            when :root
              [name_prefix, collection_name, prefix]
1627
            else
1628
              [name_prefix, member_name, prefix]
1629
            end
1630

1631 1632 1633 1634 1635 1636 1637 1638 1639 1640
            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
1641
          end
1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653

          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 已提交
1654
      end
J
Joshua Peek 已提交
1655

1656
      # Routing Concerns allow you to declare common routes that can be reused
1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675
      # 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
1676
      module Concerns
1677
        # Define a routing concern using a name.
1678
        #
1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700
        # 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]
1701 1702
        #   end
        #
1703 1704 1705
        # Or, using a callable object, you might implement something more
        # specific to your application, which would be out of place in your
        # routes file.
1706
        #
1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717
        #   # 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]
1718 1719 1720
        #     end
        #   end
        #
1721 1722 1723 1724 1725 1726 1727 1728
        #   # routes.rb
        #   concern :purchasable, Purchasable.new(returnable: true)
        #
        #   resources :toys, concerns: :purchasable
        #   resources :electronics, concerns: :purchasable
        #   resources :pets do
        #     concerns :purchasable, returnable: false
        #   end
1729
        #
1730 1731 1732
        # 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>.
1733
        def concern(name, callable = nil, &block)
1734 1735
          callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
          @concerns[name] = callable
1736 1737
        end

1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748
        # 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
1749 1750 1751
        def concerns(*args)
          options = args.extract_options!
          args.flatten.each do |name|
1752
            if concern = @concerns[name]
1753
              concern.call(self, options)
1754 1755 1756 1757 1758 1759 1760
            else
              raise ArgumentError, "No concern named #{name} was found!"
            end
          end
        end
      end

1761 1762 1763
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1764
        @concerns = {}
1765 1766
      end

1767 1768
      include Base
      include HttpHelpers
1769
      include Redirection
1770
      include Scoping
1771
      include Concerns
1772
      include Resources
J
Joshua Peek 已提交
1773 1774
    end
  end
J
Joshua Peek 已提交
1775
end