mapper.rb 27.9 KB
Newer Older
1
require 'active_support/core_ext/hash/except'
2
require 'active_support/core_ext/object/blank'
3

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

16 17
        def initialize(app, constraints, request)
          @app, @constraints, @request = app, constraints, request
18 19 20
        end

        def call(env)
21
          req = @request.new(env)
22 23 24

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

          @app.call(env)
        end
      end

35
      class Mapping #:nodoc:
36
        IGNORE_OPTIONS = [:to, :as, :controller, :action, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
37

38 39 40
        def initialize(set, scope, args)
          @set, @scope    = set, scope
          @path, @options = extract_path_and_options(args)
41
        end
J
Joshua Peek 已提交
42

43
        def to_route
44
          [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
45
        end
J
Joshua Peek 已提交
46

47 48
        private
          def extract_path_and_options(args)
49
            options = args.extract_options!
50

51
            if using_to_shorthand?(args, options)
52 53 54 55 56
              path, to = options.find { |name, value| name.is_a?(String) }
              options.merge!(:to => to).delete(path) if path
            else
              path = args.first
            end
J
Joshua Peek 已提交
57

58
            if @scope[:module] && options[:to] && !options[:to].is_a?(Proc)
59 60 61 62 63 64 65
              if options[:to].to_s.include?("#")
                options[:to] = "#{@scope[:module]}/#{options[:to]}"
              elsif @scope[:controller].nil?
                options[:to] = "#{@scope[:module]}##{options[:to]}"
              end
            end

66
            path = normalize_path(path)
67
            path_without_format = path.sub(/\(\.:format\)$/, '')
68

69 70 71
            if using_match_shorthand?(path_without_format, options)
              options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1')
              options[:as] ||= path_without_format[1..-1].gsub("/", "_")
72 73 74
            end

            [ path, options ]
75
          end
76

77 78 79 80
          # match "account" => "account#index"
          def using_to_shorthand?(args, options)
            args.empty? && options.present?
          end
81

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

87
          def normalize_path(path)
88 89
            raise ArgumentError, "path is required" if @scope[:path].blank? && path.blank?
            Mapper.normalize_path("#{@scope[:path]}/#{path}")
90
          end
91

92 93
          def app
            Constraints.new(
94
              to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults, :module => @scope[:module]),
95 96
              blocks,
              @set.request_class
97
            )
98 99
          end

100 101 102
          def conditions
            { :path_info => @path }.merge(constraints).merge(request_method_condition)
          end
J
Joshua Peek 已提交
103

104
          def requirements
105
            @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
106 107 108 109
              requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
              @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
            end
          end
110

111
          def defaults
112 113 114 115 116 117 118 119 120
            @defaults ||= (@options[:defaults] || {}).tap do |defaults|
              defaults.merge!(default_controller_and_action)
              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

          def default_controller_and_action
            if to.respond_to?(:call)
121 122 123 124 125 126 127
              { }
            else
              defaults = case to
              when String
                controller, action = to.split('#')
                { :controller => controller, :action => action }
              when Symbol
128
                { :action => to.to_s }
129
              else
130
                {}
131
              end
J
Joshua Peek 已提交
132

133
              defaults[:controller] ||= default_controller
134
              defaults[:action]     ||= default_action
135 136 137

              defaults.delete(:controller) if defaults[:controller].blank?
              defaults.delete(:action)     if defaults[:action].blank?
138

139 140 141
              if defaults[:controller].blank? && segment_keys.exclude?("controller")
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
142

143 144 145
              if defaults[:action].blank? && segment_keys.exclude?("action")
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
146

147 148 149
              defaults
            end
          end
150

151 152 153 154 155 156
          def blocks
            if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
              block = @options[:constraints]
            else
              block = nil
            end
J
Joshua Peek 已提交
157 158

            ((@scope[:blocks] || []) + [ block ]).compact
159
          end
J
Joshua Peek 已提交
160

161 162 163
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
164

165 166 167 168 169 170
          def request_method_condition
            if via = @options[:via]
              via = Array(via).map { |m| m.to_s.upcase }
              { :request_method => Regexp.union(*via) }
            else
              { }
171
            end
172
          end
J
Joshua Peek 已提交
173

174 175
          def segment_keys
            @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
176 177
              Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
            ).names
178
          end
179

180 181 182
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
183

184
          def default_controller
185 186 187 188 189
            if @options[:controller]
              @options[:controller].to_s
            elsif @scope[:controller]
              @scope[:controller].to_s
            end
190
          end
191 192 193 194 195 196

          def default_action
            if @options[:action]
              @options[:action].to_s
            end
          end
197
      end
198

199
      # Invokes Rack::Mount::Utils.normalize path and ensure that
200 201
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
202 203
      def self.normalize_path(path)
        path = Rack::Mount::Utils.normalize_path(path)
204
        path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$}
205 206 207
        path
      end

208
      module Base
209
        def initialize(set) #:nodoc:
210 211
          @set = set
        end
212

213 214 215
        def root(options = {})
          match '/', options.reverse_merge(:as => :root)
        end
216

217
        def match(*args)
218 219
          mapping = Mapping.new(@set, @scope, args).to_route
          @set.add_route(*mapping)
220 221
          self
        end
222

223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
        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

          match(path, options.merge(:to => app, :anchor => false))
          self
        end

238 239 240 241
        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
      end

      module HttpHelpers
        def get(*args, &block)
          map_method(:get, *args, &block)
        end

        def post(*args, &block)
          map_method(:post, *args, &block)
        end

        def put(*args, &block)
          map_method(:put, *args, &block)
        end

        def delete(*args, &block)
          map_method(:delete, *args, &block)
        end

261 262 263
        def redirect(*args, &block)
          options = args.last.is_a?(Hash) ? args.pop : {}

264 265 266
          path      = args.shift || block
          path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
          status    = options[:status] || 301
267
          body      = 'Moved Permanently'
268 269

          lambda do |env|
270
            req = Request.new(env)
271 272 273 274 275

            params = [req.symbolized_path_parameters]
            params << req if path_proc.arity > 1

            uri = URI.parse(path_proc.call(*params))
276 277
            uri.scheme ||= req.scheme
            uri.host   ||= req.host
278
            uri.port   ||= req.port unless req.port == 80
279 280 281 282 283 284 285

            headers = {
              'Location' => uri.to_s,
              'Content-Type' => 'text/html',
              'Content-Length' => body.length.to_s
            }
            [ status, headers, [body] ]
286
          end
287 288 289 290 291 292 293 294 295 296 297 298 299
        end

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

      module Scoping
300
        def initialize(*args) #:nodoc:
301 302 303 304 305 306
          @scope = {}
          super
        end

        def scope(*args)
          options = args.extract_options!
307
          options = options.dup
308

309 310
          if name_prefix = options.delete(:name_prefix)
            options[:as] ||= name_prefix
311
            ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new router syntax. Use :as instead.", caller
312 313
          end

314 315 316 317 318 319 320
          case args.first
          when String
            options[:path] = args.first
          when Symbol
            options[:controller] = args.first
          end

321
          recover = {}
322

323 324 325
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
            block, options[:constraints] = options[:constraints], {}
326
          end
327

328 329 330 331 332
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
333 334
          end

335 336
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
337

338 339
          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)
340 341 342 343

          yield
          self
        ensure
344 345 346 347 348 349
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
350 351 352 353 354 355
        end

        def controller(controller)
          scope(controller.to_sym) { yield }
        end

356
        def namespace(path, options = {})
357
          path = path.to_s
358 359 360
          options = { :path => path, :as => path, :module => path,
                      :shallow_path => path, :shallow_prefix => path }.merge!(options)
          scope(options) { yield }
361 362 363 364 365 366
        end

        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
        end

367 368 369 370
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

371 372 373 374 375
        def match(*args)
          options = args.extract_options!

          options = (@scope[:options] || {}).merge(options)

376 377 378 379
          if @scope[:as] && !options[:as].blank?
            options[:as] = "#{@scope[:as]}_#{options[:as]}"
          elsif @scope[:as] && options[:as] == ""
            options[:as] = @scope[:as].to_s
380 381 382 383 384
          end

          args.push(options)
          super(*args)
        end
385 386 387 388 389 390 391

        private
          def scope_options
            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
          end

          def merge_path_scope(parent, child)
392
            Mapper.normalize_path("#{parent}/#{child}")
393 394
          end

395 396 397 398
          def merge_shallow_path_scope(parent, child)
            Mapper.normalize_path("#{parent}/#{child}")
          end

399
          def merge_as_scope(parent, child)
400
            parent ? "#{parent}_#{child}" : child
401 402
          end

403 404 405 406
          def merge_shallow_prefix_scope(parent, child)
            parent ? "#{parent}_#{child}" : child
          end

407
          def merge_module_scope(parent, child)
408 409 410 411
            parent ? "#{parent}/#{child}" : child
          end

          def merge_controller_scope(parent, child)
412
            @scope[:module] ? "#{@scope[:module]}/#{child}" : child
413 414
          end

415
          def merge_path_names_scope(parent, child)
416 417 418 419 420 421 422
            merge_options_scope(parent, child)
          end

          def merge_constraints_scope(parent, child)
            merge_options_scope(parent, child)
          end

423 424 425 426
          def merge_defaults_scope(parent, child)
            merge_options_scope(parent, child)
          end

427
          def merge_blocks_scope(parent, child)
428 429 430
            merged = parent ? parent.dup : []
            merged << child if child
            merged
431 432 433 434 435
          end

          def merge_options_scope(parent, child)
            (parent || {}).merge(child)
          end
436 437 438 439

          def merge_shallow_scope(parent, child)
            child ? true : false
          end
440 441
      end

J
Joshua Peek 已提交
442
      module Resources
443 444 445 446
        # CANONICAL_ACTIONS holds all actions that does not need a prefix or
        # a path appended since they fit properly in their scope level.
        VALID_ON_OPTIONS = [:new, :collection, :member]
        CANONICAL_ACTIONS = [:index, :create, :new, :show, :update, :destroy]
447 448
        MERGE_FROM_SCOPE_OPTIONS = [:shallow, :constraints]

449
        class Resource #:nodoc:
450
          DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit]
451

452
          attr_reader :controller, :path, :options
453 454

          def initialize(entities, options = {})
455 456
            @name       = entities.to_s
            @path       = options.delete(:path) || @name
457
            @controller = (options.delete(:controller) || @name).to_s
458
            @as         = options.delete(:as)
459
            @options    = options
460 461
          end

462
          def default_actions
463
            self.class::DEFAULT_ACTIONS
464 465 466 467
          end

          def actions
            if only = options[:only]
468
              Array(only).map(&:to_sym)
469
            elsif except = options[:except]
470
              default_actions - Array(except).map(&:to_sym)
471 472 473 474 475
            else
              default_actions
            end
          end

476
          def name
477
            @as || @name
478 479
          end

480 481 482 483 484 485
          def plural
            name.to_s.pluralize
          end

          def singular
            name.to_s.singularize
486 487 488
          end

          def member_name
489
            singular
490 491
          end

492 493
          alias_method :nested_name, :member_name

494
          # Checks for uncountable plurals, and appends "_index" if they're.
495
          def collection_name
496
            singular == plural ? "#{plural}_index" : plural
497 498
          end

499 500
          def shallow?
            options[:shallow] ? true : false
501
          end
502 503 504 505 506 507 508 509 510 511 512 513 514 515

          def constraints
            options[:constraints] || {}
          end

          def id_constraint?
            options[:id] && options[:id].is_a?(Regexp) || constraints[:id] && constraints[:id].is_a?(Regexp)
          end

          def id_constraint
            options[:id] || constraints[:id]
          end

          def collection_options
516 517 518 519
            (options || {}).dup.tap do |opts|
              opts.delete(:id)
              opts[:constraints] = options[:constraints].dup if options[:constraints]
              opts[:constraints].delete(:id) if options[:constraints].is_a?(Hash)
520 521 522
            end
          end

523 524
          def nested_path
            "#{path}/:#{singular}_id"
525 526
          end

527
          def nested_options
528
            {}.tap do |opts|
529
              opts[:as] = member_name
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
              opts["#{singular}_id".to_sym] = id_constraint if id_constraint?
              opts[:options] = { :shallow => shallow? } unless options[:shallow].nil?
            end
          end

          def resource_scope
            [{ :controller => controller }]
          end

          def collection_scope
            [path, collection_options]
          end

          def member_scope
            ["#{path}/:id", options]
          end

547 548
          def new_scope(new_path)
            ["#{path}/#{new_path}"]
549 550 551 552
          end

          def nested_scope
            [nested_path, nested_options]
553
          end
554 555 556
        end

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

559 560 561 562 563 564 565 566
          def initialize(entities, options)
            @name       = entities.to_s
            @path       = options.delete(:path) || @name
            @controller = (options.delete(:controller) || @name.to_s.pluralize).to_s
            @as         = options.delete(:as)
            @options    = options
          end

567 568
          def member_name
            name
569
          end
570
          alias :collection_name :member_name
571

572 573
          def nested_path
            path
574 575
          end

576 577
          def nested_options
            {}.tap do |opts|
578
              opts[:as] = member_name
579 580
              opts[:options] = { :shallow => shallow? } unless @options[:shallow].nil?
            end
581
          end
582

583 584
          def shallow?
            false
585 586
          end

587 588
          def member_scope
            [path, options]
589
          end
590 591
        end

592
        def initialize(*args) #:nodoc:
593
          super
594
          @scope[:path_names] = @set.resources_path_names
595 596
        end

597 598 599 600
        def resources_path_names(options)
          @scope[:path_names].merge!(options)
        end

J
Joshua Peek 已提交
601
        def resource(*resources, &block)
J
Joshua Peek 已提交
602
          options = resources.extract_options!
J
Joshua Peek 已提交
603

604
          if apply_common_behavior_for(:resource, resources, options, &block)
605 606 607
            return self
          end

608 609
          resource_scope(SingletonResource.new(resources.pop, options)) do
            yield if block_given?
610

611
            collection_scope do
612 613 614 615 616 617
              post :create
            end if parent_resource.actions.include?(:create)

            new_scope do
              get :new
            end if parent_resource.actions.include?(:new)
618

619 620 621 622 623
            member_scope  do
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
              get    :edit if parent_resource.actions.include?(:edit)
624 625 626
            end
          end

J
Joshua Peek 已提交
627
          self
628 629
        end

J
Joshua Peek 已提交
630
        def resources(*resources, &block)
J
Joshua Peek 已提交
631
          options = resources.extract_options!
632

633
          if apply_common_behavior_for(:resources, resources, options, &block)
634 635 636
            return self
          end

637 638
          resource_scope(Resource.new(resources.pop, options)) do
            yield if block_given?
J
Joshua Peek 已提交
639

640 641 642 643
            collection_scope do
              get  :index if parent_resource.actions.include?(:index)
              post :create if parent_resource.actions.include?(:create)
            end
644

645 646 647 648
            new_scope do
              get :new
            end if parent_resource.actions.include?(:new)

649 650 651 652 653
            member_scope  do
              get    :show if parent_resource.actions.include?(:show)
              put    :update if parent_resource.actions.include?(:update)
              delete :destroy if parent_resource.actions.include?(:destroy)
              get    :edit if parent_resource.actions.include?(:edit)
654 655 656
            end
          end

J
Joshua Peek 已提交
657
          self
658 659
        end

J
Joshua Peek 已提交
660 661 662
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
663 664
          end

665 666
          collection_scope do
            yield
J
Joshua Peek 已提交
667
          end
668
        end
J
Joshua Peek 已提交
669

J
Joshua Peek 已提交
670
        def member
671 672
          unless resource_scope?
            raise ArgumentError, "can't use member outside resource(s) scope"
J
Joshua Peek 已提交
673
          end
J
Joshua Peek 已提交
674

675 676
          member_scope do
            yield
677 678 679 680 681 682 683
          end
        end

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

685 686
          new_scope do
            yield
J
Joshua Peek 已提交
687
          end
J
Joshua Peek 已提交
688 689
        end

690
        def nested
691 692
          unless resource_scope?
            raise ArgumentError, "can't use nested outside resource(s) scope"
693 694 695
          end

          with_scope_level(:nested) do
696 697
            if parent_resource.shallow?
              with_exclusive_scope do
698
                if @scope[:shallow_path].blank?
699 700
                  scope(*parent_resource.nested_scope) { yield }
                else
701
                  scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
702 703 704 705 706 707
                    scope(*parent_resource.nested_scope) { yield }
                  end
                end
              end
            else
              scope(*parent_resource.nested_scope) { yield }
708 709 710 711
            end
          end
        end

712
        def namespace(path, options = {})
713
          if resource_scope?
714 715 716 717 718 719
            nested { super }
          else
            super
          end
        end

720 721 722 723 724 725
        def shallow
          scope(:shallow => true) do
            yield
          end
        end

J
Joshua Peek 已提交
726
        def match(*args)
727
          options = args.extract_options!.dup
728 729
          options[:anchor] = true unless options.key?(:anchor)

730
          if args.length > 1
731
            args.each { |path| match(path, options.dup) }
732 733 734
            return self
          end

735 736
          on = options.delete(:on)
          if VALID_ON_OPTIONS.include?(on)
737
            args.push(options)
738 739 740
            return send(on){ match(*args) }
          elsif on
            raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
741 742
          end

743 744
          if @scope[:scope_level] == :resource
            args.push(options)
J
Joshua Peek 已提交
745 746
            return member { match(*args) }
          end
J
Joshua Peek 已提交
747

748
          path = options.delete(:path)
749
          action = args.first
750

751 752 753 754
          if action.is_a?(Symbol)
            path = path_for_action(action, path)
            options[:to] ||= action
            options[:as]   = name_for_action(action, options[:as])
755 756

            with_exclusive_scope do
757 758 759 760 761 762 763 764 765 766 767
              return super(path, options)
            end
          elsif resource_method_scope?
            path = path_for_custom_action
            options[:as] = name_for_action(options[:as]) if options[:as]
            args.push(options)

            with_exclusive_scope do
              scope(path) do
                return super
              end
768
            end
769 770 771 772
          end

          if resource_scope?
            raise ArgumentError, "can't define route directly in resource(s) scope"
J
Joshua Peek 已提交
773
          end
J
Joshua Peek 已提交
774

775
          args.push(options)
J
Joshua Peek 已提交
776
          super
J
Joshua Peek 已提交
777 778
        end

779
        def root(options={})
780
          if @scope[:scope_level] == :resources
781
            with_scope_level(:nested) do
782
              scope(parent_resource.path, :as => parent_resource.collection_name) do
783 784 785 786 787 788
                super(options)
              end
            end
          else
            super(options)
          end
789 790
        end

791
        protected
792

793
          def parent_resource #:nodoc:
794 795 796
            @scope[:scope_level_resource]
          end

797
          def apply_common_behavior_for(method, resources, options, &block)
798 799 800 801 802 803
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

            if path_names = options.delete(:path_names)
804
              scope(:path_names => path_names) do
805 806 807 808 809
                send(method, resources.pop, options, &block)
              end
              return true
            end

810 811 812 813
            scope_options = @scope.slice(*MERGE_FROM_SCOPE_OPTIONS).delete_if{ |k,v| v.blank? }
            options.reverse_merge!(scope_options) unless scope_options.empty?
            options.reverse_merge!(@scope[:options]) unless @scope[:options].blank?

814
            if resource_scope?
815 816 817 818 819 820 821 822 823
              nested do
                send(method, resources.pop, options, &block)
              end
              return true
            end

            false
          end

824 825 826 827
          def resource_scope?
            [:resource, :resources].include?(@scope[:scope_level])
          end

828 829 830 831
          def resource_method_scope?
            [:collection, :member, :new].include?(@scope[:scope_level])
          end

832
          def with_exclusive_scope
833
            begin
834 835
              old_name_prefix, old_path = @scope[:as], @scope[:path]
              @scope[:as], @scope[:path] = nil, nil
836

837 838 839
              with_scope_level(:exclusive) do
                yield
              end
840
            ensure
841
              @scope[:as], @scope[:path] = old_name_prefix, old_path
842 843 844
            end
          end

845
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
846
            old, @scope[:scope_level] = @scope[:scope_level], kind
847
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
848 849 850
            yield
          ensure
            @scope[:scope_level] = old
851
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
852
          end
853 854 855 856 857 858 859 860 861

          def resource_scope(resource)
            with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do
              scope(*parent_resource.resource_scope) do
                yield
              end
            end
          end

862 863 864 865 866 867 868 869
          def new_scope
            with_scope_level(:new) do
              scope(*parent_resource.new_scope(action_path(:new))) do
                yield
              end
            end
          end

870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
          def collection_scope
            with_scope_level(:collection) do
              scope(*parent_resource.collection_scope) do
                yield
              end
            end
          end

          def member_scope
            with_scope_level(:member) do
              scope(*parent_resource.member_scope) do
                yield
              end
            end
          end

886 887 888 889 890 891 892 893
          def canonical_action?(action, flag)
            flag && CANONICAL_ACTIONS.include?(action)
          end

          def shallow_scoping?
            parent_resource && parent_resource.shallow? && @scope[:scope_level] == :member
          end

894
          def path_for_action(action, path)
895
            prefix = shallow_scoping? ?
896 897
              "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]

898
            if canonical_action?(action, path.blank?)
899
              "#{prefix}(.:format)"
900
            else
901
              "#{prefix}/#{action_path(action, path)}(.:format)"
902 903 904
            end
          end

905
          def path_for_custom_action
906 907
            if shallow_scoping?
              "#{@scope[:shallow_path]}/#{parent_resource.path}/:id"
908
            else
909
              @scope[:path]
910 911 912
            end
          end

913 914
          def action_path(name, path = nil)
            path || @scope[:path_names][name.to_sym] || name.to_s
915 916
          end

917 918 919 920 921 922 923 924
          def prefix_name_for_action(action, as)
            if as.present?
              "#{as}_"
            elsif as
              ""
            elsif !canonical_action?(action, @scope[:scope_level])
              "#{action}_"
            end
925 926
          end

927 928 929 930 931 932 933 934 935
          def name_for_action(action, as=nil)
            prefix = prefix_name_for_action(action, as)
            name_prefix = @scope[:as]

            if parent_resource
              collection_name = parent_resource.collection_name
              member_name = parent_resource.member_name
              name_prefix = "#{name_prefix}_" if name_prefix.present?
            end 
936

937 938
            case @scope[:scope_level]
            when :collection
939
              "#{prefix}#{name_prefix}#{collection_name}"
940
            when :new
941
              "#{prefix}new_#{name_prefix}#{member_name}"
942
            else
943
              if shallow_scoping?
944
                shallow_prefix = "#{@scope[:shallow_prefix]}_" if @scope[:shallow_prefix].present?
945
                "#{prefix}#{shallow_prefix}#{member_name}"
946
              else
947
                "#{prefix}#{name_prefix}#{member_name}"
948 949 950
              end
            end
          end
J
Joshua Peek 已提交
951
      end
J
Joshua Peek 已提交
952

953 954 955 956
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
957 958
    end
  end
J
Joshua Peek 已提交
959
end