mapper.rb 59.4 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
        SHORTHAND_REGEX = %r{/[\w/]+$}
54
        WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
55

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

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

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

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

73
        private
74

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

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

114 115
            if using_match_shorthand?(path_without_format, @options)
              to_shorthand    = @options[:to].blank?
116
              @options[:to] ||= path_without_format.gsub(/\(.*\)/, "")[1..-1].sub(%r{/([^/]*)$}, '#\1')
117 118
            end

119
            @options.merge!(default_controller_and_action(to_shorthand))
120
          end
121

122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
          # match "account/overview"
          def using_match_shorthand?(path, options)
            path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX
          end

          def normalize_format!
            if options[:format] == true
              options[:format] = /.+/
            elsif options[:format] == false
              options.delete(:format)
            end
          end

          def normalize_requirements!
            constraints.each do |key, requirement|
              next unless segment_keys.include?(key) || key == :controller
138

139
              if requirement.source =~ ANCHOR_CHARACTERS_REGEX
140 141
                raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
              end
142

143
              if requirement.multiline?
144
                raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
145
              end
146

147
              @requirements[key] = requirement
148
            end
149

150 151 152 153 154 155 156
            if options[:format] == true
              @requirements[:format] = /.+/
            elsif Regexp === options[:format]
              @requirements[:format] = options[:format]
            elsif String === options[:format]
              @requirements[:format] = Regexp.compile(options[:format])
            end
157
          end
158

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

163 164 165
            options.each do |key, default|
              next if Regexp === default || IGNORE_OPTIONS.include?(key)
              @defaults[key] = default
166 167
            end

168 169 170 171 172
            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
173 174
            end

175 176 177 178
            if Regexp === options[:format]
              @defaults[:format] = nil
            elsif String === options[:format]
              @defaults[:format] = options[:format]
179
            end
180
          end
181

182 183
          def normalize_conditions!
            @conditions.merge!(:path_info => path)
184

185 186 187 188
            constraints.each do |key, condition|
              next if segment_keys.include?(key) || key == :controller
              @conditions[key] = condition
            end
J
Joshua Peek 已提交
189

190 191 192 193 194 195 196
            @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

197 198 199 200 201 202 203 204
            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" \
                    "If you want to expose your action to GET, use `get` in the router:\n\n" \
                    "  Instead of: match \"controller#action\"\n" \
                    "  Do: get \"controller#action\""
              raise msg
205
            end
206

207 208 209
            if via = options[:via]
              list = Array(via).map { |m| m.to_s.dasherize.upcase }
              @conditions.merge!(:request_method => list)
210 211 212
            end
          end

213 214 215 216
          def app
            Constraints.new(endpoint, blocks, @set.request_class)
          end

217
          def default_controller_and_action(to_shorthand=nil)
218
            if to.respond_to?(:call)
219 220
              { }
            else
221
              if to.is_a?(String)
222
                controller, action = to.split('#')
223 224
              elsif to.is_a?(Symbol)
                action = to.to_s
225
              end
J
Joshua Peek 已提交
226

227 228
              controller ||= default_controller
              action     ||= default_action
229

230
              unless controller.is_a?(Regexp) || to_shorthand
231 232
                controller = [@scope[:module], controller].compact.join("/").presence
              end
233

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

238 239
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
240

241
              if controller.blank? && segment_keys.exclude?(:controller)
242 243
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
244

245
              if action.blank? && segment_keys.exclude?(:action)
246 247
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
248

249 250 251 252 253 254
              if controller.is_a?(String) && controller !~ /\A[a-z_\/]+\z/
                message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems."
                message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
                raise ArgumentError, message
              end

A
Aaron Patterson 已提交
255
              hash = {}
A
Aaron Patterson 已提交
256 257
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
258
              hash
259 260
            end
          end
261

262
          def blocks
263 264
            if options[:constraints].present? && !options[:constraints].is_a?(Hash)
              [options[:constraints]]
265
            else
266
              scope[:blocks] || []
267 268
            end
          end
J
Joshua Peek 已提交
269

270
          def constraints
271 272
            @constraints ||= {}.tap do |constraints|
              constraints.merge!(scope[:constraints]) if scope[:constraints]
273

274 275 276 277 278
              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)
279
            end
280
          end
J
Joshua Peek 已提交
281

282
          def segment_keys
283 284 285 286 287 288
            @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
          end

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

290 291 292 293 294 295 296 297 298 299
          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)
300
          end
301

302
          def to
303
            options[:to]
304
          end
J
Joshua Peek 已提交
305

306
          def default_controller
307
            options[:controller] || scope[:controller]
308
          end
309 310

          def default_action
311
            options[:action] || scope[:action]
312
          end
313
      end
314

315
      # Invokes Rack::Mount::Utils.normalize path and ensure that
316 317
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
318
      def self.normalize_path(path)
319
        path = Journey::Router::Utils.normalize_path(path)
320
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
321 322 323
        path
      end

324
      def self.normalize_name(name)
325
        normalize_path(name)[1..-1].tr("/", "_")
326 327
      end

328
      module Base
329 330
        # You can specify what Rails should route "/" to with the root method:
        #
A
AvnerCohen 已提交
331
        #   root to: 'pages#main'
332
        #
333
        # For options, see +match+, as +root+ uses it internally.
334
        #
B
Brian Cardarella 已提交
335 336 337 338
        # You can also pass a string which will expand
        #
        #   root 'pages#main'
        #
339 340 341
        # 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.
342
        def root(options = {})
B
Brian Cardarella 已提交
343
          options = { :to => options } if options.is_a?(String)
344
          match '/', { :as => :root, :via => :get }.merge!(options)
345
        end
346

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

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

501 502 503 504 505 506 507
            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 已提交
508 509
          options[:as]  ||= app_name(app)
          options[:via] ||= :all
510

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

          define_generate_prefix(app, options[:as])
514 515 516
          self
        end

517 518 519 520
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
521

522 523 524 525 526 527
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

528 529 530
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
531 532 533 534 535

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
536
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
537
            end
538 539 540
          end

          def define_generate_prefix(app, name)
541
            return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
542 543

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

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

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

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

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

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

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

        private
603
          def map_method(method, args, &block)
604
            options = args.extract_options!
605 606
            options[:via]    = method
            options[:path] ||= args.first if args.first.is_a?(String)
607
            match(*args, options, &block)
608 609 610 611
            self
          end
      end

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

701
          options[:path] = args.flatten.join('/') if args.any?
702
          options[:constraints] ||= {}
703

704
          if options[:constraints].is_a?(Hash)
705 706 707 708 709
            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)
710 711
          else
            block, options[:constraints] = options[:constraints], {}
712 713
          end

714 715 716 717 718
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
719 720
          end

721 722
          recover[:blocks] = @scope[:blocks]
          @scope[:blocks]  = merge_blocks_scope(@scope[:blocks], block)
723 724 725 726

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

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

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

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

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

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

856
        private
J
José Valim 已提交
857
          def scope_options #:nodoc:
858 859 860
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

J
José Valim 已提交
861
          def merge_path_scope(parent, child) #:nodoc:
862
            Mapper.normalize_path("#{parent}/#{child}")
863 864
          end

J
José Valim 已提交
865
          def merge_shallow_path_scope(parent, child) #:nodoc:
866 867 868
            Mapper.normalize_path("#{parent}/#{child}")
          end

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

J
José Valim 已提交
873
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
874 875 876
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
877
          def merge_module_scope(parent, child) #:nodoc:
878 879 880
            parent ? "#{parent}/#{child}" : child
          end

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

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

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

J
José Valim 已提交
893
          def merge_defaults_scope(parent, child) #:nodoc:
894 895 896
            merge_options_scope(parent, child)
          end

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

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

J
José Valim 已提交
907
          def merge_shallow_scope(parent, child) #:nodoc:
908 909
            child ? true : false
          end
910

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

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

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

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

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

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

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

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

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

1000
          alias :member_name :singular
1001

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

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

1012
          alias :collection_scope :path
1013 1014

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

1018 1019
          alias :shallow_scope :member_scope

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

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

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

1032 1033 1034
        end

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

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

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

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

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1059 1060
        end

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

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

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

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

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

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

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

1106
            set_member_mappings_for_resource
1107 1108
          end

J
Joshua Peek 已提交
1109
          self
1110 1111
        end

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

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

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

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

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

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

1265
            set_member_mappings_for_resource
1266 1267
          end

J
Joshua Peek 已提交
1268
          self
1269 1270
        end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1392
          paths.each { |_path| decomposed_match(_path, options.dup) }
1393 1394
          self
        end
1395

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

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

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

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

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

1432
        def root(options={})
1433
          if @scope[:scope_level] == :resources
1434 1435
            with_scope_level(:root) do
              scope(parent_resource.path) do
1436 1437 1438 1439 1440 1441
                super(options)
              end
            end
          else
            super(options)
          end
1442 1443
        end

1444
        protected
1445

1446
          def parent_resource #:nodoc:
1447 1448 1449
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1450
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1451 1452 1453 1454 1455
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1456 1457 1458 1459 1460
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1461
            options.keys.each do |k|
1462 1463 1464
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1465 1466 1467
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1468 1469 1470 1471 1472
                send(method, resources.pop, options, &block)
              end
              return true
            end

1473 1474 1475 1476
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1477 1478 1479
            false
          end

J
José Valim 已提交
1480
          def action_options?(options) #:nodoc:
1481 1482 1483
            options[:only] || options[:except]
          end

J
José Valim 已提交
1484
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1485
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1486 1487
          end

J
José Valim 已提交
1488
          def scope_action_options #:nodoc:
1489 1490 1491
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1492
          def resource_scope? #:nodoc:
1493
            [:resource, :resources].include? @scope[:scope_level]
1494 1495
          end

J
José Valim 已提交
1496
          def resource_method_scope? #:nodoc:
1497
            [:collection, :member, :new].include? @scope[:scope_level]
1498 1499
          end

1500
          def with_exclusive_scope
1501
            begin
1502 1503
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1504

1505 1506 1507
              with_scope_level(:exclusive) do
                yield
              end
1508
            ensure
1509
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1510 1511 1512
            end
          end

1513
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1514
            old, @scope[:scope_level] = @scope[:scope_level], kind
1515
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1516 1517 1518
            yield
          ensure
            @scope[:scope_level] = old
1519
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1520
          end
1521

1522 1523
          def resource_scope(kind, resource) #:nodoc:
            with_scope_level(kind, resource) do
1524
              scope(parent_resource.resource_scope) do
1525 1526 1527 1528 1529
                yield
              end
            end
          end

J
José Valim 已提交
1530
          def nested_options #:nodoc:
1531 1532
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1533 1534
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1535 1536

            options
1537 1538
          end

1539 1540
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1541 1542
          end

1543 1544
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1545 1546
          end

J
José Valim 已提交
1547
          def canonical_action?(action, flag) #:nodoc:
1548
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1549 1550
          end

J
José Valim 已提交
1551
          def shallow_scoping? #:nodoc:
1552
            shallow? && @scope[:scope_level] == :member
1553 1554
          end

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

1559
            if canonical_action?(action, path.blank?)
1560
              prefix.to_s
1561
            else
1562
              "#{prefix}/#{action_path(action, path)}"
1563 1564 1565
            end
          end

J
José Valim 已提交
1566
          def action_path(name, path = nil) #:nodoc:
1567
            name = name.to_sym if name.is_a?(String)
1568
            path || @scope[:path_names][name] || name.to_s
1569 1570
          end

J
José Valim 已提交
1571
          def prefix_name_for_action(as, action) #:nodoc:
1572
            if as
1573
              as.to_s
1574
            elsif !canonical_action?(action, @scope[:scope_level])
1575
              action.to_s
1576
            end
1577 1578
          end

J
José Valim 已提交
1579
          def name_for_action(as, action) #:nodoc:
1580
            prefix = prefix_name_for_action(as, action)
1581
            prefix = Mapper.normalize_name(prefix) if prefix
1582 1583 1584
            name_prefix = @scope[:as]

            if parent_resource
1585
              return nil unless as || action
1586

1587 1588
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1589
            end
1590

1591
            name = case @scope[:scope_level]
1592
            when :nested
1593
              [name_prefix, prefix]
1594
            when :collection
1595
              [prefix, name_prefix, collection_name]
1596
            when :new
1597 1598 1599 1600 1601
              [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]
1602
            else
1603
              [name_prefix, member_name, prefix]
1604
            end
1605

1606 1607 1608 1609 1610 1611 1612 1613 1614 1615
            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
1616
          end
1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628

          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 已提交
1629
      end
J
Joshua Peek 已提交
1630

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

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

1736 1737 1738
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1739
        @concerns = {}
1740 1741
      end

1742 1743
      include Base
      include HttpHelpers
1744
      include Redirection
1745
      include Scoping
1746
      include Concerns
1747
      include Resources
J
Joshua Peek 已提交
1748 1749
    end
  end
J
Joshua Peek 已提交
1750
end