mapper.rb 43.9 KB
Newer Older
1
require 'erb'
2
require 'active_support/core_ext/hash/except'
3
require 'active_support/core_ext/object/blank'
4
require 'active_support/inflector'
5
require 'action_dispatch/routing/redirection'
6

J
Joshua Peek 已提交
7 8
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
9
    class Mapper
10
      class Constraints #:nodoc:
11
        def self.new(app, constraints, request = Rack::Request)
12
          if constraints.any?
13
            super(app, constraints, request)
14 15 16 17 18
          else
            app
          end
        end

19 20
        attr_reader :app

21 22
        def initialize(app, constraints, request)
          @app, @constraints, @request = app, constraints, request
23 24 25
        end

        def call(env)
26
          req = @request.new(env)
27 28 29

          @constraints.each { |constraint|
            if constraint.respond_to?(:matches?) && !constraint.matches?(req)
J
Joshua Peek 已提交
30
              return [ 404, {'X-Cascade' => 'pass'}, [] ]
31
            elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req))
J
Joshua Peek 已提交
32
              return [ 404, {'X-Cascade' => 'pass'}, [] ]
33 34 35 36 37
            end
          }

          @app.call(env)
        end
38 39 40 41 42

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

45
      class Mapping #:nodoc:
46
        IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
47

48
        def initialize(set, scope, path, options)
49 50
          @set, @scope = set, scope
          @options = (@scope[:options] || {}).merge(options)
51
          @path = normalize_path(path)
52
          normalize_options!
53
        end
J
Joshua Peek 已提交
54

55
        def to_route
56
          [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
57
        end
J
Joshua Peek 已提交
58

59
        private
60 61 62

          def normalize_options!
            path_without_format = @path.sub(/\(\.:format\)$/, '')
63

64 65 66
            if using_match_shorthand?(path_without_format, @options)
              to_shorthand    = @options[:to].blank?
              @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1')
67 68
            end

69
            @options.merge!(default_controller_and_action(to_shorthand))
70 71 72 73 74 75 76 77 78 79 80 81

            requirements.each do |name, requirement|
              # segment_keys.include?(k.to_s) || k == :controller
              next unless Regexp === requirement && !constraints[name]

              if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
                raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
              end
              if requirement.multiline?
                raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
              end
            end
82
          end
83

84
          # match "account/overview"
85
          def using_match_shorthand?(path, options)
86
            path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w\/]+$}
87
          end
88

89
          def normalize_path(path)
90 91
            raise ArgumentError, "path is required" if path.blank?
            path = Mapper.normalize_path(path)
92 93 94 95 96 97 98 99 100 101 102

            if path.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.reverse_merge!(:controller => /.+?/)
            end

103 104 105 106
            if @options[:format] == false
              @options.delete(:format)
              path
            elsif path.include?(":format")
107 108 109 110
              path
            else
              "#{path}(.:format)"
            end
111
          end
112

113 114
          def app
            Constraints.new(
115
              to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
116 117
              blocks,
              @set.request_class
118
            )
119 120
          end

121 122 123
          def conditions
            { :path_info => @path }.merge(constraints).merge(request_method_condition)
          end
J
Joshua Peek 已提交
124

125
          def requirements
126
            @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
127 128 129 130
              requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
              @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
            end
          end
131

132
          def defaults
133 134 135 136 137 138
            @defaults ||= (@options[:defaults] || {}).tap do |defaults|
              defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults]
              @options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) }
            end
          end

139
          def default_controller_and_action(to_shorthand=nil)
140
            if to.respond_to?(:call)
141 142
              { }
            else
143
              if to.is_a?(String)
144
                controller, action = to.split('#')
145 146
              elsif to.is_a?(Symbol)
                action = to.to_s
147
              end
J
Joshua Peek 已提交
148

149 150
              controller ||= default_controller
              action     ||= default_action
151

152 153 154
              unless controller.is_a?(Regexp) || to_shorthand
                controller = [@scope[:module], controller].compact.join("/").presence
              end
155

156 157 158 159
              if controller.is_a?(String) && controller =~ %r{\A/}
                raise ArgumentError, "controller name should not start with a slash"
              end

160 161
              controller = controller.to_s unless controller.is_a?(Regexp)
              action     = action.to_s     unless action.is_a?(Regexp)
162

163
              if controller.blank? && segment_keys.exclude?("controller")
164 165
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
166

167
              if action.blank? && segment_keys.exclude?("action")
168 169
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
170

A
Aaron Patterson 已提交
171
              hash = {}
A
Aaron Patterson 已提交
172 173
              hash[:controller] = controller unless controller.blank?
              hash[:action]     = action unless action.blank?
A
Aaron Patterson 已提交
174
              hash
175 176
            end
          end
177

178
          def blocks
A
Aaron Patterson 已提交
179 180
            block = @scope[:blocks] || []

181
            if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
A
Aaron Patterson 已提交
182
              block << @options[:constraints]
183
            end
J
Joshua Peek 已提交
184

A
Aaron Patterson 已提交
185
            block
186
          end
J
Joshua Peek 已提交
187

188 189 190
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
191

192 193
          def request_method_condition
            if via = @options[:via]
194 195
              via = Array(via).map { |m| m.to_s.dasherize.upcase }
              { :request_method => %r[^#{via.join('|')}$] }
196 197
            else
              { }
198
            end
199
          end
J
Joshua Peek 已提交
200

201 202
          def segment_keys
            @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
203 204
              Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
            ).names
205
          end
206

207 208 209
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
210

211
          def default_controller
212
            if @options[:controller]
213
              @options[:controller]
214
            elsif @scope[:controller]
215
              @scope[:controller]
216
            end
217
          end
218 219 220

          def default_action
            if @options[:action]
221
              @options[:action]
222 223
            elsif @scope[:action]
              @scope[:action]
224 225
            end
          end
226
      end
227

228
      # Invokes Rack::Mount::Utils.normalize path and ensure that
229 230
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
231 232
      def self.normalize_path(path)
        path = Rack::Mount::Utils.normalize_path(path)
233
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$}
234 235 236
        path
      end

237 238 239 240
      def self.normalize_name(name)
        normalize_path(name)[1..-1].gsub("/", "_")
      end

241
      module Base
242
        def initialize(set) #:nodoc:
243 244
          @set = set
        end
245

246 247 248 249 250
        # You can specify what Rails should route "/" to with the root method:
        #
        #   root :to => 'pages#main'
        #
        # You should put the root route at the end of <tt>config/routes.rb</tt>.
251 252 253
        def root(options = {})
          match '/', options.reverse_merge(:as => :root)
        end
254

255 256 257 258 259 260 261 262 263
        # When you set up a regular route, you supply a series of symbols that
        # Rails maps to parts of an incoming HTTP request.
        #
        #   match ':controller/:action/:id/:user_id'
        #
        # Two of these symbols are special: :controller maps to the name of a
        # controller in your application, and :action maps to the name of an
        # action within that controller. Anything other than :controller or
        # :action will be available to the action as part of params.
264 265
        def match(path, options=nil)
          mapping = Mapping.new(@set, @scope, path, options || {}).to_route
266
          @set.add_route(*mapping)
267 268
          self
        end
269

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
        # Mount a Rack-based application to be used within the application.
        #
        # mount SomeRackApp, :at => "some_route"
        #
        # Alternatively:
        #
        # mount(SomeRackApp => "some_route")
        #
        # 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:
        #
        # mount(SomeRackApp => "some_route", :as => "exciting")
        #
        # This will generate the +exciting_path+ and +exciting_url+ helpers
        # which can be used to navigate to this mounted app.
287 288 289 290 291 292 293 294 295 296 297
        def mount(app, options = nil)
          if options
            path = options.delete(:at)
          else
            options = app
            app, path = options.find { |k, v| k.respond_to?(:call) }
            options.delete(app) if app
          end

          raise "A rack application must be specified" unless path

298 299
          options[:as] ||= app_name(app)

300
          match(path, options.merge(:to => app, :anchor => false, :format => false))
301 302

          define_generate_prefix(app, options[:as])
303 304 305
          self
        end

306 307 308 309
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
310

311 312 313 314 315 316
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

317 318 319
        private
          def app_name(app)
            return unless app.respond_to?(:routes)
320 321 322 323 324 325 326

            if app.respond_to?(:railtie_name)
              app.railtie_name
            else
              class_name = app.class.is_a?(Class) ? app.name : app.class.name
              ActiveSupport::Inflector.underscore(class_name).gsub("/", "_")
            end
327 328 329 330 331 332
          end

          def define_generate_prefix(app, name)
            return unless app.respond_to?(:routes)

            _route = @set.named_routes.routes[name.to_sym]
P
Piotr Sarnacki 已提交
333 334
            _routes = @set
            app.routes.define_mounted_helper(name)
335 336
            app.routes.class_eval do
              define_method :_generate_prefix do |options|
P
Piotr Sarnacki 已提交
337
                prefix_options = options.slice(*_route.segment_keys)
338 339
                # we must actually delete prefix segment keys to avoid passing them to next url_for
                _route.segment_keys.each { |k| options.delete(k) }
P
Piotr Sarnacki 已提交
340
                _routes.url_helpers.send("#{name}_path", prefix_options)
341 342 343
              end
            end
          end
344 345 346
      end

      module HttpHelpers
347
        # Define a route that only recognizes HTTP GET.
348 349 350 351 352
        # For supported arguments, see +match+.
        #
        # Example:
        #
        # get 'bacon', :to => 'food#bacon'
353 354 355 356
        def get(*args, &block)
          map_method(:get, *args, &block)
        end

357
        # Define a route that only recognizes HTTP POST.
358 359 360 361 362
        # For supported arguments, see +match+.
        #
        # Example:
        #
        # post 'bacon', :to => 'food#bacon'
363 364 365 366
        def post(*args, &block)
          map_method(:post, *args, &block)
        end

367
        # Define a route that only recognizes HTTP PUT.
368 369 370 371 372
        # For supported arguments, see +match+.
        #
        # Example:
        #
        # put 'bacon', :to => 'food#bacon'
373 374 375 376
        def put(*args, &block)
          map_method(:put, *args, &block)
        end

377 378 379 380 381 382
        # Define a route that only recognizes HTTP PUT.
        # For supported arguments, see +match+.
        #
        # Example:
        #
        # delete 'broccoli', :to => 'food#broccoli'
383 384 385 386 387 388 389 390 391 392 393 394 395 396
        def delete(*args, &block)
          map_method(:delete, *args, &block)
        end

        private
          def map_method(method, *args, &block)
            options = args.extract_options!
            options[:via] = method
            args.push(options)
            match(*args, &block)
            self
          end
      end

397 398 399 400 401 402 403 404 405
      # 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 app/controllers/admin directory, and you can group them together
      # in your router:
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
406
      #
407 408
      # This will create a number of routes for each of the posts and comments
      # controller. For Admin::PostsController, Rails will create:
409
      #
410 411 412 413 414 415 416
      #   GET	    /admin/posts
      #   GET	    /admin/posts/new
      #   POST	  /admin/posts
      #   GET	    /admin/posts/1
      #   GET	    /admin/posts/1/edit
      #   PUT	    /admin/posts/1
      #   DELETE  /admin/posts/1
417
      #
418
      # If you want to route /posts (without the prefix /admin) to
419
      # Admin::PostsController, you could use
420
      #
421
      #   scope :module => "admin" do
422
      #     resources :posts
423 424 425
      #   end
      #
      # or, for a single case
426
      #
427
      #   resources :posts, :module => "admin"
428
      #
429
      # If you want to route /admin/posts to PostsController
430
      # (without the Admin:: module prefix), you could use
431
      #
432
      #   scope "/admin" do
433
      #     resources :posts
434 435 436
      #   end
      #
      # or, for a single case
437
      #
438 439 440 441 442
      #   resources :posts, :path => "/admin"
      #
      # 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
      # PostsController:
443
      #
444 445 446 447 448 449 450
      #   GET	    /admin/posts
      #   GET	    /admin/posts/new
      #   POST	  /admin/posts
      #   GET	    /admin/posts/1
      #   GET	    /admin/posts/1/edit
      #   PUT	    /admin/posts/1
      #   DELETE  /admin/posts/1
451
      module Scoping
452
        def initialize(*args) #:nodoc:
453 454 455 456
          @scope = {}
          super
        end

457 458 459 460
        # === Supported options
        # [:module]
        #   If you want to route /posts (without the prefix /admin) to
        #   Admin::PostsController, you could use
461
        #
462 463 464
        #     scope :module => "admin" do
        #       resources :posts
        #     end
465
        #
466
        # [:path]
467
        #   If you want to prefix the route, you could use
468
        #
469 470 471
        #     scope :path => "/admin" do
        #       resources :posts
        #     end
472
        #
R
Ryan Bigg 已提交
473
        #   This will prefix all of the +posts+ resource's requests with '/admin'
474 475 476 477
        #
        # [:as]
        #  Prefixes the routing helpers in this scope with the specified label.
        #
R
Ryan Bigg 已提交
478 479 480
        #    scope :as => "sekret" do
        #      resources :posts
        #    end
481 482
        #
        # Helpers such as +posts_path+ will now be +sekret_posts_path+
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
        #
        # [:shallow_path]
        #
        #   Prefixes nested shallow routes with the specified path.
        #
        #   scope :shallow_path => "sekret" do
        #     resources :posts do
        #       resources :comments, :shallow => true
        #     end
        #
        #   The +comments+ resource here will have the following routes generated for it:
        #
        #     post_comments    GET    /sekret/posts/:post_id/comments(.:format)
        #     post_comments    POST   /sekret/posts/:post_id/comments(.:format)
        #     new_post_comment GET    /sekret/posts/:post_id/comments/new(.:format)
        #     edit_comment     GET    /sekret/comments/:id/edit(.:format)
        #     comment          GET    /sekret/comments/:id(.:format)
        #     comment          PUT    /sekret/comments/:id(.:format)
        #     comment          DELETE /sekret/comments/:id(.:format)
502 503
        def scope(*args)
          options = args.extract_options!
504
          options = options.dup
505

506
          options[:path] = args.first if args.first.is_a?(String)
507
          recover = {}
508

509 510 511
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
            block, options[:constraints] = options[:constraints], {}
512
          end
513

514 515 516 517 518
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
519 520
          end

521 522
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
523

524 525
          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)
526 527 528 529

          yield
          self
        ensure
530 531 532 533 534 535
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
536 537
        end

538 539 540 541 542 543
        # Scopes routes to a specific controller
        #
        # Example:
        #   controller "food" do
        #     match "bacon", :action => "bacon"
        #   end
544 545 546
        def controller(controller, options={})
          options[:controller] = controller
          scope(options) { yield }
547 548
        end

549 550 551 552 553 554 555 556
        # Scopes routes to a specific namespace. For example:
        #
        #   namespace :admin do
        #     resources :posts
        #   end
        #
        # This generates the following routes:
        #
557 558 559 560 561 562 563
        #      admin_posts GET    /admin/posts(.:format)          {:action=>"index", :controller=>"admin/posts"}
        #      admin_posts POST   /admin/posts(.:format)          {:action=>"create", :controller=>"admin/posts"}
        #   new_admin_post GET    /admin/posts/new(.:format)      {:action=>"new", :controller=>"admin/posts"}
        #  edit_admin_post GET    /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"}
        #       admin_post GET    /admin/posts/:id(.:format)      {:action=>"show", :controller=>"admin/posts"}
        #       admin_post PUT    /admin/posts/:id(.:format)      {:action=>"update", :controller=>"admin/posts"}
        #       admin_post DELETE /admin/posts/:id(.:format)      {:action=>"destroy", :controller=>"admin/posts"}
564 565 566 567 568 569 570 571 572 573 574
        # === Supported options
        #
        # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ all default to the name of the namespace.
        #
        # [:path]
        #   The path prefix for the routes.
        #
        #   namespace :admin, :path => "sekret" do
        #     resources :posts
        #   end
        #
575 576 577 578 579 580 581 582 583 584 585 586 587 588 589
        #   All routes for the above +resources+ will be accessible through +/sekret/posts+, rather than +/admin/posts+
        #
        # [:module]
        #   The namespace for the controllers.
        #
        #   namespace :admin, :module => "sekret" do
        #     resources :posts
        #   end
        #
        #   The +PostsController+ here should go in the +Sekret+ namespace and so it should be defined like this:
        #
        #   class Sekret::PostsController < ApplicationController
        #     # code go here
        #   end
        #
590
        # [:as]
591
        #   Changes the name used in routing helpers for this namespace.
592
        #
593 594 595
        #     namespace :admin, :as => "sekret" do
        #       resources :posts
        #     end
596 597
        #
        # Routing helpers such as +admin_posts_path+ will now be +sekret_posts_path+.
598 599 600
        #
        # [:shallow_path]
        #   See the +scope+ method.
601
        def namespace(path, options = {})
602
          path = path.to_s
603 604 605
          options = { :path => path, :as => path, :module => path,
                      :shallow_path => path, :shallow_prefix => path }.merge!(options)
          scope(options) { yield }
606
        end
607

R
Ryan Bigg 已提交
608 609 610 611 612 613 614 615 616 617
        # === 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:
        #
        #   constraints(:id => /\d+\.\d+) do
        #     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.
618
        #
R
Ryan Bigg 已提交
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
        # You may use this to also resrict other parameters:
        #
        #   resources :posts do
        #     constraints(:post_id => /\d+\.\d+) do
        #       resources :comments
        #     end
        #
        # === Restricting based on IP
        #
        # Routes can also be constrained to an IP or a certain range of IP addresses:
        #
        #   constraints(:ip => /192.168.\d+.\d+/) do
        #     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
        #
        # Requests to routes can be constrained based on specific critera:
        #
        #    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
        #      def self.matches(request)
        #        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
662 663 664 665
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

R
Ryan Bigg 已提交
666 667 668 669 670
        # Allows you to set default parameters for a route, such as this:
        # defaults :id => 'home' do
        #   match 'scoped_pages/(:id)', :to => 'pages#show'
        # end
        # Using this, the +:id+ parameter here will default to 'home'.
671 672 673 674
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

675
        private
J
José Valim 已提交
676
          def scope_options #:nodoc:
677 678 679
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

J
José Valim 已提交
680
          def merge_path_scope(parent, child) #:nodoc:
681
            Mapper.normalize_path("#{parent}/#{child}")
682 683
          end

J
José Valim 已提交
684
          def merge_shallow_path_scope(parent, child) #:nodoc:
685 686 687
            Mapper.normalize_path("#{parent}/#{child}")
          end

J
José Valim 已提交
688
          def merge_as_scope(parent, child) #:nodoc:
689
            parent ? "#{parent}_#{child}" : child
690 691
          end

J
José Valim 已提交
692
          def merge_shallow_prefix_scope(parent, child) #:nodoc:
693 694 695
            parent ? "#{parent}_#{child}" : child
          end

J
José Valim 已提交
696
          def merge_module_scope(parent, child) #:nodoc:
697 698 699
            parent ? "#{parent}/#{child}" : child
          end

J
José Valim 已提交
700
          def merge_controller_scope(parent, child) #:nodoc:
701
            child
702 703
          end

J
José Valim 已提交
704
          def merge_path_names_scope(parent, child) #:nodoc:
705 706 707
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
708
          def merge_constraints_scope(parent, child) #:nodoc:
709 710 711
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
712
          def merge_defaults_scope(parent, child) #:nodoc:
713 714 715
            merge_options_scope(parent, child)
          end

J
José Valim 已提交
716
          def merge_blocks_scope(parent, child) #:nodoc:
717 718 719
            merged = parent ? parent.dup : []
            merged << child if child
            merged
720 721
          end

J
José Valim 已提交
722
          def merge_options_scope(parent, child) #:nodoc:
723
            (parent || {}).except(*override_keys(child)).merge(child)
724
          end
725

J
José Valim 已提交
726
          def merge_shallow_scope(parent, child) #:nodoc:
727 728
            child ? true : false
          end
729

J
José Valim 已提交
730
          def override_keys(child) #:nodoc:
731 732
            child.key?(:only) || child.key?(:except) ? [:only, :except] : []
          end
733 734
      end

735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
      # 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
      # app/controllers/admin directory, and you can group them together in your
      # router:
      #
      #   namespace "admin" do
      #     resources :posts, :comments
      #   end
      #
J
Joshua Peek 已提交
766
      module Resources
767 768
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
769 770 771
        VALID_ON_OPTIONS  = [:new, :collection, :member]
        RESOURCE_OPTIONS  = [:as, :controller, :path, :only, :except]
        CANONICAL_ACTIONS = %w(index create new show update destroy)
772

773
        class Resource #:nodoc:
774
          DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit]
775

776
          attr_reader :controller, :path, :options
777 778

          def initialize(entities, options = {})
779
            @name       = entities.to_s
780
            @path       = (options.delete(:path) || @name).to_s
781
            @controller = (options.delete(:controller) || @name).to_s
782
            @as         = options.delete(:as)
783
            @options    = options
784 785
          end

786
          def default_actions
787
            self.class::DEFAULT_ACTIONS
788 789
          end

790
          def actions
791
            if only = @options[:only]
792
              Array(only).map(&:to_sym)
793
            elsif except = @options[:except]
794 795 796 797 798 799
              default_actions - Array(except).map(&:to_sym)
            else
              default_actions
            end
          end

800
          def name
801
            @as || @name
802 803
          end

804
          def plural
805
            @plural ||= name.to_s
806 807 808
          end

          def singular
809
            @singular ||= name.to_s.singularize
810 811
          end

812
          alias :member_name :singular
813

814
          # Checks for uncountable plurals, and appends "_index" if they're.
815
          def collection_name
816
            singular == plural ? "#{plural}_index" : plural
817 818
          end

819
          def resource_scope
820
            { :controller => controller }
821 822
          end

823
          alias :collection_scope :path
824 825

          def member_scope
826
            "#{path}/:id"
827 828
          end

829
          def new_scope(new_path)
830
            "#{path}/#{new_path}"
831 832 833
          end

          def nested_scope
834
            "#{path}/:#{singular}_id"
835
          end
836

837 838 839
        end

        class SingletonResource < Resource #:nodoc:
840
          DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit]
841

842
          def initialize(entities, options)
843
            @as         = nil
844
            @name       = entities.to_s
845
            @path       = (options.delete(:path) || @name).to_s
846
            @controller = (options.delete(:controller) || plural).to_s
847 848 849 850
            @as         = options.delete(:as)
            @options    = options
          end

851 852
          def plural
            @plural ||= name.to_s.pluralize
853 854
          end

855 856
          def singular
            @singular ||= name.to_s
857
          end
858 859 860 861 862 863

          alias :member_name :singular
          alias :collection_name :singular

          alias :member_scope :path
          alias :nested_scope :path
864 865
        end

866
        def initialize(*args) #:nodoc:
867
          super
868
          @scope[:path_names] = @set.resources_path_names
869 870
        end

871 872 873 874
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
        # Sometimes, you have a resource that clients always look up without
        # referencing an ID. A common example, /profile always shows the
        # profile of the currently logged in user. In this case, you can use
        # a singular resource to map /profile (rather than /profile/:id) to
        # the show action:
        #
        #   resource :geocoder
        #
        # creates six different routes in your application, all mapping to
        # the GeoCoders controller (note that the controller is named after
        # the plural):
        #
        #   GET     /geocoder/new
        #   POST    /geocoder
        #   GET     /geocoder
        #   GET     /geocoder/edit
        #   PUT     /geocoder
        #   DELETE  /geocoder
J
Joshua Peek 已提交
893
        def resource(*resources, &block)
J
Joshua Peek 已提交
894
          options = resources.extract_options!
J
Joshua Peek 已提交
895

896
          if apply_common_behavior_for(:resource, resources, options, &block)
897 898 899
            return self
          end

900 901
          resource_scope(SingletonResource.new(resources.pop, options)) do
            yield if block_given?
902

903
            collection do
904
              post :create
905
            end if parent_resource.actions.include?(:create)
906

907
            new do
908
              get :new
909
            end if parent_resource.actions.include?(:new)
910

911
            member do
912
              get    :edit if parent_resource.actions.include?(:edit)
913 914 915
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
916 917 918
            end
          end

J
Joshua Peek 已提交
919
          self
920 921
        end

922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937
        # 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
        # the Photos controller:
        #
        #   GET     /photos/new
        #   POST    /photos
        #   GET     /photos/:id
        #   GET     /photos/:id/edit
        #   PUT     /photos/:id
        #   DELETE  /photos/:id
938
        #
939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
        # Resources can also be nested infinitely by using this block syntax:
        #
        #   resources :photos do
        #     resources :comments
        #   end
        #
        # This generates the following comments routes:
        #
        #   GET     /photos/:id/comments/new
        #   POST    /photos/:id/comments
        #   GET     /photos/:id/comments/:id
        #   GET     /photos/:id/comments/:id/edit
        #   PUT     /photos/:id/comments/:id
        #   DELETE  /photos/:id/comments/:id
        #
954 955 956 957 958 959 960 961
        # === Supported options
        # [:path_names]
        #   Allows you to change the paths of the seven default actions.
        #   Paths not specified are not changed.
        #
        #     resources :posts, :path_names => { :new => "brand_new" }
        #
        #   The above example will now change /posts/new to /posts/brand_new
962 963 964 965 966 967 968
        #
        # [:module]
        #   Set the module where the controller can be found. Defaults to nothing.
        #
        #     resources :posts, :module => "admin"
        #
        #   All requests to the posts resources will now go to +Admin::PostsController+.
969 970 971 972 973 974 975 976
        #
        # [:path]
        #
        #  Set a path prefix for this resource.
        #
        #     resources :posts, :path => "admin"
        #
        #  All actions for this resource will now be at +/admin/posts+.
J
Joshua Peek 已提交
977
        def resources(*resources, &block)
J
Joshua Peek 已提交
978
          options = resources.extract_options!
979

980
          if apply_common_behavior_for(:resources, resources, options, &block)
981 982 983
            return self
          end

984
          resource_scope(Resource.new(resources.pop, options)) do
985
            yield if block_given?
J
Joshua Peek 已提交
986

987
            collection do
988 989
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
990
            end
991

992
            new do
993
              get :new
994
            end if parent_resource.actions.include?(:new)
995

996
            member do
997
              get    :edit if parent_resource.actions.include?(:edit)
998 999 1000
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
1001 1002 1003
            end
          end

J
Joshua Peek 已提交
1004
          self
1005 1006
        end

1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
        # 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>
        # with GET, and route to the search action of PhotosController. It will also
        # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
        # route helpers.
J
Joshua Peek 已提交
1019
        def collection
1020 1021
          unless resource_scope?
            raise ArgumentError, "can't use collection outside resource(s) scope"
1022 1023
          end

1024 1025 1026 1027
          with_scope_level(:collection) do
            scope(parent_resource.collection_scope) do
              yield
            end
J
Joshua Peek 已提交
1028
          end
1029
        end
J
Joshua Peek 已提交
1030

1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041
        # 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
        # preview action of PhotosController. It will also create the
        # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
J
Joshua Peek 已提交
1042
        def member
1043 1044
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
1045
          end
J
Joshua Peek 已提交
1046

1047 1048 1049 1050
          with_scope_level(:member) do
            scope(parent_resource.member_scope) do
              yield
            end
1051 1052 1053 1054 1055 1056 1057
          end
        end

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

1059 1060 1061 1062
          with_scope_level(:new) do
            scope(parent_resource.new_scope(action_path(:new))) do
              yield
            end
J
Joshua Peek 已提交
1063
          end
J
Joshua Peek 已提交
1064 1065
        end

1066
        def nested
1067 1068
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
1069 1070 1071
          end

          with_scope_level(:nested) do
1072
            if shallow?
1073
              with_exclusive_scope do
1074
                if @scope[:shallow_path].blank?
1075
                  scope(parent_resource.nested_scope, nested_options) { yield }
1076
                else
1077
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1078
                    scope(parent_resource.nested_scope, nested_options) { yield }
1079 1080 1081 1082
                  end
                end
              end
            else
1083
              scope(parent_resource.nested_scope, nested_options) { yield }
1084 1085 1086 1087
            end
          end
        end

1088
        def namespace(path, options = {})
1089
          if resource_scope?
1090 1091 1092 1093 1094 1095
            nested { super }
          else
            super
          end
        end

1096 1097 1098 1099 1100 1101
        def shallow
          scope(:shallow => true) do
            yield
          end
        end

1102 1103 1104 1105
        def shallow?
          parent_resource.instance_of?(Resource) && @scope[:shallow]
        end

J
Joshua Peek 已提交
1106
        def match(*args)
1107
          options = args.extract_options!.dup
1108 1109
          options[:anchor] = true unless options.key?(:anchor)

1110
          if args.length > 1
1111
            args.each { |path| match(path, options.dup) }
1112 1113 1114
            return self
          end

1115 1116
          on = options.delete(:on)
          if VALID_ON_OPTIONS.include?(on)
1117
            args.push(options)
1118 1119 1120
            return send(on){ match(*args) }
          elsif on
            raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1121 1122
          end

1123 1124 1125 1126
          if @scope[:scope_level] == :resources
            args.push(options)
            return nested { match(*args) }
          elsif @scope[:scope_level] == :resource
1127
            args.push(options)
J
Joshua Peek 已提交
1128 1129
            return member { match(*args) }
          end
J
Joshua Peek 已提交
1130

1131
          action = args.first
1132
          path = path_for_action(action, options.delete(:path))
1133

1134 1135 1136
          if action.to_s =~ /^[\w\/]+$/
            options[:action] ||= action unless action.to_s.include?("/")
          else
1137 1138 1139 1140 1141 1142 1143
            action = nil
          end

          if options.key?(:as) && !options[:as]
            options.delete(:as)
          else
            options[:as] = name_for_action(options[:as], action)
J
Joshua Peek 已提交
1144
          end
J
Joshua Peek 已提交
1145

1146
          super(path, options)
J
Joshua Peek 已提交
1147 1148
        end

1149
        def root(options={})
1150
          if @scope[:scope_level] == :resources
1151 1152
            with_scope_level(:root) do
              scope(parent_resource.path) do
1153 1154 1155 1156 1157 1158
                super(options)
              end
            end
          else
            super(options)
          end
1159 1160
        end

1161
        protected
1162

1163
          def parent_resource #:nodoc:
1164 1165 1166
            @scope[:scope_level_resource]
          end

J
José Valim 已提交
1167
          def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1168 1169 1170 1171 1172
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

1173 1174 1175 1176 1177
            if resource_scope?
              nested { send(method, resources.pop, options, &block) }
              return true
            end

1178
            options.keys.each do |k|
1179 1180 1181
              (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
            end

1182 1183 1184
            scope_options = options.slice!(*RESOURCE_OPTIONS)
            unless scope_options.empty?
              scope(scope_options) do
1185 1186 1187 1188 1189
                send(method, resources.pop, options, &block)
              end
              return true
            end

1190 1191 1192 1193
            unless action_options?(options)
              options.merge!(scope_action_options) if scope_action_options?
            end

1194 1195 1196
            false
          end

J
José Valim 已提交
1197
          def action_options?(options) #:nodoc:
1198 1199 1200
            options[:only] || options[:except]
          end

J
José Valim 已提交
1201
          def scope_action_options? #:nodoc:
1202 1203 1204
            @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except])
          end

J
José Valim 已提交
1205
          def scope_action_options #:nodoc:
1206 1207 1208
            @scope[:options].slice(:only, :except)
          end

J
José Valim 已提交
1209
          def resource_scope? #:nodoc:
1210 1211 1212
            [:resource, :resources].include?(@scope[:scope_level])
          end

J
José Valim 已提交
1213
          def resource_method_scope? #:nodoc:
1214 1215 1216
            [:collection, :member, :new].include?(@scope[:scope_level])
          end

1217
          def with_exclusive_scope
1218
            begin
1219 1220
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
1221

1222 1223 1224
              with_scope_level(:exclusive) do
                yield
              end
1225
            ensure
1226
              @scope[:as], @scope[:path] = old_name_prefix, old_path
1227 1228 1229
            end
          end

1230
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
1231
            old, @scope[:scope_level] = @scope[:scope_level], kind
1232
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
1233 1234 1235
            yield
          ensure
            @scope[:scope_level] = old
1236
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
1237
          end
1238

J
José Valim 已提交
1239
          def resource_scope(resource) #:nodoc:
1240
            with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do
1241
              scope(parent_resource.resource_scope) do
1242 1243 1244 1245 1246
                yield
              end
            end
          end

J
José Valim 已提交
1247
          def nested_options #:nodoc:
1248 1249 1250 1251 1252 1253
            {}.tap do |options|
              options[:as] = parent_resource.member_name
              options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint?
            end
          end

J
José Valim 已提交
1254
          def id_constraint? #:nodoc:
1255
            @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
1256 1257
          end

J
José Valim 已提交
1258
          def id_constraint #:nodoc:
1259
            @scope[:constraints][:id]
1260 1261
          end

J
José Valim 已提交
1262
          def canonical_action?(action, flag) #:nodoc:
1263
            flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1264 1265
          end

J
José Valim 已提交
1266
          def shallow_scoping? #:nodoc:
1267
            shallow? && @scope[:scope_level] == :member
1268 1269
          end

J
José Valim 已提交
1270
          def path_for_action(action, path) #:nodoc:
1271
            prefix = shallow_scoping? ?
1272 1273
              "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]

1274 1275
            path = if canonical_action?(action, path.blank?)
              prefix.to_s
1276
            else
1277
              "#{prefix}/#{action_path(action, path)}"
1278 1279 1280
            end
          end

J
José Valim 已提交
1281
          def action_path(name, path = nil) #:nodoc:
1282
            path || @scope[:path_names][name.to_sym] || name.to_s
1283 1284
          end

J
José Valim 已提交
1285
          def prefix_name_for_action(as, action) #:nodoc:
1286
            if as
1287
              as.to_s
1288
            elsif !canonical_action?(action, @scope[:scope_level])
1289
              action.to_s
1290
            end
1291 1292
          end

J
José Valim 已提交
1293
          def name_for_action(as, action) #:nodoc:
1294
            prefix = prefix_name_for_action(as, action)
1295
            prefix = Mapper.normalize_name(prefix) if prefix
1296 1297 1298
            name_prefix = @scope[:as]

            if parent_resource
1299 1300
              return nil if as.nil? && action.nil?

1301 1302
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
1303
            end
1304

1305
            name = case @scope[:scope_level]
1306 1307
            when :nested
              [member_name, prefix]
1308
            when :collection
1309
              [prefix, name_prefix, collection_name]
1310
            when :new
1311 1312 1313 1314 1315
              [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]
1316
            else
1317
              [name_prefix, member_name, prefix]
1318
            end
1319

1320
            candidate = name.select(&:present?).join("_").presence
1321
            candidate unless as.nil? && @set.routes.find { |r| r.name == candidate }
1322
          end
J
Joshua Peek 已提交
1323
      end
J
Joshua Peek 已提交
1324

J
José Valim 已提交
1325
      module Shorthand #:nodoc:
1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337
        def match(*args)
          if args.size == 1 && args.last.is_a?(Hash)
            options  = args.pop
            path, to = options.find { |name, value| name.is_a?(String) }
            options.merge!(:to => to).delete(path)
            super(path, options)
          else
            super
          end
        end
      end

1338 1339
      include Base
      include HttpHelpers
1340
      include Redirection
1341 1342
      include Scoping
      include Resources
1343
      include Shorthand
J
Joshua Peek 已提交
1344 1345
    end
  end
J
Joshua Peek 已提交
1346
end