mapper.rb 75.5 KB
Newer Older
1 2 3
require "active_support/core_ext/hash/slice"
require "active_support/core_ext/enumerable"
require "active_support/core_ext/array/extract_options"
D
David Heinemeier Hansson 已提交
4
require "active_support/core_ext/regexp"
5 6
require "action_dispatch/routing/redirection"
require "action_dispatch/routing/endpoint"
7

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

13
      class Constraints < Routing::Endpoint #:nodoc:
14
        attr_reader :app, :constraints
15

A
Aaron Patterson 已提交
16 17 18 19
        SERVE = ->(app, req) { app.serve req }
        CALL  = ->(app, req) { app.call req.env }

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

A
Aaron Patterson 已提交
29
          @strategy = strategy
30

31
          @app, @constraints, = app, constraints
32 33
        end

A
Aaron Patterson 已提交
34
        def dispatcher?; @strategy == SERVE; end
35

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

43
        def serve(req)
44
          return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
45

A
Aaron Patterson 已提交
46
          @strategy.call @app, req
47
        end
48 49 50

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

55
      class Mapping #:nodoc:
56
        ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
57
        OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
58

59
        attr_reader :requirements, :defaults
A
Aaron Patterson 已提交
60
        attr_reader :to, :default_controller, :default_action
61
        attr_reader :required_defaults, :ast
62

63
        def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
64 65
          options = scope[:options].merge(options) if scope[:options]

66
          defaults = (scope[:defaults] || {}).dup
A
Aaron Patterson 已提交
67
          scope_constraints = scope[:constraints] || {}
68

69
          new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
70 71
        end

72 73 74 75 76 77 78 79 80 81 82 83
        def self.check_via(via)
          if via.empty?
            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 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" \
              "  Instead of: match \"controller#action\"\n" \
              "  Do: get \"controller#action\""
            raise ArgumentError, msg
          end
          via
        end

A
Aaron Patterson 已提交
84 85 86 87 88 89 90 91 92 93 94 95 96
        def self.normalize_path(path, format)
          path = Mapper.normalize_path(path)

          if format == true
            "#{path}.:format"
          elsif optional_format?(path, format)
            "#{path}(.:format)"
          else
            path
          end
        end

        def self.optional_format?(path, format)
97
          format != false && path !~ OPTIONAL_FORMAT_REGEX
A
Aaron Patterson 已提交
98 99
        end

100
        def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
101
          @defaults = defaults
102
          @set = set
103

A
Aaron Patterson 已提交
104
          @to                 = to
105
          @default_controller = controller
106
          @default_action     = default_action
107 108
          @ast                = ast
          @anchor             = anchor
A
Aaron Patterson 已提交
109
          @via                = via
110
          @internal           = options.delete(:internal)
111

A
Aaron Patterson 已提交
112
          path_params = ast.find_all(&:symbol?).map(&:to_sym)
113

114 115 116
          options = add_wildcard_options(options, formatted, ast)

          options = normalize_options!(options, path_params, modyoule)
117

118
          split_options = constraints(options, path_params)
A
Aaron Patterson 已提交
119

120
          constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
121 122

          if options_constraints.is_a?(Hash)
123
            @defaults = Hash[options_constraints.find_all { |key, default|
124
              URL_OPTIONS.include?(key) && (String === default || Integer === default)
125
            }].merge @defaults
126
            @blocks = blocks
127
            constraints.merge! options_constraints
128 129
          else
            @blocks = blocks(options_constraints)
130 131
          end

132
          requirements, conditions = split_constraints path_params, constraints
A
Aaron Patterson 已提交
133 134
          verify_regexp_requirements requirements.map(&:last).grep(Regexp)

135
          formats = normalize_format(formatted)
136

137 138
          @requirements = formats[:requirements].merge Hash[requirements]
          @conditions = Hash[conditions]
139
          @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
140

141
          if path_params.include?(:action) && !@requirements.key?(:action)
142
            @defaults[:action] ||= "index"
143 144
          end

145
          @required_defaults = (split_options[:required_defaults] || []).map(&:first)
146
        end
J
Joshua Peek 已提交
147

148
        def make_route(name, precedence)
149
          route = Journey::Route.new(name,
150 151 152 153
                            application,
                            path,
                            conditions,
                            required_defaults,
154
                            defaults,
155
                            request_method,
J
Jon Moss 已提交
156 157
                            precedence,
                            @internal)
158 159 160 161

          route
        end

162 163
        def application
          app(@blocks)
164
        end
J
Joshua Peek 已提交
165

166 167 168 169 170 171 172 173 174 175 176
        def path
          build_path @ast, requirements, @anchor
        end

        def conditions
          build_conditions @conditions, @set.request_class
        end

        def build_conditions(current_conditions, request_class)
          conditions = current_conditions.dup

177 178 179 180 181 182 183
          conditions.keep_if do |k, _|
            request_class.public_method_defined?(k)
          end
        end
        private :build_conditions

        def request_method
184
          @via.map { |x| Journey::Route.verb_matcher(x) }
185
        end
186
        private :request_method
187

188 189
        JOINED_SEPARATORS = SEPARATORS.join # :nodoc:

190
        def build_path(ast, requirements, anchor)
191
          pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
192

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
          # Find all the symbol nodes that are adjacent to literal nodes and alter
          # the regexp so that Journey will partition them into custom routes.
          ast.find_all { |node|
            next unless node.cat?

            if node.left.literal? && node.right.symbol?
              symbol = node.right
            elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
              symbol = node.right.left
            elsif node.left.symbol? && node.right.literal?
              symbol = node.left
            elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
              symbol = node.left
            else
              next
            end

            if symbol
              symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
            end
213 214 215 216 217 218
          }

          pattern
        end
        private :build_path

219
        private
220
          def add_wildcard_options(options, formatted, path_ast)
221
            # Add a constraint for wildcard route to make it non-greedy and match the
222
            # optional format part of the route by default.
223
            if formatted != false
224 225 226 227 228
              path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
                hash[node.name.to_sym] ||= /.+?/
              }.merge options
            else
              options
229
            end
230
          end
231

232
          def normalize_options!(options, path_params, modyoule)
233
            if path_params.include?(:controller)
234
              raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
235 236 237 238 239

              # 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' }
240
              options[:controller] ||= /.+?/
241
            end
242

243
            if to.respond_to?(:action) || to.respond_to?(:call)
A
Aaron Patterson 已提交
244 245
              options
            else
246 247 248 249 250 251 252
              to_endpoint = split_to to
              controller  = to_endpoint[0] || default_controller
              action      = to_endpoint[1] || default_action

              controller = add_controller_module(controller, modyoule)

              options.merge! check_controller_and_action(path_params, controller, action)
A
Aaron Patterson 已提交
253
            end
254 255
          end

256
          def split_constraints(path_params, constraints)
257
            constraints.partition do |key, requirement|
A
Aaron Patterson 已提交
258
              path_params.include?(key) || key == :controller
259
            end
260
          end
261

262
          def normalize_format(formatted)
A
Aaron Patterson 已提交
263 264
            case formatted
            when true
265 266
              { requirements: { format: /.+/ },
                defaults:     {} }
A
Aaron Patterson 已提交
267
            when Regexp
268 269
              { requirements: { format: formatted },
                defaults:     { format: nil } }
A
Aaron Patterson 已提交
270
            when String
271 272 273
              { requirements: { format: Regexp.compile(formatted) },
                defaults:     { format: formatted } }
            else
274
              { requirements: {}, defaults: {} }
275
            end
276
          end
277

A
Aaron Patterson 已提交
278 279 280 281 282
          def verify_regexp_requirements(requirements)
            requirements.each do |requirement|
              if requirement.source =~ ANCHOR_CHARACTERS_REGEX
                raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
              end
Y
Yves Senn 已提交
283

A
Aaron Patterson 已提交
284 285 286
              if requirement.multiline?
                raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
              end
Y
Yves Senn 已提交
287 288 289
            end
          end

290 291
          def normalize_defaults(options)
            Hash[options.reject { |_, default| Regexp === default }]
292
          end
293

294
          def app(blocks)
295
            if to.respond_to?(:action)
296
              Routing::RouteSet::StaticDispatcher.new to
297 298 299 300
            elsif to.respond_to?(:call)
              Constraints.new(to, blocks, Constraints::CALL)
            elsif blocks.any?
              Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
301
            else
302
              dispatcher(defaults.key?(:controller))
303
            end
304 305
          end

306
          def check_controller_and_action(path_params, controller, action)
307
            hash = check_part(:controller, controller, path_params, {}) do |part|
A
Aaron Patterson 已提交
308
              translate_controller(part) {
309 310
                message = "'#{part}' 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"
311

A
Aaron Patterson 已提交
312 313
                raise ArgumentError, message
              }
314
            end
315

316
            check_part(:action, action, path_params, hash) { |part|
A
Aaron Patterson 已提交
317
              part.is_a?(Regexp) ? part : part.to_s
318
            }
319
          end
320

321
          def check_part(name, part, path_params, hash)
322
            if part
A
Aaron Patterson 已提交
323
              hash[name] = yield(part)
324
            else
325
              unless path_params.include?(name)
326
                message = "Missing :#{name} key on routes definition, please check your routes."
327 328
                raise ArgumentError, message
              end
329
            end
330
            hash
331
          end
332

333
          def split_to(to)
334
            if to =~ /#/
335
              to.split("#")
336 337
            else
              []
338
            end
339
          end
340

341
          def add_controller_module(controller, modyoule)
342 343
            if modyoule && !controller.is_a?(Regexp)
              if controller =~ %r{\A/}
344
                controller[1..-1]
345
              else
346
                [modyoule, controller].compact.join("/")
347
              end
348 349
            else
              controller
350 351 352
            end
          end

A
Aaron Patterson 已提交
353
          def translate_controller(controller)
354 355
            return controller if Regexp === controller
            return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
356

357
            yield
358 359
          end

360 361 362
          def blocks(callable_constraint)
            unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
              raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
363
            end
364
            [callable_constraint]
365
          end
J
Joshua Peek 已提交
366

367
          def constraints(options, path_params)
368
            options.group_by do |key, option|
A
Aaron Patterson 已提交
369
              if Regexp === option
370
                :constraints
A
Aaron Patterson 已提交
371
              else
372 373 374 375 376
                if path_params.include?(key)
                  :path_params
                else
                  :required_defaults
                end
377
              end
378
            end
379
          end
J
Joshua Peek 已提交
380

381
          def dispatcher(raise_on_name_error)
A
Aaron Patterson 已提交
382
            Routing::RouteSet::Dispatcher.new raise_on_name_error
383 384
          end
      end
385

386
      # Invokes Journey::Router::Utils.normalize_path and ensure that
387 388
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
389
      def self.normalize_path(path)
390
        path = Journey::Router::Utils.normalize_path(path)
391
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
392 393 394
        path
      end

395
      def self.normalize_name(name)
396
        normalize_path(name)[1..-1].tr("/", "_")
397 398
      end

399
      module Base
400
        # Matches a URL pattern to one or more routes.
401
        #
402
        # You should not use the +match+ method in your router
403 404 405
        # without specifying an HTTP method.
        #
        # If you want to expose your action to both GET and POST, use:
406
        #
407
        #   # sets :controller, :action and :id in params
408 409
        #   match ':controller/:action/:id', via: [:get, :post]
        #
410
        # Note that +:controller+, +:action+ and +:id+ are interpreted as URL
411
        # query parameters and thus available through +params+ in an action.
412
        #
413
        # If you want to expose your action to GET, use +get+ in the router:
414 415 416 417 418 419 420 421
        #
        # Instead of:
        #
        #   match ":controller/:action/:id"
        #
        # Do:
        #
        #   get ":controller/:action/:id"
422
        #
423 424 425 426
        # 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:
        #
427
        #   get 'songs/*category/:title', to: 'songs#show'
428 429 430 431 432
        #
        #   # 'songs/rock/classic/stairway-to-heaven' sets
        #   #  params[:category] = 'rock/classic'
        #   #  params[:title] = 'stairway-to-heaven'
        #
433 434 435 436
        # To match a wildcard parameter, it must have a name assigned to it.
        # Without a variable name to attach the glob parameter to, the route
        # can't be parsed.
        #
437 438
        # When a pattern points to an internal route, the route's +:action+ and
        # +:controller+ should be set in options or hash shorthand. Examples:
439
        #
440 441 442
        #   match 'photos/:id' => 'photos#show', via: :get
        #   match 'photos/:id', to: 'photos#show', via: :get
        #   match 'photos/:id', controller: 'photos', action: 'show', via: :get
443
        #
444 445 446
        # A pattern can also point to a +Rack+ endpoint i.e. anything that
        # responds to +call+:
        #
447
        #   match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
448
        #   match 'photos/:id', to: PhotoRackApp, via: :get
449
        #   # Yes, controller actions are just rack endpoints
450
        #   match 'photos/:id', to: PhotosController.action(:show), via: :get
451
        #
452 453
        # Because requesting various HTTP verbs with a single action has security
        # implications, you must either specify the actions in
454
        # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
455
        # instead +match+
456
        #
457
        # === Options
458
        #
459
        # Any options not seen here are passed on as params with the URL.
460 461 462 463 464 465 466
        #
        # [:controller]
        #   The route's controller.
        #
        # [:action]
        #   The route's action.
        #
467
        # [:param]
468
        #   Overrides the default resource identifier +:id+ (name of the
469 470 471
        #   dynamic segment used to generate the routes).
        #   You can access that segment from your controller using
        #   <tt>params[<:param>]</tt>.
Y
yui-knk 已提交
472 473 474 475 476
        #   In your router:
        #
        #      resources :user, param: :name
        #
        #   You can override <tt>ActiveRecord::Base#to_param</tt> of a related
477
        #   model to construct a URL:
Y
yui-knk 已提交
478 479
        #
        #      class User < ActiveRecord::Base
480
        #        def to_param
Y
yui-knk 已提交
481 482 483 484 485 486
        #          name
        #        end
        #      end
        #
        #   user = User.find_by(name: 'Phusion')
        #   user_path(user)  # => "/users/Phusion"
487
        #
488 489 490 491 492 493
        # [:path]
        #   The path prefix for the routes.
        #
        # [:module]
        #   The namespace for :controller.
        #
494
        #     match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
495
        #     # => Sekret::PostsController
496 497 498 499 500 501 502 503 504
        #
        #   See <tt>Scoping#namespace</tt> for its scope equivalent.
        #
        # [:as]
        #   The name used to generate routing helpers.
        #
        # [:via]
        #   Allowed HTTP verb(s) for route.
        #
505 506 507
        #      match 'path', to: 'c#a', via: :get
        #      match 'path', to: 'c#a', via: [:get, :post]
        #      match 'path', to: 'c#a', via: :all
508 509
        #
        # [:to]
510 511
        #   Points to a +Rack+ endpoint. Can be an object that responds to
        #   +call+ or a string representing a controller's action.
512
        #
513
        #      match 'path', to: 'controller#action', via: :get
514
        #      match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
515
        #      match 'path', to: RackApp, via: :get
516 517 518
        #
        # [:on]
        #   Shorthand for wrapping routes in a specific RESTful context. Valid
519
        #   values are +:member+, +:collection+, and +:new+. Only use within
520 521 522
        #   <tt>resource(s)</tt> block. For example:
        #
        #      resource :bar do
523
        #        match 'foo', to: 'c#a', on: :member, via: [:get, :post]
524 525 526 527 528 529
        #      end
        #
        #   Is equivalent to:
        #
        #      resource :bar do
        #        member do
530
        #          match 'foo', to: 'c#a', via: [:get, :post]
531 532 533 534
        #        end
        #      end
        #
        # [:constraints]
535
        #   Constrains parameters with a hash of regular expressions
Y
Yves Senn 已提交
536 537 538
        #   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.).
539
        #
540
        #     match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
541
        #
542
        #     match 'json_only', constraints: { format: 'json' }, via: :get
Y
Yves Senn 已提交
543
        #
544
        #     class Whitelist
545 546
        #       def matches?(request) request.remote_ip == '1.2.3.4' end
        #     end
547
        #     match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
548 549 550 551 552 553 554 555
        #
        #   See <tt>Scoping#constraints</tt> for more examples with its scope
        #   equivalent.
        #
        # [:defaults]
        #   Sets defaults for parameters
        #
        #     # Sets params[:format] to 'jpg' by default
556
        #     match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
557 558
        #
        #   See <tt>Scoping#defaults</tt> for its scope equivalent.
559 560
        #
        # [:anchor]
561
        #   Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
562 563 564
        #   false, the pattern matches any request prefixed with the given path.
        #
        #     # Matches any request starting with 'path'
565
        #     match 'path', to: 'c#a', anchor: false, via: :get
566 567
        #
        # [:format]
568
        #   Allows you to specify the default value for optional +format+
V
Vijay Dev 已提交
569
        #   segment or disable it by supplying +false+.
570
        def match(path, options = nil)
571
        end
572

573 574
        # Mount a Rack-based application to be used within the application.
        #
A
AvnerCohen 已提交
575
        #   mount SomeRackApp, at: "some_route"
576 577 578
        #
        # Alternatively:
        #
R
Ryan Bigg 已提交
579
        #   mount(SomeRackApp => "some_route")
580
        #
581 582
        # For options, see +match+, as +mount+ uses it internally.
        #
583 584 585 586 587
        # 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 已提交
588
        #   mount(SomeRackApp => "some_route", as: "exciting")
589 590 591
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
592 593 594
        def mount(app, options = nil)
          if options
            path = options.delete(:at)
595
          elsif Hash === app
596
            options = app
597
            app, path = options.find { |k, _| k.respond_to?(:call) }
598 599 600
            options.delete(app) if app
          end

601 602 603 604 605 606 607 608
          raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
          raise ArgumentError, <<-MSG.strip_heredoc unless path
            Must be called with mount point

              mount SomeRackApp, at: "some_route"
              or
              mount(SomeRackApp => "some_route")
          MSG
609

610 611
          rails_app = rails_app? app
          options[:as] ||= app_name(app, rails_app)
A
Aaron Patterson 已提交
612

613
          target_as       = name_for_action(options[:as], path)
P
Pratik Naik 已提交
614
          options[:via] ||= :all
615

616
          match(path, options.merge(to: app, anchor: false, format: false))
617

618
          define_generate_prefix(app, target_as) if rails_app
619 620 621
          self
        end

622 623 624 625
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
626

627 628 629 630 631 632
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

633 634
        # Query if the following named route was already defined.
        def has_named_route?(name)
A
Aaron Patterson 已提交
635
          @set.named_routes.key? name
636 637
        end

638
        private
A
Aaron Patterson 已提交
639 640
          def rails_app?(app)
            app.is_a?(Class) && app < Rails::Railtie
641 642
          end

643 644
          def app_name(app, rails_app)
            if rails_app
645
              app.railtie_name
646 647
            elsif app.is_a?(Class)
              class_name = app.name
648 649 650 651
              ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
            end
          end

652
          def define_generate_prefix(app, name)
653
            _route = @set.named_routes.get name
P
Piotr Sarnacki 已提交
654 655
            _routes = @set
            app.routes.define_mounted_helper(name)
656
            app.routes.extend Module.new {
A
Aaron Patterson 已提交
657
              def optimize_routes_generation?; false; end
658
              define_method :find_script_name do |options|
659 660 661 662
                if options.key? :script_name
                  super(options)
                else
                  prefix_options = options.slice(*_route.segment_keys)
663
                  prefix_options[:relative_url_root] = "".freeze
664
                  # We must actually delete prefix segment keys to avoid passing them to next url_for.
665 666
                  _route.segment_keys.each { |k| options.delete(k) }
                  _routes.url_helpers.send("#{name}_path", prefix_options)
667
                end
668
              end
669
            }
670
          end
671 672 673
      end

      module HttpHelpers
674
        # Define a route that only recognizes HTTP GET.
C
Cesar Carruitero 已提交
675
        # For supported arguments, see match[rdoc-ref:Base#match]
676
        #
A
AvnerCohen 已提交
677
        #   get 'bacon', to: 'food#bacon'
678
        def get(*args, &block)
679
          map_method(:get, args, &block)
680 681
        end

682
        # Define a route that only recognizes HTTP POST.
C
Cesar Carruitero 已提交
683
        # For supported arguments, see match[rdoc-ref:Base#match]
684
        #
A
AvnerCohen 已提交
685
        #   post 'bacon', to: 'food#bacon'
686
        def post(*args, &block)
687
          map_method(:post, args, &block)
688 689
        end

690
        # Define a route that only recognizes HTTP PATCH.
C
Cesar Carruitero 已提交
691
        # For supported arguments, see match[rdoc-ref:Base#match]
692
        #
A
AvnerCohen 已提交
693
        #   patch 'bacon', to: 'food#bacon'
694 695 696 697
        def patch(*args, &block)
          map_method(:patch, args, &block)
        end

698
        # Define a route that only recognizes HTTP PUT.
C
Cesar Carruitero 已提交
699
        # For supported arguments, see match[rdoc-ref:Base#match]
700
        #
A
AvnerCohen 已提交
701
        #   put 'bacon', to: 'food#bacon'
702
        def put(*args, &block)
703
          map_method(:put, args, &block)
704 705
        end

706
        # Define a route that only recognizes HTTP DELETE.
C
Cesar Carruitero 已提交
707
        # For supported arguments, see match[rdoc-ref:Base#match]
708
        #
A
AvnerCohen 已提交
709
        #   delete 'broccoli', to: 'food#broccoli'
710
        def delete(*args, &block)
711
          map_method(:delete, args, &block)
712 713 714
        end

        private
715
          def map_method(method, args, &block)
716
            options = args.extract_options!
717
            options[:via] = method
718
            match(*args, options, &block)
719 720 721 722
            self
          end
      end

723 724 725
      # 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 已提交
726 727
      # the <tt>app/controllers/admin</tt> directory, and you can group them
      # together in your router:
728 729 730 731
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
732
      #
733
      # This will create a number of routes for each of the posts and comments
S
Sebastian Martinez 已提交
734
      # controller. For <tt>Admin::PostsController</tt>, Rails will create:
735
      #
736 737 738 739 740
      #   GET       /admin/posts
      #   GET       /admin/posts/new
      #   POST      /admin/posts
      #   GET       /admin/posts/1
      #   GET       /admin/posts/1/edit
741
      #   PATCH/PUT /admin/posts/1
742
      #   DELETE    /admin/posts/1
743
      #
744
      # If you want to route /posts (without the prefix /admin) to
S
Sebastian Martinez 已提交
745
      # <tt>Admin::PostsController</tt>, you could use
746
      #
A
AvnerCohen 已提交
747
      #   scope module: "admin" do
748
      #     resources :posts
749 750 751
      #   end
      #
      # or, for a single case
752
      #
A
AvnerCohen 已提交
753
      #   resources :posts, module: "admin"
754
      #
S
Sebastian Martinez 已提交
755
      # If you want to route /admin/posts to +PostsController+
756
      # (without the <tt>Admin::</tt> module prefix), you could use
757
      #
758
      #   scope "/admin" do
759
      #     resources :posts
760 761 762
      #   end
      #
      # or, for a single case
763
      #
A
AvnerCohen 已提交
764
      #   resources :posts, path: "/admin/posts"
765 766 767
      #
      # 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 已提交
768
      # +PostsController+:
769
      #
770 771 772 773 774
      #   GET       /admin/posts
      #   GET       /admin/posts/new
      #   POST      /admin/posts
      #   GET       /admin/posts/1
      #   GET       /admin/posts/1/edit
775
      #   PATCH/PUT /admin/posts/1
776
      #   DELETE    /admin/posts/1
777
      module Scoping
778
        # Scopes a set of routes to the given default options.
779 780 781
        #
        # Take the following route definition as an example:
        #
A
AvnerCohen 已提交
782
        #   scope path: ":account_id", as: "account" do
783 784 785 786
        #     resources :projects
        #   end
        #
        # This generates helpers such as +account_projects_path+, just like +resources+ does.
787 788
        # The difference here being that the routes generated are like /:account_id/projects,
        # rather than /accounts/:account_id/projects.
789
        #
790
        # === Options
791
        #
792
        # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
793
        #
S
Sebastian Martinez 已提交
794
        #   # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
795
        #   scope module: "admin" do
796 797
        #     resources :posts
        #   end
798
        #
799
        #   # prefix the posts resource's requests with '/admin'
A
AvnerCohen 已提交
800
        #   scope path: "/admin" do
801 802
        #     resources :posts
        #   end
803
        #
S
Sebastian Martinez 已提交
804
        #   # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
A
AvnerCohen 已提交
805
        #   scope as: "sekret" do
806 807
        #     resources :posts
        #   end
808
        def scope(*args)
809
          options = args.extract_options!.dup
A
Aaron Patterson 已提交
810
          scope = {}
811

812
          options[:path] = args.flatten.join("/") if args.any?
813
          options[:constraints] ||= {}
814

815
          unless nested_scope?
816 817
            options[:shallow_path] ||= options[:path] if options.key?(:path)
            options[:shallow_prefix] ||= options[:as] if options.key?(:as)
818 819
          end

820
          if options[:constraints].is_a?(Hash)
821
            defaults = options[:constraints].select do |k, v|
822
              URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
823 824
            end

825
            options[:defaults] = defaults.merge(options[:defaults] || {})
826 827
          else
            block, options[:constraints] = options[:constraints], {}
828 829
          end

830 831 832 833 834
          if options.key?(:only) || options.key?(:except)
            scope[:action_options] = { only: options.delete(:only),
                                       except: options.delete(:except) }
          end

835
          if options.key? :anchor
836
            raise ArgumentError, "anchor is ignored unless passed to `match`"
837 838
          end

839
          @scope.options.each do |option|
840 841 842 843 844
            if option == :blocks
              value = block
            elsif option == :options
              value = options
            else
845
              value = options.delete(option) { POISON }
846 847
            end

848
            unless POISON == value
A
Aaron Patterson 已提交
849
              scope[option] = send("merge_#{option}_scope", @scope[option], value)
850
            end
851 852
          end

A
Aaron Patterson 已提交
853
          @scope = @scope.new scope
854 855 856
          yield
          self
        ensure
A
Aaron Patterson 已提交
857
          @scope = @scope.parent
858 859
        end

860 861
        POISON = Object.new # :nodoc:

862 863 864
        # Scopes routes to a specific controller
        #
        #   controller "food" do
865
        #     match "bacon", action: :bacon, via: :get
866
        #   end
A
Aaron Patterson 已提交
867
        def controller(controller)
868 869 870 871
          @scope = @scope.new(controller: controller)
          yield
        ensure
          @scope = @scope.parent
872 873
        end

874 875 876 877 878 879 880 881
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
882 883 884 885 886
        #       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
887
        #        admin_post PATCH/PUT /admin/posts/:id(.:format)      admin/posts#update
888
        #        admin_post DELETE    /admin/posts/:id(.:format)      admin/posts#destroy
889
        #
890
        # === Options
891
        #
892 893
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
        # options all default to the name of the namespace.
894
        #
895 896
        # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
        # <tt>Resources#resources</tt>.
897
        #
898
        #   # accessible through /sekret/posts rather than /admin/posts
A
AvnerCohen 已提交
899
        #   namespace :admin, path: "sekret" do
900 901
        #     resources :posts
        #   end
902
        #
S
Sebastian Martinez 已提交
903
        #   # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
904
        #   namespace :admin, module: "sekret" do
905 906
        #     resources :posts
        #   end
907
        #
S
Sebastian Martinez 已提交
908
        #   # generates +sekret_posts_path+ rather than +admin_posts_path+
A
AvnerCohen 已提交
909
        #   namespace :admin, as: "sekret" do
910 911
        #     resources :posts
        #   end
912
        def namespace(path, options = {})
913
          path = path.to_s
914 915 916 917 918 919 920 921

          defaults = {
            module:         path,
            as:             options.fetch(:as, path),
            shallow_path:   options.fetch(:path, path),
            shallow_prefix: options.fetch(:as, path)
          }

922
          path_scope(options.delete(:path) { path }) do
923 924
            scope(defaults.merge!(options)) { yield }
          end
925
        end
926

R
Ryan Bigg 已提交
927 928 929 930
        # === 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 已提交
931
        #   constraints(id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
932 933 934 935 936
        #     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.
937
        #
R
R.T. Lechow 已提交
938
        # You may use this to also restrict other parameters:
R
Ryan Bigg 已提交
939 940
        #
        #   resources :posts do
A
AvnerCohen 已提交
941
        #     constraints(post_id: /\d+\.\d+/) do
R
Ryan Bigg 已提交
942 943
        #       resources :comments
        #     end
J
James Miller 已提交
944
        #   end
R
Ryan Bigg 已提交
945 946 947 948 949
        #
        # === Restricting based on IP
        #
        # Routes can also be constrained to an IP or a certain range of IP addresses:
        #
A
AvnerCohen 已提交
950
        #   constraints(ip: /192\.168\.\d+\.\d+/) do
R
Ryan Bigg 已提交
951 952 953 954 955 956 957 958
        #     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 已提交
959
        # Requests to routes can be constrained based on specific criteria:
R
Ryan Bigg 已提交
960
        #
961
        #    constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
R
Ryan Bigg 已提交
962 963 964 965 966 967 968 969
        #      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
970
        #      def self.matches?(request)
R
Ryan Bigg 已提交
971 972 973 974 975 976 977 978 979 980 981
        #        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
982
        def constraints(constraints = {})
983
          scope(constraints: constraints) { yield }
984 985
        end

R
Ryan Bigg 已提交
986
        # Allows you to set default parameters for a route, such as this:
A
AvnerCohen 已提交
987 988
        #   defaults id: 'home' do
        #     match 'scoped_pages/(:id)', to: 'pages#show'
989
        #   end
R
Ryan Bigg 已提交
990
        # Using this, the +:id+ parameter here will default to 'home'.
991
        def defaults(defaults = {})
992 993 994 995
          @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
          yield
        ensure
          @scope = @scope.parent
996 997
        end

998
        private
A
Akira Matsuda 已提交
999
          def merge_path_scope(parent, child)
1000
            Mapper.normalize_path("#{parent}/#{child}")
1001 1002
          end

A
Akira Matsuda 已提交
1003
          def merge_shallow_path_scope(parent, child)
1004 1005 1006
            Mapper.normalize_path("#{parent}/#{child}")
          end

A
Akira Matsuda 已提交
1007
          def merge_as_scope(parent, child)
1008
            parent ? "#{parent}_#{child}" : child
1009 1010
          end

A
Akira Matsuda 已提交
1011
          def merge_shallow_prefix_scope(parent, child)
1012 1013 1014
            parent ? "#{parent}_#{child}" : child
          end

A
Akira Matsuda 已提交
1015
          def merge_module_scope(parent, child)
1016 1017 1018
            parent ? "#{parent}/#{child}" : child
          end

A
Akira Matsuda 已提交
1019
          def merge_controller_scope(parent, child)
1020
            child
1021 1022
          end

A
Akira Matsuda 已提交
1023
          def merge_action_scope(parent, child)
1024 1025 1026
            child
          end

A
Akira Matsuda 已提交
1027
          def merge_via_scope(parent, child)
1028 1029 1030
            child
          end

A
Akira Matsuda 已提交
1031
          def merge_format_scope(parent, child)
1032 1033 1034
            child
          end

A
Akira Matsuda 已提交
1035
          def merge_path_names_scope(parent, child)
1036 1037 1038
            merge_options_scope(parent, child)
          end

A
Akira Matsuda 已提交
1039
          def merge_constraints_scope(parent, child)
1040 1041 1042
            merge_options_scope(parent, child)
          end

A
Akira Matsuda 已提交
1043
          def merge_defaults_scope(parent, child)
1044 1045 1046
            merge_options_scope(parent, child)
          end

A
Akira Matsuda 已提交
1047
          def merge_blocks_scope(parent, child)
1048 1049 1050
            merged = parent ? parent.dup : []
            merged << child if child
            merged
1051 1052
          end

A
Akira Matsuda 已提交
1053
          def merge_options_scope(parent, child)
1054
            (parent || {}).merge(child)
1055
          end
1056

A
Akira Matsuda 已提交
1057
          def merge_shallow_scope(parent, child)
1058 1059
            child ? true : false
          end
1060 1061 1062 1063

          def merge_to_scope(parent, child)
            child
          end
1064 1065
      end

1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089
      # 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 已提交
1090 1091
      # <tt>app/controllers/admin</tt> directory, and you can group them together
      # in your router:
1092 1093 1094 1095 1096
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
S
Sebastian Martinez 已提交
1097 1098
      # 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
1099 1100
      # overrides this restriction, e.g:
      #
A
AvnerCohen 已提交
1101
      #   resources :articles, id: /[^\/]+/
1102
      #
S
Sebastian Martinez 已提交
1103
      # This allows any character other than a slash as part of your +:id+.
1104
      #
J
Joshua Peek 已提交
1105
      module Resources
1106 1107
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
1108
        VALID_ON_OPTIONS  = [:new, :collection, :member]
1109
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except, :param, :concerns]
1110
        CANONICAL_ACTIONS = %w(index create new show update destroy)
1111

1112
        class Resource #:nodoc:
1113
          attr_reader :controller, :path, :param
1114

1115
          def initialize(entities, api_only, shallow, options = {})
1116
            @name       = entities.to_s
1117 1118 1119
            @path       = (options[:path] || @name).to_s
            @controller = (options[:controller] || @name).to_s
            @as         = options[:as]
1120
            @param      = (options[:param] || :id).to_sym
1121
            @options    = options
1122
            @shallow    = shallow
1123
            @api_only   = api_only
1124 1125
            @only       = options.delete :only
            @except     = options.delete :except
1126 1127
          end

1128
          def default_actions
1129 1130 1131 1132 1133
            if @api_only
              [:index, :create, :show, :update, :destroy]
            else
              [:index, :create, :new, :show, :update, :destroy, :edit]
            end
1134 1135
          end

1136
          def actions
1137 1138 1139 1140
            if @only
              Array(@only).map(&:to_sym)
            elsif @except
              default_actions - Array(@except).map(&:to_sym)
1141 1142 1143 1144 1145
            else
              default_actions
            end
          end

1146
          def name
1147
            @as || @name
1148 1149
          end

1150
          def plural
1151
            @plural ||= name.to_s
1152 1153 1154
          end

          def singular
1155
            @singular ||= name.to_s.singularize
1156 1157
          end

1158
          alias :member_name :singular
1159

1160
          # Checks for uncountable plurals, and appends "_index" if the plural
1161
          # and singular form are the same.
1162
          def collection_name
1163
            singular == plural ? "#{plural}_index" : plural
1164 1165
          end

1166
          def resource_scope
A
Aaron Patterson 已提交
1167
            controller
1168 1169
          end

1170
          alias :collection_scope :path
1171 1172

          def member_scope
1173
            "#{path}/:#{param}"
1174 1175
          end

1176 1177
          alias :shallow_scope :member_scope

1178
          def new_scope(new_path)
1179
            "#{path}/#{new_path}"
1180 1181
          end

1182 1183 1184 1185
          def nested_param
            :"#{singular}_#{param}"
          end

1186
          def nested_scope
1187
            "#{path}/:#{nested_param}"
1188
          end
1189

1190 1191 1192
          def shallow?
            @shallow
          end
A
Aaron Patterson 已提交
1193 1194

          def singleton?; false; end
1195 1196 1197
        end

        class SingletonResource < Resource #:nodoc:
1198
          def initialize(entities, api_only, shallow, options)
1199
            super
1200
            @as         = nil
1201 1202
            @controller = (options[:controller] || plural).to_s
            @as         = options[:as]
1203 1204
          end

1205
          def default_actions
1206 1207 1208 1209 1210
            if @api_only
              [:show, :create, :update, :destroy]
            else
              [:show, :create, :update, :destroy, :new, :edit]
            end
1211 1212
          end

1213 1214
          def plural
            @plural ||= name.to_s.pluralize
1215 1216
          end

1217 1218
          def singular
            @singular ||= name.to_s
1219
          end
1220 1221 1222 1223 1224 1225

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
A
Aaron Patterson 已提交
1226 1227

          def singleton?; true; end
1228 1229
        end

1230 1231 1232 1233
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

1234 1235 1236 1237 1238 1239
        # 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:
        #
1240
        #   resource :profile
1241
        #
1242
        # This creates six different routes in your application, all mapping to
1243
        # the +Profiles+ controller (note that the controller is named after
1244 1245
        # the plural):
        #
1246 1247 1248 1249 1250
        #   GET       /profile/new
        #   GET       /profile
        #   GET       /profile/edit
        #   PATCH/PUT /profile
        #   DELETE    /profile
1251
        #   POST      /profile
1252
        #
1253
        # === Options
1254
        # Takes same options as +resources+.
J
Joshua Peek 已提交
1255
        def resource(*resources, &block)
1256
          options = resources.extract_options!.dup
J
Joshua Peek 已提交
1257

1258
          if apply_common_behavior_for(:resource, resources, options, &block)
1259 1260 1261
            return self
          end

1262
          with_scope_level(:resource) do
1263
            options = apply_action_options options
1264 1265
            resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
              yield if block_given?
1266

1267
              concerns(options[:concerns]) if options[:concerns]
1268

1269 1270 1271
              new do
                get :new
              end if parent_resource.actions.include?(:new)
1272

1273
              set_member_mappings_for_resource
1274 1275 1276 1277

              collection do
                post :create
              end if parent_resource.actions.include?(:create)
1278
            end
1279 1280
          end

J
Joshua Peek 已提交
1281
          self
1282 1283
        end

1284 1285 1286 1287 1288 1289 1290 1291
        # 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 已提交
1292
        # the +Photos+ controller:
1293
        #
1294 1295 1296 1297 1298
        #   GET       /photos
        #   GET       /photos/new
        #   POST      /photos
        #   GET       /photos/:id
        #   GET       /photos/:id/edit
1299
        #   PATCH/PUT /photos/:id
1300
        #   DELETE    /photos/:id
1301
        #
1302 1303 1304 1305 1306 1307 1308 1309
        # Resources can also be nested infinitely by using this block syntax:
        #
        #   resources :photos do
        #     resources :comments
        #   end
        #
        # This generates the following comments routes:
        #
1310 1311 1312 1313 1314
        #   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
1315
        #   PATCH/PUT /photos/:photo_id/comments/:id
1316
        #   DELETE    /photos/:photo_id/comments/:id
1317
        #
1318
        # === Options
1319 1320
        # Takes same options as <tt>Base#match</tt> as well as:
        #
1321
        # [:path_names]
A
Aviv Ben-Yosef 已提交
1322 1323
        #   Allows you to change the segment component of the +edit+ and +new+ actions.
        #   Actions not specified are not changed.
1324
        #
A
AvnerCohen 已提交
1325
        #     resources :posts, path_names: { new: "brand_new" }
1326
        #
1327
        #   The above example will now change /posts/new to /posts/brand_new.
1328
        #
1329 1330 1331
        # [:path]
        #   Allows you to change the path prefix for the resource.
        #
A
AvnerCohen 已提交
1332
        #     resources :posts, path: 'postings'
1333
        #
1334
        #   The resource and all segments will now route to /postings instead of /posts.
1335
        #
1336 1337
        # [:only]
        #   Only generate routes for the given actions.
1338
        #
A
AvnerCohen 已提交
1339 1340
        #     resources :cows, only: :show
        #     resources :cows, only: [:show, :index]
1341
        #
1342 1343
        # [:except]
        #   Generate all routes except for the given actions.
1344
        #
A
AvnerCohen 已提交
1345 1346
        #     resources :cows, except: :show
        #     resources :cows, except: [:show, :index]
1347 1348 1349 1350 1351
        #
        # [:shallow]
        #   Generates shallow routes for nested resource(s). When placed on a parent resource,
        #   generates shallow routes for all nested resources.
        #
A
AvnerCohen 已提交
1352
        #     resources :posts, shallow: true do
1353 1354 1355 1356 1357 1358
        #       resources :comments
        #     end
        #
        #   Is the same as:
        #
        #     resources :posts do
A
AvnerCohen 已提交
1359
        #       resources :comments, except: [:show, :edit, :update, :destroy]
1360
        #     end
A
AvnerCohen 已提交
1361
        #     resources :comments, only: [:show, :edit, :update, :destroy]
1362 1363 1364 1365
        #
        #   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>.
1366 1367 1368 1369
        #
        # [:shallow_path]
        #   Prefixes nested shallow routes with the specified path.
        #
A
AvnerCohen 已提交
1370
        #     scope shallow_path: "sekret" do
1371
        #       resources :posts do
A
AvnerCohen 已提交
1372
        #         resources :comments, shallow: true
1373
        #       end
1374 1375 1376 1377
        #     end
        #
        #   The +comments+ resource here will have the following routes generated for it:
        #
1378 1379 1380 1381 1382
        #     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)
1383
        #     comment          PATCH/PUT /sekret/comments/:id(.:format)
1384
        #     comment          DELETE    /sekret/comments/:id(.:format)
1385
        #
1386 1387 1388
        # [:shallow_prefix]
        #   Prefixes nested shallow route names with specified prefix.
        #
A
AvnerCohen 已提交
1389
        #     scope shallow_prefix: "sekret" do
1390
        #       resources :posts do
A
AvnerCohen 已提交
1391
        #         resources :comments, shallow: true
1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404
        #       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)
        #
1405
        # [:format]
1406
        #   Allows you to specify the default value for optional +format+
V
Vijay Dev 已提交
1407
        #   segment or disable it by supplying +false+.
1408
        #
1409
        # === Examples
1410
        #
S
Sebastian Martinez 已提交
1411
        #   # routes call <tt>Admin::PostsController</tt>
A
AvnerCohen 已提交
1412
        #   resources :posts, module: "admin"
1413
        #
1414
        #   # resource actions are at /admin/posts.
A
AvnerCohen 已提交
1415
        #   resources :posts, path: "admin/posts"
J
Joshua Peek 已提交
1416
        def resources(*resources, &block)
1417
          options = resources.extract_options!.dup
1418

1419
          if apply_common_behavior_for(:resources, resources, options, &block)
1420 1421 1422
            return self
          end

1423
          with_scope_level(:resources) do
1424
            options = apply_action_options options
1425 1426
            resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
              yield if block_given?
J
Joshua Peek 已提交
1427

1428
              concerns(options[:concerns]) if options[:concerns]
1429

1430 1431 1432 1433
              collection do
                get  :index if parent_resource.actions.include?(:index)
                post :create if parent_resource.actions.include?(:create)
              end
1434

1435 1436 1437
              new do
                get :new
              end if parent_resource.actions.include?(:new)
1438

1439 1440
              set_member_mappings_for_resource
            end
1441 1442
          end

J
Joshua Peek 已提交
1443
          self
1444 1445
        end

1446 1447 1448 1449 1450 1451 1452 1453 1454
        # 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 已提交
1455
        # with GET, and route to the search action of +PhotosController+. It will also
1456 1457
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1458
        def collection
1459 1460
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1461 1462
          end

1463
          with_scope_level(:collection) do
1464
            path_scope(parent_resource.collection_scope) do
1465 1466
              yield
            end
J
Joshua Peek 已提交
1467
          end
1468
        end
J
Joshua Peek 已提交
1469

1470 1471 1472 1473 1474 1475 1476 1477 1478
        # 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 已提交
1479
        # preview action of +PhotosController+. It will also create the
1480
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1481
        def member
1482 1483
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1484
          end
J
Joshua Peek 已提交
1485

1486
          with_scope_level(:member) do
1487
            if shallow?
1488 1489 1490
              shallow_scope {
                path_scope(parent_resource.member_scope) { yield }
              }
1491
            else
1492
              path_scope(parent_resource.member_scope) { yield }
1493
            end
1494 1495 1496 1497 1498 1499 1500
          end
        end

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

1502
          with_scope_level(:new) do
1503
            path_scope(parent_resource.new_scope(action_path(:new))) do
1504 1505
              yield
            end
J
Joshua Peek 已提交
1506
          end
J
Joshua Peek 已提交
1507 1508
        end

1509
        def nested
1510 1511
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1512 1513 1514
          end

          with_scope_level(:nested) do
1515
            if shallow? && shallow_nesting_depth >= 1
1516 1517 1518 1519 1520
              shallow_scope do
                path_scope(parent_resource.nested_scope) do
                  scope(nested_options) { yield }
                end
              end
1521
            else
1522 1523 1524
              path_scope(parent_resource.nested_scope) do
                scope(nested_options) { yield }
              end
1525 1526 1527 1528
            end
          end
        end

1529
        # See ActionDispatch::Routing::Mapper::Scoping#namespace.
1530
        def namespace(path, options = {})
1531
          if resource_scope?
1532 1533 1534 1535 1536 1537
            nested { super }
          else
            super
          end
        end

1538
        def shallow
1539 1540 1541 1542
          @scope = @scope.new(shallow: true)
          yield
        ensure
          @scope = @scope.parent
1543 1544
        end

1545
        def shallow?
A
Aaron Patterson 已提交
1546
          !parent_resource.singleton? && @scope[:shallow]
1547 1548
        end

1549
        # Matches a URL pattern to one or more routes.
1550 1551 1552 1553 1554
        # For more information, see match[rdoc-ref:Base#match].
        #
        #   match 'path' => 'controller#action', via: patch
        #   match 'path', to: 'controller#action', via: :post
        #   match 'path', 'otherpath', on: :member, via: :get
1555
        def match(path, *rest, &block)
1556 1557
          if rest.empty? && Hash === path
            options  = path
1558
            path, to = options.find { |name, _value| name.is_a?(String) }
1559

1560
            raise ArgumentError, "Route path not specified" if path.nil?
V
Volmer 已提交
1561

1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574
            case to
            when Symbol
              options[:action] = to
            when String
              if to =~ /#/
                options[:to] = to
              else
                options[:controller] = to
              end
            else
              options[:to] = to
            end

1575
            options.delete(path)
1576 1577 1578 1579 1580 1581
            paths = [path]
          else
            options = rest.pop || {}
            paths = [path] + rest
          end

1582 1583 1584 1585
          if options.key?(:defaults)
            defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
          else
            map_match(paths, options, &block)
A
Aaron Patterson 已提交
1586 1587 1588
          end
        end

1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602
        # You can specify what Rails should route "/" to with the root method:
        #
        #   root to: 'pages#main'
        #
        # For options, see +match+, as +root+ uses it internally.
        #
        # You can also pass a string which will expand
        #
        #   root 'pages#main'
        #
        # 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.
        def root(path, options = {})
1603 1604
          if path.is_a?(String)
            options[:to] = path
1605
          elsif path.is_a?(Hash) && options.empty?
1606 1607 1608 1609 1610
            options = path
          else
            raise ArgumentError, "must be called with a path and/or options"
          end

A
Aaron Patterson 已提交
1611
          if @scope.resources?
1612
            with_scope_level(:root) do
A
Aaron Patterson 已提交
1613
              path_scope(parent_resource.path) do
1614
                match_root_route(options)
1615 1616 1617
              end
            end
          else
1618
            match_root_route(options)
1619
          end
1620 1621
        end

1622
        private
1623

1624
          def parent_resource
1625 1626 1627
            @scope[:scope_level_resource]
          end

1628
          def apply_common_behavior_for(method, resources, options, &block)
1629 1630 1631 1632 1633
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1634 1635 1636 1637 1638 1639 1640
            if options.delete(:shallow)
              shallow do
                send(method, resources.pop, options, &block)
              end
              return true
            end

1641 1642 1643 1644 1645
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1646
            options.keys.each do |k|
1647 1648 1649
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1650 1651 1652
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1653 1654 1655 1656 1657 1658 1659 1660
                send(method, resources.pop, options, &block)
              end
              return true
            end

            false
          end

1661
          def apply_action_options(options)
1662 1663
            return options if action_options? options
            options.merge scope_action_options
1664 1665
          end

1666
          def action_options?(options)
1667
            options[:only] || options[:except]
1668 1669
          end

1670
          def scope_action_options
1671
            @scope[:action_options] || {}
1672 1673
          end

1674
          def resource_scope?
1675
            @scope.resource_scope?
1676 1677
          end

1678
          def resource_method_scope?
1679
            @scope.resource_method_scope?
1680 1681
          end

1682
          def nested_scope?
1683
            @scope.nested?
1684 1685
          end

1686
          def with_scope_level(kind) # :doc:
1687
            @scope = @scope.new_level(kind)
J
Joshua Peek 已提交
1688 1689
            yield
          ensure
A
Aaron Patterson 已提交
1690
            @scope = @scope.parent
J
Joshua Peek 已提交
1691
          end
1692

1693
          def resource_scope(resource)
1694
            @scope = @scope.new(scope_level_resource: resource)
1695

1696
            controller(resource.resource_scope) { yield }
1697
          ensure
A
Aaron Patterson 已提交
1698
            @scope = @scope.parent
1699 1700
          end

1701
          def nested_options
1702
            options = { as: parent_resource.member_name }
1703
            options[:constraints] = {
1704 1705
              parent_resource.nested_param => param_constraint
            } if param_constraint?
1706 1707

            options
1708 1709
          end

1710
          def shallow_nesting_depth
1711 1712 1713
            @scope.find_all { |node|
              node.frame[:scope_level_resource]
            }.count { |node| node.frame[:scope_level_resource].shallow? }
1714 1715
          end

1716
          def param_constraint?
1717
            @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1718 1719
          end

1720
          def param_constraint
1721
            @scope[:constraints][parent_resource.param]
1722 1723
          end

1724
          def canonical_action?(action)
1725
            resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1726 1727
          end

1728
          def shallow_scope
1729 1730
            scope = { as: @scope[:shallow_prefix],
                      path: @scope[:shallow_path] }
A
Aaron Patterson 已提交
1731
            @scope = @scope.new scope
1732

1733
            yield
1734
          ensure
A
Aaron Patterson 已提交
1735
            @scope = @scope.parent
1736 1737
          end

1738
          def path_for_action(action, path)
A
Aaron Patterson 已提交
1739 1740 1741
            return "#{@scope[:path]}/#{path}" if path

            if canonical_action?(action)
1742
              @scope[:path].to_s
1743
            else
A
Aaron Patterson 已提交
1744
              "#{@scope[:path]}/#{action_path(action)}"
1745 1746 1747
            end
          end

1748
          def action_path(name)
1749
            @scope[:path_names][name.to_sym] || name
1750 1751
          end

1752
          def prefix_name_for_action(as, action)
1753
            if as
1754
              prefix = as
1755
            elsif !canonical_action?(action)
1756
              prefix = action
1757
            end
A
Aaron Patterson 已提交
1758

1759 1760
            if prefix && prefix != "/" && !prefix.empty?
              Mapper.normalize_name prefix.to_s.tr("-", "_")
A
Aaron Patterson 已提交
1761
            end
1762 1763
          end

1764
          def name_for_action(as, action)
1765
            prefix = prefix_name_for_action(as, action)
1766 1767 1768
            name_prefix = @scope[:as]

            if parent_resource
1769
              return nil unless as || action
1770

1771 1772
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1773
            end
1774

R
rono23 已提交
1775
            action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1776
            candidate = action_name.select(&:present?).join("_")
1777

R
rono23 已提交
1778
            unless candidate.empty?
1779 1780 1781 1782
              # 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?
1783
                candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
1784 1785 1786 1787
              else
                candidate
              end
            end
1788
          end
1789

1790
          def set_member_mappings_for_resource # :doc:
1791 1792 1793 1794 1795 1796 1797 1798 1799 1800
            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
1801

1802
          def api_only? # :doc:
1803 1804
            @set.api_only?
          end
1805

1806 1807 1808 1809 1810 1811
          def path_scope(path)
            @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
            yield
          ensure
            @scope = @scope.parent
          end
1812

1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839
          def map_match(paths, options)
            if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
              raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
            end

            if @scope[:to]
              options[:to] ||= @scope[:to]
            end

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

            controller = options.delete(:controller) || @scope[:controller]
            option_path = options.delete :path
            to = options.delete :to
            via = Mapping.check_via Array(options.delete(:via) {
              @scope[:via]
            })
            formatted = options.delete(:format) { @scope[:format] }
            anchor = options.delete(:anchor) { true }
            options_constraints = options.delete(:constraints) || {}

            path_types = paths.group_by(&:class)
            path_types.fetch(String, []).each do |_path|
              route_options = options.dup
              if _path && option_path
1840
                raise ArgumentError, "Ambiguous route definition. Both :path and the route path where specified as strings."
1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853
              end
              to = get_to_from_path(_path, to, route_options[:action])
              decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
            end

            path_types.fetch(Symbol, []).each do |action|
              route_options = options.dup
              decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
            end

            self
          end

1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868
          def get_to_from_path(path, to, action)
            return to if to || action

            path_without_format = path.sub(/\(\.:format\)$/, "")
            if using_match_shorthand?(path_without_format)
              path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
            else
              nil
            end
          end

          def using_match_shorthand?(path)
            path =~ %r{^/?[-\w]+/[-\w/]+$}
          end

A
Akira Matsuda 已提交
1869
          def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883
            if on = options.delete(:on)
              send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
            else
              case @scope.scope_level
              when :resources
                nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
              when :resource
                member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
              else
                add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
              end
            end
          end

A
Akira Matsuda 已提交
1884
          def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907
            path = path_for_action(action, _path)
            raise ArgumentError, "path is required" if path.blank?

            action = action.to_s

            default_action = options.delete(:action) || @scope[:action]

            if action =~ /^[\w\-\/]+$/
              default_action ||= action.tr("-", "_") unless action.include?("/")
            else
              action = nil
            end

            as = if !options.fetch(:as, true) # if it's set to nil or false
              options.delete(:as)
            else
              name_for_action(options.delete(:as), action)
            end

            path = Mapping.normalize_path URI.parser.escape(path), formatted
            ast = Journey::Parser.parse path

            mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
J
Jerry Tao 已提交
1908
            @set.add_route(mapping, as)
1909 1910
          end

1911
          def match_root_route(options)
R
Ryo Hashimoto 已提交
1912
            name = has_named_route?(name_for_action(:root, nil)) ? nil : :root
1913
            args = ["/", { as: name, via: :get }.merge!(options)]
1914

1915
            match(*args)
1916
          end
J
Joshua Peek 已提交
1917
      end
J
Joshua Peek 已提交
1918

1919
      # Routing Concerns allow you to declare common routes that can be reused
1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938
      # 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
1939
      module Concerns
1940
        # Define a routing concern using a name.
1941
        #
1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963
        # 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]
1964 1965
        #   end
        #
1966 1967 1968
        # Or, using a callable object, you might implement something more
        # specific to your application, which would be out of place in your
        # routes file.
1969
        #
1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980
        #   # 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]
1981 1982 1983
        #     end
        #   end
        #
1984 1985 1986 1987 1988 1989 1990 1991
        #   # routes.rb
        #   concern :purchasable, Purchasable.new(returnable: true)
        #
        #   resources :toys, concerns: :purchasable
        #   resources :electronics, concerns: :purchasable
        #   resources :pets do
        #     concerns :purchasable, returnable: false
        #   end
1992
        #
1993 1994 1995
        # 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>.
1996
        def concern(name, callable = nil, &block)
1997 1998
          callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
          @concerns[name] = callable
1999 2000
        end

2001 2002 2003 2004 2005 2006
        # Use the named concerns
        #
        #   resources :posts do
        #     concerns :commentable
        #   end
        #
2007
        # Concerns also work in any routes helper that you want to use:
2008 2009 2010 2011
        #
        #   namespace :posts do
        #     concerns :commentable
        #   end
2012 2013 2014
        def concerns(*args)
          options = args.extract_options!
          args.flatten.each do |name|
2015
            if concern = @concerns[name]
2016
              concern.call(self, options)
2017 2018 2019 2020 2021 2022 2023
            else
              raise ArgumentError, "No concern named #{name} was found!"
            end
          end
        end
      end

A
Andrew White 已提交
2024 2025
      module CustomUrls
        # Define custom url helpers that will be added to the application's
2026
        # routes. This allows you to override and/or replace the default behavior
2027 2028
        # of routing helpers, e.g:
        #
A
Andrew White 已提交
2029
        #   direct :homepage do
2030 2031 2032
        #     "http://www.rubyonrails.org"
        #   end
        #
A
Andrew White 已提交
2033
        #   direct :commentable do |model|
2034 2035 2036
        #     [ model, anchor: model.dom_id ]
        #   end
        #
A
Andrew White 已提交
2037
        #   direct :main do
2038
        #     { controller: "pages", action: "index", subdomain: "www" }
2039 2040
        #   end
        #
2041 2042
        # The return value from the block passed to +direct+ must be a valid set of
        # arguments for +url_for+ which will actually build the URL string. This can
A
Andrew White 已提交
2043 2044
        # be one of the following:
        #
2045
        #   * A string, which is treated as a generated URL
2046
        #   * A hash, e.g. { controller: "pages", action: "index" }
A
Andrew White 已提交
2047 2048 2049 2050
        #   * An array, which is passed to `polymorphic_url`
        #   * An Active Model instance
        #   * An Active Model class
        #
2051 2052
        # NOTE: Other URL helpers can be called in the block but be careful not to invoke
        # your custom URL helper again otherwise it will result in a stack overflow error.
A
Andrew White 已提交
2053 2054
        #
        # You can also specify default options that will be passed through to
2055
        # your URL helper definition, e.g:
A
Andrew White 已提交
2056 2057
        #
        #   direct :browse, page: 1, size: 10 do |options|
2058
        #     [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
A
Andrew White 已提交
2059 2060
        #   end
        #
2061
        # In this instance the +params+ object comes from the context in which the the
2062
        # block is executed, e.g. generating a URL inside a controller action or a view.
A
Andrew White 已提交
2063 2064 2065 2066
        # If the block is executed where there isn't a params object such as this:
        #
        #   Rails.application.routes.url_helpers.browse_path
        #
2067
        # then it will raise a +NameError+. Because of this you need to be aware of the
2068
        # context in which you will use your custom URL helper when defining it.
A
Andrew White 已提交
2069
        #
2070 2071
        # NOTE: The +direct+ method can't be used inside of a scope block such as
        # +namespace+ or +scope+ and will raise an error if it detects that it is.
A
Andrew White 已提交
2072 2073 2074 2075 2076 2077 2078 2079
        def direct(name, options = {}, &block)
          unless @scope.root?
            raise RuntimeError, "The direct method can't be used inside a routes scope block"
          end

          @set.add_url_helper(name, options, &block)
        end

2080
        # Define custom polymorphic mappings of models to URLs. This alters the
2081 2082
        # behavior of +polymorphic_url+ and consequently the behavior of
        # +link_to+ and +form_for+ when passed a model instance, e.g:
A
Andrew White 已提交
2083
        #
A
Andrew White 已提交
2084 2085 2086
        #   resource :basket
        #
        #   resolve "Basket" do
A
Andrew White 已提交
2087 2088 2089
        #     [:basket]
        #   end
        #
2090 2091
        # This will now generate "/basket" when a +Basket+ instance is passed to
        # +link_to+ or +form_for+ instead of the standard "/baskets/:id".
A
Andrew White 已提交
2092
        #
2093
        # NOTE: This custom behavior only applies to simple polymorphic URLs where
A
Andrew White 已提交
2094 2095 2096 2097 2098 2099 2100 2101
        # a single model instance is passed and not more complicated forms, e.g:
        #
        #   # config/routes.rb
        #   resource :profile
        #   namespace :admin do
        #     resources :users
        #   end
        #
A
Andrew White 已提交
2102
        #   resolve("User") { [:profile] }
A
Andrew White 已提交
2103 2104
        #
        #   # app/views/application/_menu.html.erb
2105 2106
        #   link_to "Profile", @current_user
        #   link_to "Profile", [:admin, @current_user]
A
Andrew White 已提交
2107
        #
2108
        # The first +link_to+ will generate "/profile" but the second will generate
2109
        # the standard polymorphic URL of "/admin/users/1".
A
Andrew White 已提交
2110
        #
A
Andrew White 已提交
2111
        # You can pass options to a polymorphic mapping - the arity for the block
A
Andrew White 已提交
2112 2113
        # needs to be two as the instance is passed as the first argument, e.g:
        #
2114
        #   resolve "Basket", anchor: "items" do |basket, options|
A
Andrew White 已提交
2115 2116 2117
        #     [:basket, options]
        #   end
        #
2118
        # This generates the URL "/basket#items" because when the last item in an
2119
        # array passed to +polymorphic_url+ is a hash then it's treated as options
2120
        # to the URL helper that gets called.
A
Andrew White 已提交
2121
        #
2122 2123
        # NOTE: The +resolve+ method can't be used inside of a scope block such as
        # +namespace+ or +scope+ and will raise an error if it detects that it is.
A
Andrew White 已提交
2124
        def resolve(*args, &block)
2125
          unless @scope.root?
A
Andrew White 已提交
2126
            raise RuntimeError, "The resolve method can't be used inside a routes scope block"
2127 2128
          end

A
Andrew White 已提交
2129 2130 2131 2132 2133
          options = args.extract_options!
          args = args.flatten(1)

          args.each do |klass|
            @set.add_polymorphic_mapping(klass, options, &block)
2134
          end
2135 2136 2137
        end
      end

A
Aaron Patterson 已提交
2138
      class Scope # :nodoc:
2139 2140
        OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
                   :controller, :action, :path_names, :constraints,
2141
                   :shallow, :blocks, :defaults, :via, :format, :options, :to]
2142

2143
        RESOURCE_SCOPES = [:resource, :resources]
2144
        RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
2145

A
Aaron Patterson 已提交
2146
        attr_reader :parent, :scope_level
A
Aaron Patterson 已提交
2147

A
Aaron Patterson 已提交
2148
        def initialize(hash, parent = NULL, scope_level = nil)
A
Aaron Patterson 已提交
2149 2150
          @hash = hash
          @parent = parent
2151
          @scope_level = scope_level
A
Aaron Patterson 已提交
2152 2153
        end

2154 2155 2156 2157
        def nested?
          scope_level == :nested
        end

2158 2159 2160 2161 2162 2163 2164 2165
        def null?
          @hash.nil? && @parent.nil?
        end

        def root?
          @parent.null?
        end

A
Aaron Patterson 已提交
2166 2167 2168 2169
        def resources?
          scope_level == :resources
        end

2170 2171 2172 2173
        def resource_method_scope?
          RESOURCE_METHOD_SCOPES.include? scope_level
        end

2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190
        def action_name(name_prefix, prefix, collection_name, member_name)
          case scope_level
          when :nested
            [name_prefix, prefix]
          when :collection
            [prefix, name_prefix, collection_name]
          when :new
            [prefix, :new, name_prefix, member_name]
          when :member
            [prefix, name_prefix, member_name]
          when :root
            [name_prefix, collection_name, prefix]
          else
            [name_prefix, member_name, prefix]
          end
        end

2191 2192 2193 2194
        def resource_scope?
          RESOURCE_SCOPES.include? scope_level
        end

2195 2196 2197 2198
        def options
          OPTIONS
        end

A
Aaron Patterson 已提交
2199
        def new(hash)
2200
          self.class.new hash, self, scope_level
A
Aaron Patterson 已提交
2201 2202
        end

2203
        def new_level(level)
2204
          self.class.new(frame, self, level)
2205 2206
        end

A
Aaron Patterson 已提交
2207
        def [](key)
2208 2209
          scope = find { |node| node.frame.key? key }
          scope && scope.frame[key]
A
Aaron Patterson 已提交
2210
        end
A
Aaron Patterson 已提交
2211 2212 2213 2214 2215

        include Enumerable

        def each
          node = self
M
Molchanov Andrey 已提交
2216
          until node.equal? NULL
2217
            yield node
A
Aaron Patterson 已提交
2218 2219 2220 2221 2222 2223
            node = node.parent
          end
        end

        def frame; @hash; end

2224
        NULL = Scope.new(nil, nil)
A
Aaron Patterson 已提交
2225 2226
      end

2227 2228
      def initialize(set) #:nodoc:
        @set = set
2229
        @scope = Scope.new(path_names: @set.resources_path_names)
2230
        @concerns = {}
2231 2232
      end

2233 2234
      include Base
      include HttpHelpers
2235
      include Redirection
2236
      include Scoping
2237
      include Concerns
2238
      include Resources
A
Andrew White 已提交
2239
      include CustomUrls
J
Joshua Peek 已提交
2240 2241
    end
  end
J
Joshua Peek 已提交
2242
end