mapper.rb 59.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/inflector'
7
require 'action_dispatch/routing/redirection'
8

J
Joshua Peek 已提交
9 10
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
11
    class Mapper
12 13
      URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]

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

23
        attr_reader :app, :constraints
24

25 26
        def initialize(app, constraints, request)
          @app, @constraints, @request = app, constraints, request
27 28
        end

29
        def matches?(env)
30
          req = @request.new(env)
31

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

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

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

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

55
        attr_reader :scope, :path, :options, :requirements, :conditions, :defaults
56

57 58 59
        def initialize(set, scope, path, options)
          @set, @scope, @path, @options = set, scope, path, options
          @requirements, @conditions, @defaults = {}, {}, {}
60

61 62 63 64
          normalize_path!
          normalize_options!
          normalize_requirements!
          normalize_conditions!
65
          normalize_defaults!
66
        end
J
Joshua Peek 已提交
67

68
        def to_route
69
          [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
70
        end
J
Joshua Peek 已提交
71

72
        private
73

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

93
          def normalize_options!
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
            @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
112

113
            @options.merge!(default_controller_and_action)
114 115 116 117 118
          end

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

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

Y
Yves Senn 已提交
132 133 134 135 136 137 138 139 140 141
          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

142 143 144
          def normalize_defaults!
            @defaults.merge!(scope[:defaults]) if scope[:defaults]
            @defaults.merge!(options[:defaults]) if options[:defaults]
145

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

151 152 153 154 155
            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
156 157
            end

158 159 160 161
            if Regexp === options[:format]
              @defaults[:format] = nil
            elsif String === options[:format]
              @defaults[:format] = options[:format]
162
            end
163
          end
164

165 166
          def normalize_conditions!
            @conditions.merge!(:path_info => path)
167

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

173 174 175 176 177 178 179
            @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

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

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

197 198 199 200
          def app
            Constraints.new(endpoint, blocks, @set.request_class)
          end

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

211 212
              controller ||= default_controller
              action     ||= default_action
213

214
              unless controller.is_a?(Regexp)
215 216
                controller = [@scope[:module], controller].compact.join("/").presence
              end
217

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

222 223
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
224

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

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

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

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

254
          def constraints
255 256
            @constraints ||= {}.tap do |constraints|
              constraints.merge!(scope[:constraints]) if scope[:constraints]
257

258 259 260 261 262
              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)
263
            end
264
          end
J
Joshua Peek 已提交
265

266
          def segment_keys
267 268 269 270 271 272
            @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
          end

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

274 275 276 277 278 279 280 281 282 283
          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)
284
          end
285

286
          def to
287
            options[:to]
288
          end
J
Joshua Peek 已提交
289

290
          def default_controller
291
            options[:controller] || scope[:controller]
292
          end
293 294

          def default_action
295
            options[:action] || scope[:action]
296
          end
297
      end
298

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

308
      def self.normalize_name(name)
309
        normalize_path(name)[1..-1].tr("/", "_")
310 311
      end

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

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

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

488 489 490 491 492 493 494
            options = app
            app, path = options.find { |k, v| k.respond_to?(:call) }
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

P
Pratik Naik 已提交
495 496
          options[:as]  ||= app_name(app)
          options[:via] ||= :all
497

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

          define_generate_prefix(app, options[:as])
501 502 503
          self
        end

504 505 506 507
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
508

509 510 511 512 513 514
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

515 516 517
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
518 519 520 521 522

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
523
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
524
            end
525 526 527
          end

          def define_generate_prefix(app, name)
528
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
529 530

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
531 532
            _routes = @set
            app.routes.define_mounted_helper(name)
J
José Valim 已提交
533 534 535 536 537
            app.routes.singleton_class.class_eval do
              define_method :mounted? do
                true
              end

538
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
539
                prefix_options = options.slice(*_route.segment_keys)
540 541
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
542
                _routes.url_helpers.send("#{name}_path", prefix_options)
543 544 545
              end
            end
          end
546 547 548
      end

      module HttpHelpers
549
        # Define a route that only recognizes HTTP GET.
C
Cesar Carruitero 已提交
550
        # For supported arguments, see match[rdoc-ref:Base#match]
551
        #
A
AvnerCohen 已提交
552
        #   get 'bacon', to: 'food#bacon'
553
        def get(*args, &block)
554
          map_method(:get, args, &block)
555 556
        end

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

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

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

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

        private
590
          def map_method(method, args, &block)
591
            options = args.extract_options!
592
            options[:via] = method
593
            match(*args, options, &block)
594 595 596 597
            self
          end
      end

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

687
          options[:path] = args.flatten.join('/') if args.any?
688
          options[:constraints] ||= {}
689

690
          if options[:constraints].is_a?(Hash)
691 692 693 694 695
            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)
696 697
          else
            block, options[:constraints] = options[:constraints], {}
698 699
          end

700 701 702 703 704
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
705 706
          end

707 708
          recover[:blocks] = @scope[:blocks]
          @scope[:blocks]  = merge_blocks_scope(@scope[:blocks], block)
709 710 711 712

          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)

713 714 715
          yield
          self
        ensure
716
          @scope.merge!(recover)
717 718
        end

719 720 721
        # Scopes routes to a specific controller
        #
        #   controller "food" do
A
AvnerCohen 已提交
722
        #     match "bacon", action: "bacon"
723
        #   end
724 725 726
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
727 728
        end

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

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

R
Ryan Bigg 已提交
833
        # Allows you to set default parameters for a route, such as this:
A
AvnerCohen 已提交
834 835
        #   defaults id: 'home' do
        #     match 'scoped_pages/(:id)', to: 'pages#show'
836
        #   end
R
Ryan Bigg 已提交
837
        # Using this, the +:id+ parameter here will default to 'home'.
838 839 840 841
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

842
        private
J
José Valim 已提交
843
          def scope_options #:nodoc:
844 845 846
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

J
José Valim 已提交
847
          def merge_path_scope(parent, child) #:nodoc:
848
            Mapper.normalize_path("#{parent}/#{child}")
849 850
          end

J
José Valim 已提交
851
          def merge_shallow_path_scope(parent, child) #:nodoc:
852 853 854
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
855
          def merge_as_scope(parent, child) #:nodoc:
856
            parent ? "#{parent}_#{child}" : child
857 858
          end

J
José Valim 已提交
859
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
860 861 862
            parent ? "#{parent}_#{child}" : child
          end

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

J
José Valim 已提交
867
          def merge_controller_scope(parent, child) #:nodoc:
868
            child
869 870
          end

J
José Valim 已提交
871
          def merge_path_names_scope(parent, child) #:nodoc:
872 873 874
            merge_options_scope(parent, child)
          end

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

J
José Valim 已提交
879
          def merge_defaults_scope(parent, child) #:nodoc:
880 881 882
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
883
          def merge_blocks_scope(parent, child) #:nodoc:
884 885 886
            merged = parent ? parent.dup : []
            merged << child if child
            merged
887 888
          end

J
José Valim 已提交
889
          def merge_options_scope(parent, child) #:nodoc:
890
            (parent || {}).except(*override_keys(child)).merge!(child)
891
          end
892

J
José Valim 已提交
893
          def merge_shallow_scope(parent, child) #:nodoc:
894 895
            child ? true : false
          end
896

J
José Valim 已提交
897
          def override_keys(child) #:nodoc:
898 899
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
900 901
      end

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

948
        class Resource #:nodoc:
949
          attr_reader :controller, :path, :options, :param
950 951

          def initialize(entities, options = {})
952
            @name       = entities.to_s
953 954 955
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
956
            @param      = (options[:param] || :id).to_sym
957
            @options    = options
958 959
          end

960
          def default_actions
961
            [:index, :create, :new, :show, :update, :destroy, :edit]
962 963
          end

964
          def actions
965
            if only = @options[:only]
966
              Array(only).map(&:to_sym)
967
            elsif except = @options[:except]
968 969 970 971 972 973
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

974
          def name
975
            @as || @name
976 977
          end

978
          def plural
979
            @plural ||= name.to_s
980 981 982
          end

          def singular
983
            @singular ||= name.to_s.singularize
984 985
          end

986
          alias :member_name :singular
987

988
          # Checks for uncountable plurals, and appends "_index" if the plural
989
          # and singular form are the same.
990
          def collection_name
991
            singular == plural ? "#{plural}_index" : plural
992 993
          end

994
          def resource_scope
995
            { :controller => controller }
996 997
          end

998
          alias :collection_scope :path
999 1000

          def member_scope
1001
            "#{path}/:#{param}"
1002 1003
          end

1004 1005
          alias :shallow_scope :member_scope

1006
          def new_scope(new_path)
1007
            "#{path}/#{new_path}"
1008 1009
          end

1010 1011 1012 1013
          def nested_param
            :"#{singular}_#{param}"
          end

1014
          def nested_scope
1015
            "#{path}/:#{nested_param}"
1016
          end
1017

1018 1019 1020
        end

        class SingletonResource < Resource #:nodoc:
1021
          def initialize(entities, options)
1022
            super
1023
            @as         = nil
1024 1025
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1026 1027
          end

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

1032 1033
          def plural
            @plural ||= name.to_s.pluralize
1034 1035
          end

1036 1037
          def singular
            @singular ||= name.to_s
1038
          end
1039 1040 1041 1042 1043 1044

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1045 1046
        end

1047 1048 1049 1050
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

1051 1052 1053 1054 1055 1056 1057 1058 1059
        # 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 :geocoder
        #
        # creates six different routes in your application, all mapping to
S
Sebastian Martinez 已提交
1060
        # the +GeoCoders+ controller (note that the controller is named after
1061 1062
        # the plural):
        #
1063 1064 1065 1066
        #   GET       /geocoder/new
        #   POST      /geocoder
        #   GET       /geocoder
        #   GET       /geocoder/edit
1067
        #   PATCH/PUT /geocoder
1068
        #   DELETE    /geocoder
1069
        #
1070
        # === Options
1071
        # Takes same options as +resources+.
J
Joshua Peek 已提交
1072
        def resource(*resources, &block)
1073
          options = resources.extract_options!.dup
J
Joshua Peek 已提交
1074

1075
          if apply_common_behavior_for(:resource, resources, options, &block)
1076 1077 1078
            return self
          end

1079
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1080
            yield if block_given?
1081

1082 1083
            concerns(options[:concerns]) if options[:concerns]

1084
            collection do
1085
              post :create
1086
            end if parent_resource.actions.include?(:create)
1087

1088
            new do
1089
              get :new
1090
            end if parent_resource.actions.include?(:new)
1091

1092
            set_member_mappings_for_resource
1093 1094
          end

J
Joshua Peek 已提交
1095
          self
1096 1097
        end

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

1233
          if apply_common_behavior_for(:resources, resources, options, &block)
1234 1235 1236
            return self
          end

1237
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1238
            yield if block_given?
J
Joshua Peek 已提交
1239

1240 1241
            concerns(options[:concerns]) if options[:concerns]

1242
            collection do
1243 1244
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1245
            end
1246

1247
            new do
1248
              get :new
1249
            end if parent_resource.actions.include?(:new)
1250

1251
            set_member_mappings_for_resource
1252 1253
          end

J
Joshua Peek 已提交
1254
          self
1255 1256
        end

1257 1258 1259 1260 1261 1262 1263 1264 1265
        # 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 已提交
1266
        # with GET, and route to the search action of +PhotosController+. It will also
1267 1268
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1269
        def collection
1270 1271
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1272 1273
          end

1274 1275 1276 1277
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1278
          end
1279
        end
J
Joshua Peek 已提交
1280

1281 1282 1283 1284 1285 1286 1287 1288 1289
        # 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 已提交
1290
        # preview action of +PhotosController+. It will also create the
1291
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1292
        def member
1293 1294
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1295
          end
J
Joshua Peek 已提交
1296

1297 1298 1299 1300
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1301 1302 1303 1304 1305 1306 1307
          end
        end

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

1309 1310 1311 1312
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1313
          end
J
Joshua Peek 已提交
1314 1315
        end

1316
        def nested
1317 1318
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1319 1320 1321
          end

          with_scope_level(:nested) do
1322
            if shallow?
1323
              with_exclusive_scope do
1324
                if @scope[:shallow_path].blank?
1325
                  scope(parent_resource.nested_scope, nested_options) { yield }
1326
                else
1327
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1328
                    scope(parent_resource.nested_scope, nested_options) { yield }
1329 1330 1331 1332
                  end
                end
              end
            else
1333
              scope(parent_resource.nested_scope, nested_options) { yield }
1334 1335 1336 1337
            end
          end
        end

1338
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1339
        def namespace(path, options = {})
1340
          if resource_scope?
1341 1342 1343 1344 1345 1346
            nested { super }
          else
            super
          end
        end

1347
        def shallow
1348
          scope(:shallow => true, :shallow_path => @scope[:path]) do
1349 1350 1351 1352
            yield
          end
        end

1353 1354 1355 1356
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

1357
        # match 'path' => 'controller#action'
R
Rafael Mendonça França 已提交
1358
        # match 'path', to: 'controller#action'
1359
        # match 'path', 'otherpath', on: :member, via: :get
1360 1361 1362 1363
        def match(path, *rest)
          if rest.empty? && Hash === path
            options  = path
            path, to = options.find { |name, value| name.is_a?(String) }
1364 1365
            options[:to] = to
            options.delete(path)
1366 1367 1368 1369 1370 1371
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1372 1373 1374 1375 1376
          path_without_format = path.to_s.sub(/\(\.:format\)$/, '')
          if using_match_shorthand?(path_without_format, options)
            options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
          end

1377 1378
          options[:anchor] = true unless options.key?(:anchor)

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

1383 1384 1385 1386 1387
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
            decomposed_match(_path, route_options)
          end
1388 1389
          self
        end
1390

1391 1392 1393 1394
        def using_match_shorthand?(path, options)
          path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
        end

1395
        def decomposed_match(path, options) # :nodoc:
A
Aaron Patterson 已提交
1396 1397
          if on = options.delete(:on)
            send(on) { decomposed_match(path, options) }
1398
          else
A
Aaron Patterson 已提交
1399 1400 1401 1402 1403 1404 1405 1406
            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 已提交
1407
          end
1408
        end
J
Joshua Peek 已提交
1409

1410
        def add_route(action, options) # :nodoc:
1411
          path = path_for_action(action, options.delete(:path))
1412
          action = action.to_s.dup
1413

1414 1415
          if action =~ /^[\w\/]+$/
            options[:action] ||= action unless action.include?("/")
1416
          else
1417 1418 1419
            action = nil
          end

1420
          if !options.fetch(:as, true)
1421 1422 1423
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1424
          end
J
Joshua Peek 已提交
1425

1426
          mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1427 1428
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1429 1430
        end

1431 1432 1433 1434 1435 1436 1437 1438 1439
        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

1440
          if @scope[:scope_level] == :resources
1441 1442
            with_scope_level(:root) do
              scope(parent_resource.path) do
1443 1444 1445 1446 1447 1448
                super(options)
              end
            end
          else
            super(options)
          end
1449 1450
        end

1451
        protected
1452

1453
          def parent_resource #:nodoc:
1454 1455 1456
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1457
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1458 1459 1460 1461 1462
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1463 1464 1465 1466 1467
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1468
            options.keys.each do |k|
1469 1470 1471
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1472 1473 1474
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1475 1476 1477 1478 1479
                send(method, resources.pop, options, &block)
              end
              return true
            end

1480 1481 1482 1483
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1484 1485 1486
            false
          end

J
José Valim 已提交
1487
          def action_options?(options) #:nodoc:
1488 1489 1490
            options[:only] || options[:except]
          end

J
José Valim 已提交
1491
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1492
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1493 1494
          end

J
José Valim 已提交
1495
          def scope_action_options #:nodoc:
1496 1497 1498
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1499
          def resource_scope? #:nodoc:
1500
            [:resource, :resources].include? @scope[:scope_level]
1501 1502
          end

J
José Valim 已提交
1503
          def resource_method_scope? #:nodoc:
1504
            [:collection, :member, :new].include? @scope[:scope_level]
1505 1506
          end

1507
          def with_exclusive_scope
1508
            begin
1509 1510
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1511

1512 1513 1514
              with_scope_level(:exclusive) do
                yield
              end
1515
            ensure
1516
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1517 1518 1519
            end
          end

1520
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1521
            old, @scope[:scope_level] = @scope[:scope_level], kind
1522
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1523 1524 1525
            yield
          ensure
            @scope[:scope_level] = old
1526
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1527
          end
1528

1529 1530
          def resource_scope(kind, resource) #:nodoc:
            with_scope_level(kind, resource) do
1531
              scope(parent_resource.resource_scope) do
1532 1533 1534 1535 1536
                yield
              end
            end
          end

J
José Valim 已提交
1537
          def nested_options #:nodoc:
1538 1539
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1540 1541
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1542 1543

            options
1544 1545
          end

1546 1547
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1548 1549
          end

1550 1551
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1552 1553
          end

J
José Valim 已提交
1554
          def canonical_action?(action, flag) #:nodoc:
1555
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1556 1557
          end

J
José Valim 已提交
1558
          def shallow_scoping? #:nodoc:
1559
            shallow? && @scope[:scope_level] == :member
1560 1561
          end

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

1566
            if canonical_action?(action, path.blank?)
1567
              prefix.to_s
1568
            else
1569
              "#{prefix}/#{action_path(action, path)}"
1570 1571 1572
            end
          end

J
José Valim 已提交
1573
          def action_path(name, path = nil) #:nodoc:
1574
            name = name.to_sym if name.is_a?(String)
1575
            path || @scope[:path_names][name] || name.to_s
1576 1577
          end

J
José Valim 已提交
1578
          def prefix_name_for_action(as, action) #:nodoc:
1579
            if as
1580
              as.to_s
1581
            elsif !canonical_action?(action, @scope[:scope_level])
1582
              action.to_s
1583
            end
1584 1585
          end

J
José Valim 已提交
1586
          def name_for_action(as, action) #:nodoc:
1587
            prefix = prefix_name_for_action(as, action)
1588
            prefix = Mapper.normalize_name(prefix) if prefix
1589 1590 1591
            name_prefix = @scope[:as]

            if parent_resource
1592
              return nil unless as || action
1593

1594 1595
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1596
            end
1597

1598
            name = case @scope[:scope_level]
1599
            when :nested
1600
              [name_prefix, prefix]
1601
            when :collection
1602
              [prefix, name_prefix, collection_name]
1603
            when :new
1604 1605 1606 1607 1608
              [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]
1609
            else
1610
              [name_prefix, member_name, prefix]
1611
            end
1612

1613 1614 1615 1616 1617 1618 1619 1620 1621 1622
            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
1623
          end
1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635

          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 已提交
1636
      end
J
Joshua Peek 已提交
1637

1638
      # Routing Concerns allow you to declare common routes that can be reused
1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657
      # 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
1658
      module Concerns
1659
        # Define a routing concern using a name.
1660
        #
1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682
        # 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]
1683 1684
        #   end
        #
1685 1686 1687
        # Or, using a callable object, you might implement something more
        # specific to your application, which would be out of place in your
        # routes file.
1688
        #
1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699
        #   # 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]
1700 1701 1702
        #     end
        #   end
        #
1703 1704 1705 1706 1707 1708 1709 1710
        #   # routes.rb
        #   concern :purchasable, Purchasable.new(returnable: true)
        #
        #   resources :toys, concerns: :purchasable
        #   resources :electronics, concerns: :purchasable
        #   resources :pets do
        #     concerns :purchasable, returnable: false
        #   end
1711
        #
1712 1713 1714
        # 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>.
1715
        def concern(name, callable = nil, &block)
1716 1717
          callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
          @concerns[name] = callable
1718 1719
        end

1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730
        # 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
1731 1732 1733
        def concerns(*args)
          options = args.extract_options!
          args.flatten.each do |name|
1734
            if concern = @concerns[name]
1735
              concern.call(self, options)
1736 1737 1738 1739 1740 1741 1742
            else
              raise ArgumentError, "No concern named #{name} was found!"
            end
          end
        end
      end

1743 1744 1745
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1746
        @concerns = {}
1747 1748
      end

1749 1750
      include Base
      include HttpHelpers
1751
      include Redirection
1752
      include Scoping
1753
      include Concerns
1754
      include Resources
J
Joshua Peek 已提交
1755 1756
    end
  end
J
Joshua Peek 已提交
1757
end