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

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

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

26
        attr_reader :app, :constraints
27

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

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

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

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

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

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

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

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

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

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

75
        private
76

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
          def normalize_path!
            raise ArgumentError, "path is required" if @path.blank?
            @path = Mapper.normalize_path(@path)

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

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

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

96
          def normalize_options!
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
            @options.reverse_merge!(scope[:options]) if scope[:options]
            path_without_format = path.sub(/\(\.:format\)$/, '')

            # Add a constraint for wildcard route to make it non-greedy and match the
            # optional format part of the route by default
            if path_without_format.match(WILDCARD_PATH) && @options[:format] != false
              @options[$1.to_sym] ||= /.+?/
            end

            if path_without_format.match(':controller')
              raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]

              # Add a default constraint for :controller path segments that matches namespaced
              # controllers with default routes like :controller/:action/:id(.:format), e.g:
              # GET /admin/products/show/1
              # => { controller: 'admin/products', action: 'show', id: '1' }
              @options[:controller] ||= /.+?/
            end
115

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

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

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

Y
Yves Senn 已提交
135 136 137 138 139 140 141 142 143 144
          def verify_regexp_requirement(requirement)
            if requirement.source =~ ANCHOR_CHARACTERS_REGEX
              raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
            end

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

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

149
            options.each do |key, default|
A
Akshay Vishnoi 已提交
150 151 152
              unless Regexp === default || IGNORE_OPTIONS.include?(key)
                @defaults[key] = default
              end
153 154
            end

155 156
            if options[:constraints].is_a?(Hash)
              options[:constraints].each do |key, default|
A
Akshay Vishnoi 已提交
157 158 159
                if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
                  @defaults[key] ||= default
                end
160
              end
161 162
            end

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

170 171
          def normalize_conditions!
            @conditions.merge!(:path_info => path)
172

173
            constraints.each do |key, condition|
A
Akshay Vishnoi 已提交
174 175 176
              unless segment_keys.include?(key) || key == :controller
                @conditions[key] = condition
              end
177
            end
J
Joshua Peek 已提交
178

179 180
            @conditions[:required_defaults] = []
            options.each do |key, required_default|
A
Akshay Vishnoi 已提交
181 182 183
              unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default
                @conditions[:required_defaults] << key
              end
184 185
            end

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

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

203 204 205 206
          def app
            Constraints.new(endpoint, blocks, @set.request_class)
          end

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

217 218
              controller ||= default_controller
              action     ||= default_action
219

220
              unless controller.is_a?(Regexp)
221 222
                controller = [@scope[:module], controller].compact.join("/").presence
              end
223

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

228 229
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
230

231
              if controller.blank? && segment_keys.exclude?(:controller)
232 233
                message = "Missing :controller key on routes definition, please check your routes."
                raise ArgumentError, message
234
              end
J
Joshua Peek 已提交
235

236
              if action.blank? && segment_keys.exclude?(:action)
237 238
                message = "Missing :action key on routes definition, please check your routes."
                raise ArgumentError, message
239
              end
J
Joshua Peek 已提交
240

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

254
          def blocks
255 256
            if options[:constraints].present? && !options[:constraints].is_a?(Hash)
              [options[:constraints]]
257
            else
258
              scope[:blocks] || []
259 260
            end
          end
J
Joshua Peek 已提交
261

262
          def constraints
263 264
            @constraints ||= {}.tap do |constraints|
              constraints.merge!(scope[:constraints]) if scope[:constraints]
265

266 267 268 269 270
              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)
271
            end
272
          end
J
Joshua Peek 已提交
273

274
          def segment_keys
275 276 277 278 279 280
            @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
          end

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

282 283 284 285 286 287 288 289 290 291
          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)
292
          end
293

294
          def to
295
            options[:to]
296
          end
J
Joshua Peek 已提交
297

298
          def default_controller
299
            options[:controller] || scope[:controller]
300
          end
301 302

          def default_action
303
            options[:action] || scope[:action]
304
          end
305
      end
306

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

316
      def self.normalize_name(name)
317
        normalize_path(name)[1..-1].tr("/", "_")
318 319
      end

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

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

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

497
            options = app
498
            app, path = options.find { |k, _| k.respond_to?(:call) }
499 500 501 502 503
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

P
Pratik Naik 已提交
504 505
          options[:as]  ||= app_name(app)
          options[:via] ||= :all
506

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

          define_generate_prefix(app, options[:as])
510 511 512
          self
        end

513 514 515 516
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
517

518 519 520 521 522 523
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

524 525 526 527 528
        # Query if the following named route was already defined.
        def has_named_route?(name)
          @set.named_routes.routes[name.to_sym]
        end

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

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

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

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

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

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

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

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

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

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

        private
604
          def map_method(method, args, &block)
605
            options = args.extract_options!
606
            options[:via] = method
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 719 720 721 722 723
          SCOPE_OPTIONS.each do |option|
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
              value = options.delete(option)
            end

            if value
724 725 726
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
727 728 729 730 731
          end

          yield
          self
        ensure
732
          @scope.merge!(recover)
733 734
        end

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

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

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

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

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

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

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

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

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

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

883 884 885 886
          def merge_action_scope(parent, child) #:nodoc:
            child
          end

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

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

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

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

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

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

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

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

966
        class Resource #:nodoc:
967
          attr_reader :controller, :path, :options, :param
968 969

          def initialize(entities, options = {})
970
            @name       = entities.to_s
971 972 973
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
974
            @param      = (options[:param] || :id).to_sym
975
            @options    = options
976 977
          end

978
          def default_actions
979
            [:index, :create, :new, :show, :update, :destroy, :edit]
980 981
          end

982
          def actions
983
            if only = @options[:only]
984
              Array(only).map(&:to_sym)
985
            elsif except = @options[:except]
986 987 988 989 990 991
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

992
          def name
993
            @as || @name
994 995
          end

996
          def plural
997
            @plural ||= name.to_s
998 999 1000
          end

          def singular
1001
            @singular ||= name.to_s.singularize
1002 1003
          end

1004
          alias :member_name :singular
1005

1006
          # Checks for uncountable plurals, and appends "_index" if the plural
1007
          # and singular form are the same.
1008
          def collection_name
1009
            singular == plural ? "#{plural}_index" : plural
1010 1011
          end

1012
          def resource_scope
1013
            { :controller => controller }
1014 1015
          end

1016
          alias :collection_scope :path
1017 1018

          def member_scope
1019
            "#{path}/:#{param}"
1020 1021
          end

1022 1023
          alias :shallow_scope :member_scope

1024
          def new_scope(new_path)
1025
            "#{path}/#{new_path}"
1026 1027
          end

1028 1029 1030 1031
          def nested_param
            :"#{singular}_#{param}"
          end

1032
          def nested_scope
1033
            "#{path}/:#{nested_param}"
1034
          end
1035

1036 1037 1038
        end

        class SingletonResource < Resource #:nodoc:
1039
          def initialize(entities, options)
1040
            super
1041
            @as         = nil
1042 1043
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1044 1045
          end

1046 1047 1048 1049
          def default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

1050 1051
          def plural
            @plural ||= name.to_s.pluralize
1052 1053
          end

1054 1055
          def singular
            @singular ||= name.to_s
1056
          end
1057 1058 1059 1060 1061 1062

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
1063 1064
        end

1065 1066 1067 1068
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

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

1093
          if apply_common_behavior_for(:resource, resources, options, &block)
1094 1095 1096
            return self
          end

1097
          resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1098
            yield if block_given?
1099

1100 1101
            concerns(options[:concerns]) if options[:concerns]

1102
            collection do
1103
              post :create
1104
            end if parent_resource.actions.include?(:create)
1105

1106
            new do
1107
              get :new
1108
            end if parent_resource.actions.include?(:new)
1109

1110
            set_member_mappings_for_resource
1111 1112
          end

J
Joshua Peek 已提交
1113
          self
1114 1115
        end

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

1251
          if apply_common_behavior_for(:resources, resources, options, &block)
1252 1253 1254
            return self
          end

1255
          resource_scope(:resources, Resource.new(resources.pop, options)) do
1256
            yield if block_given?
J
Joshua Peek 已提交
1257

1258 1259
            concerns(options[:concerns]) if options[:concerns]

1260
            collection do
1261 1262
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
1263
            end
1264

1265
            new do
1266
              get :new
1267
            end if parent_resource.actions.include?(:new)
1268

1269
            set_member_mappings_for_resource
1270 1271
          end

J
Joshua Peek 已提交
1272
          self
1273 1274
        end

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

1292 1293 1294 1295
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1296
          end
1297
        end
J
Joshua Peek 已提交
1298

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

1315 1316 1317 1318
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1319 1320 1321 1322 1323 1324 1325
          end
        end

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

1327 1328 1329 1330
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1331
          end
J
Joshua Peek 已提交
1332 1333
        end

1334
        def nested
1335 1336
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1337 1338 1339
          end

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

1356
        # See ActionDispatch::Routing::Mapper::Scoping#namespace
1357
        def namespace(path, options = {})
1358
          if resource_scope?
1359 1360 1361 1362 1363 1364
            nested { super }
          else
            super
          end
        end

1365
        def shallow
1366
          scope(:shallow => true, :shallow_path => @scope[:path]) do
1367 1368 1369 1370
            yield
          end
        end

1371 1372 1373 1374
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

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

1390 1391
          options[:anchor] = true unless options.key?(:anchor)

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

1396 1397 1398 1399
          if @scope[:controller] && @scope[:action]
            options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
          end

1400 1401 1402
          paths.each do |_path|
            route_options = options.dup
            route_options[:path] ||= _path if _path.is_a?(String)
1403 1404 1405 1406 1407 1408

            path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
            if using_match_shorthand?(path_without_format, route_options)
              route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
            end

1409 1410
            decomposed_match(_path, route_options)
          end
1411 1412
          self
        end
1413

1414 1415 1416 1417
        def using_match_shorthand?(path, options)
          path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
        end

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

1433
        def add_route(action, options) # :nodoc:
1434
          path = path_for_action(action, options.delete(:path))
1435
          action = action.to_s.dup
1436

1437 1438
          if action =~ /^[\w\/]+$/
            options[:action] ||= action unless action.include?("/")
1439
          else
1440 1441 1442
            action = nil
          end

1443
          if !options.fetch(:as, true)
1444 1445 1446
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1447
          end
J
Joshua Peek 已提交
1448

1449
          mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1450 1451
          app, conditions, requirements, defaults, as, anchor = mapping.to_route
          @set.add_route(app, conditions, requirements, defaults, as, anchor)
J
Joshua Peek 已提交
1452 1453
        end

1454 1455 1456 1457 1458 1459 1460 1461 1462
        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

1463
          if @scope[:scope_level] == :resources
1464 1465
            with_scope_level(:root) do
              scope(parent_resource.path) do
1466 1467 1468 1469 1470 1471
                super(options)
              end
            end
          else
            super(options)
          end
1472 1473
        end

1474
        protected
1475

1476
          def parent_resource #:nodoc:
1477 1478 1479
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1480
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1481 1482 1483 1484 1485
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1486 1487 1488 1489 1490
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1491
            options.keys.each do |k|
1492 1493 1494
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1495 1496 1497
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1498 1499 1500 1501 1502
                send(method, resources.pop, options, &block)
              end
              return true
            end

1503 1504 1505 1506
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1507 1508 1509
            false
          end

J
José Valim 已提交
1510
          def action_options?(options) #:nodoc:
1511 1512 1513
            options[:only] || options[:except]
          end

J
José Valim 已提交
1514
          def scope_action_options? #:nodoc:
A
Aaron Patterson 已提交
1515
            @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1516 1517
          end

J
José Valim 已提交
1518
          def scope_action_options #:nodoc:
1519 1520 1521
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1522
          def resource_scope? #:nodoc:
1523
            RESOURCE_SCOPES.include? @scope[:scope_level]
1524 1525
          end

J
José Valim 已提交
1526
          def resource_method_scope? #:nodoc:
1527
            RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1528 1529
          end

1530
          def with_exclusive_scope
1531
            begin
1532 1533
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1534

1535 1536 1537
              with_scope_level(:exclusive) do
                yield
              end
1538
            ensure
1539
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1540 1541 1542
            end
          end

1543
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1544
            old, @scope[:scope_level] = @scope[:scope_level], kind
1545
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1546 1547 1548
            yield
          ensure
            @scope[:scope_level] = old
1549
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1550
          end
1551

1552 1553
          def resource_scope(kind, resource) #:nodoc:
            with_scope_level(kind, resource) do
1554
              scope(parent_resource.resource_scope) do
1555 1556 1557 1558 1559
                yield
              end
            end
          end

J
José Valim 已提交
1560
          def nested_options #:nodoc:
1561 1562
            options = { :as => parent_resource.member_name }
            options[:constraints] = {
1563 1564
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1565 1566

            options
1567 1568
          end

1569 1570
          def param_constraint? #:nodoc:
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1571 1572
          end

1573 1574
          def param_constraint #:nodoc:
            @scope[:constraints][parent_resource.param]
1575 1576
          end

J
José Valim 已提交
1577
          def canonical_action?(action, flag) #:nodoc:
1578
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1579 1580
          end

J
José Valim 已提交
1581
          def shallow_scoping? #:nodoc:
1582
            shallow? && @scope[:scope_level] == :member
1583 1584
          end

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

1589
            if canonical_action?(action, path.blank?)
1590
              prefix.to_s
1591
            else
1592
              "#{prefix}/#{action_path(action, path)}"
1593 1594 1595
            end
          end

J
José Valim 已提交
1596
          def action_path(name, path = nil) #:nodoc:
1597
            name = name.to_sym if name.is_a?(String)
1598
            path || @scope[:path_names][name] || name.to_s
1599 1600
          end

J
José Valim 已提交
1601
          def prefix_name_for_action(as, action) #:nodoc:
1602
            if as
1603
              as.to_s
1604
            elsif !canonical_action?(action, @scope[:scope_level])
1605
              action.to_s
1606
            end
1607 1608
          end

J
José Valim 已提交
1609
          def name_for_action(as, action) #:nodoc:
1610
            prefix = prefix_name_for_action(as, action)
1611
            prefix = Mapper.normalize_name(prefix) if prefix
1612 1613 1614
            name_prefix = @scope[:as]

            if parent_resource
1615
              return nil unless as || action
1616

1617 1618
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1619
            end
1620

1621
            name = case @scope[:scope_level]
1622
            when :nested
1623
              [name_prefix, prefix]
1624
            when :collection
1625
              [prefix, name_prefix, collection_name]
1626
            when :new
1627 1628 1629 1630 1631
              [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]
1632
            else
1633
              [name_prefix, member_name, prefix]
1634
            end
1635

1636 1637 1638 1639 1640 1641 1642 1643 1644 1645
            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
1646
          end
1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658

          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 已提交
1659
      end
J
Joshua Peek 已提交
1660

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

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

1766 1767 1768
      def initialize(set) #:nodoc:
        @set = set
        @scope = { :path_names => @set.resources_path_names }
1769
        @concerns = {}
1770 1771
      end

1772 1773
      include Base
      include HttpHelpers
1774
      include Redirection
1775
      include Scoping
1776
      include Concerns
1777
      include Resources
J
Joshua Peek 已提交
1778 1779
    end
  end
J
Joshua Peek 已提交
1780
end