mapper.rb 19.4 KB
Newer Older
1 2
require 'active_support/core_ext/hash/except'

J
Joshua Peek 已提交
3 4
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
5
    class Mapper
6
      class Constraints
7
        def self.new(app, constraints = [])
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
          if constraints.any?
            super(app, constraints)
          else
            app
          end
        end

        def initialize(app, constraints = [])
          @app, @constraints = app, constraints
        end

        def call(env)
          req = Rack::Request.new(env)

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

          @app.call(env)
        end
      end

34 35 36 37
      class Mapping
        def initialize(set, scope, args)
          @set, @scope    = set, scope
          @path, @options = extract_path_and_options(args)
38
        end
J
Joshua Peek 已提交
39

40
        def to_route
41
          [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
42
        end
J
Joshua Peek 已提交
43

44 45
        private
          def extract_path_and_options(args)
46
            options = args.extract_options!
47

48 49
            case
            when using_to_shorthand?(args, options)
50 51
              path, to = options.find { |name, value| name.is_a?(String) }
              options.merge!(:to => to).delete(path) if path
52 53
            when using_match_shorthand?(args, options)
              path = args.first
54 55 56
              options = { :to     => path.gsub("/", "#"),
                          :as     => path.gsub("/", "_")
                        }.merge(options || {})
57 58 59
            else
              path = args.first
            end
J
Joshua Peek 已提交
60

61
            [ normalize_path(path), options ]
62
          end
63

64 65 66 67
          # match "account" => "account#index"
          def using_to_shorthand?(args, options)
            args.empty? && options.present?
          end
68

69 70
          # match "account/overview"
          def using_match_shorthand?(args, options)
71
            args.present? && options.except(:via, :anchor).empty? && !args.first.include?(':')
72
          end
73

74
          def normalize_path(path)
75
            path = "#{@scope[:path]}/#{path}"
76 77
            raise ArgumentError, "path is required" if path.empty?
            Mapper.normalize_path(path)
78
          end
79

80 81 82 83 84
          def app
            Constraints.new(
              to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
              blocks
            )
85 86
          end

87 88 89
          def conditions
            { :path_info => @path }.merge(constraints).merge(request_method_condition)
          end
J
Joshua Peek 已提交
90

91
          def requirements
92
            @requirements ||= (@options[:constraints] || {}).tap do |requirements|
93 94 95 96
              requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
              @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
            end
          end
97

98 99 100 101 102 103 104 105 106
          def defaults
            @defaults ||= if to.respond_to?(:call)
              { }
            else
              defaults = case to
              when String
                controller, action = to.split('#')
                { :controller => controller, :action => action }
              when Symbol
107
                { :action => to.to_s }.merge(default_controller ? { :controller => default_controller } : {})
108
              else
109
                default_controller ? { :controller => default_controller } : {}
110
              end
J
Joshua Peek 已提交
111

112 113 114
              if defaults[:controller].blank? && segment_keys.exclude?("controller")
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
115

116 117 118
              if defaults[:action].blank? && segment_keys.exclude?("action")
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
119

120 121 122
              defaults
            end
          end
123

124 125 126 127 128 129
          def blocks
            if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
              block = @options[:constraints]
            else
              block = nil
            end
J
Joshua Peek 已提交
130 131

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

134 135 136
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
137

138 139 140 141 142 143
          def request_method_condition
            if via = @options[:via]
              via = Array(via).map { |m| m.to_s.upcase }
              { :request_method => Regexp.union(*via) }
            else
              { }
144
            end
145
          end
J
Joshua Peek 已提交
146

147 148 149 150 151
          def segment_keys
            @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
                Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
              ).names
          end
152

153 154 155
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
156

157 158
          def default_controller
            @scope[:controller].to_s if @scope[:controller]
159
          end
160
      end
161

162
      # Invokes Rack::Mount::Utils.normalize path and ensure that
163 164
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
165 166
      def self.normalize_path(path)
        path = Rack::Mount::Utils.normalize_path(path)
167
        path.sub!(%r{/(\(+)/?:}, '\1/:') unless path =~ %r{^/\(+:.*\)$}
168 169 170
        path
      end

171 172 173 174
      module Base
        def initialize(set)
          @set = set
        end
175

176 177 178
        def root(options = {})
          match '/', options.reverse_merge(:as => :root)
        end
179

180
        def match(*args)
181 182
          mapping = Mapping.new(@set, @scope, args).to_route
          @set.add_route(*mapping)
183 184
          self
        end
185 186 187 188 189

        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
      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

209 210 211
        def redirect(*args, &block)
          options = args.last.is_a?(Hash) ? args.pop : {}

212 213 214
          path      = args.shift || block
          path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
          status    = options[:status] || 301
215
          body      = 'Moved Permanently'
216 217

          lambda do |env|
218
            req = Request.new(env)
219
            uri = URI.parse(path_proc.call(req.symbolized_path_parameters))
220 221
            uri.scheme ||= req.scheme
            uri.host   ||= req.host
222
            uri.port   ||= req.port unless req.port == 80
223 224 225 226 227 228 229

            headers = {
              'Location' => uri.to_s,
              'Content-Type' => 'text/html',
              'Content-Length' => body.length.to_s
            }
            [ status, headers, [body] ]
230
          end
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
        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
        def initialize(*args)
          @scope = {}
          super
        end

        def scope(*args)
          options = args.extract_options!

          case args.first
          when String
            options[:path] = args.first
          when Symbol
            options[:controller] = args.first
          end

259
          recover = {}
260

261 262 263
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
            block, options[:constraints] = options[:constraints], {}
264
          end
265

266 267 268 269 270
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
271 272
          end

273 274
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
275

276 277
          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)
278 279 280 281

          yield
          self
        ensure
282 283 284 285 286 287
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
288 289 290 291 292 293 294
        end

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

        def namespace(path)
295
          scope(path.to_s, :name_prefix => path.to_s, :controller_namespace => path.to_s) { yield }
296 297 298 299 300 301 302 303 304 305 306
        end

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

        def match(*args)
          options = args.extract_options!

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

307 308
          if @scope[:name_prefix] && !options[:as].blank?
            options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}"
309
          elsif @scope[:name_prefix] && options[:as] == ""
310
            options[:as] = @scope[:name_prefix].to_s
311 312 313 314 315
          end

          args.push(options)
          super(*args)
        end
316 317 318 319 320 321 322

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

          def merge_path_scope(parent, child)
323
            Mapper.normalize_path("#{parent}/#{child}")
324 325 326 327 328 329
          end

          def merge_name_prefix_scope(parent, child)
            parent ? "#{parent}_#{child}" : child
          end

330
          def merge_controller_namespace_scope(parent, child)
331 332 333 334
            parent ? "#{parent}/#{child}" : child
          end

          def merge_controller_scope(parent, child)
335
            @scope[:controller_namespace] ? "#{@scope[:controller_namespace]}/#{child}" : child
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
          end

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

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

          def merge_blocks_scope(parent, child)
            (parent || []) + [child]
          end

          def merge_options_scope(parent, child)
            (parent || {}).merge(child)
          end
353 354
      end

J
Joshua Peek 已提交
355
      module Resources
356
        CRUD_ACTIONS = [:index, :show, :create, :update, :destroy]
357

358
        class Resource #:nodoc:
359 360 361 362 363
          def self.default_actions
            [:index, :create, :new, :show, :update, :destroy, :edit]
          end

          attr_reader :plural, :singular, :options
364 365

          def initialize(entities, options = {})
366
            @name = entities.to_s
367
            @options = options
368

369 370
            @plural   = @name.pluralize
            @singular = @name.singularize
371 372
          end

373 374 375 376 377 378
          def default_actions
            self.class.default_actions
          end

          def actions
            if only = options[:only]
379
              Array(only).map(&:to_sym)
380
            elsif except = options[:except]
381
              default_actions - Array(except).map(&:to_sym)
382 383 384 385 386
            else
              default_actions
            end
          end

387 388 389 390 391 392 393 394 395
          def action_type(action)
            case action
            when :index, :create
              :collection
            when :show, :update, :destroy
              :member
            end
          end

396
          def name
397
            options[:as] || @name
398 399 400
          end

          def controller
401
            options[:controller] || plural
402 403 404
          end

          def member_name
405
            singular
406 407 408
          end

          def collection_name
409
            plural
410
          end
411

412 413 414 415 416 417 418 419 420
          def name_for_action(action)
            case action_type(action)
            when :collection
              collection_name
            when :member
              member_name
            end
          end

421 422 423
          def id_segment
            ":#{singular}_id"
          end
424 425 426
        end

        class SingletonResource < Resource #:nodoc:
427 428 429 430
          def self.default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

431
          def initialize(entity, options = {})
432
            super
433 434
          end

435 436 437 438 439 440 441
          def action_type(action)
            case action
            when :show, :create, :update, :destroy
              :member
            end
          end

442 443
          def member_name
            name
444 445 446
          end
        end

447 448 449 450 451
        def initialize(*args)
          super
          @scope[:resources_path_names] = @set.resources_path_names
        end

J
Joshua Peek 已提交
452
        def resource(*resources, &block)
J
Joshua Peek 已提交
453
          options = resources.extract_options!
J
Joshua Peek 已提交
454

455
          if apply_common_behavior_for(:resource, resources, options, &block)
456 457 458
            return self
          end

459
          resource = SingletonResource.new(resources.pop, options)
460

461
          scope(:path => resource.name.to_s, :controller => resource.controller) do
462
            with_scope_level(:resource, resource) do
463 464 465 466

              scope(:name_prefix => resource.name.to_s) do
                yield if block_given?
              end
467

468
              get    :show if resource.actions.include?(:show)
469 470 471
              post   :create if resource.actions.include?(:create)
              put    :update if resource.actions.include?(:update)
              delete :destroy if resource.actions.include?(:destroy)
472 473
              get    :new, :as => resource.name if resource.actions.include?(:new)
              get    :edit, :as => resource.name if resource.actions.include?(:edit)
474 475 476
            end
          end

J
Joshua Peek 已提交
477
          self
478 479
        end

J
Joshua Peek 已提交
480
        def resources(*resources, &block)
J
Joshua Peek 已提交
481
          options = resources.extract_options!
482

483
          if apply_common_behavior_for(:resources, resources, options, &block)
484 485 486
            return self
          end

487
          resource = Resource.new(resources.pop, options)
488

489
          scope(:path => resource.name.to_s, :controller => resource.controller) do
490 491
            with_scope_level(:resources, resource) do
              yield if block_given?
J
Joshua Peek 已提交
492

493
              with_scope_level(:collection) do
494
                get  :index if resource.actions.include?(:index)
495 496
                post :create if resource.actions.include?(:create)
                get  :new, :as => resource.singular if resource.actions.include?(:new)
497
              end
498

499
              with_scope_level(:member) do
500
                scope(':id') do
501
                  get    :show if resource.actions.include?(:show)
502 503 504
                  put    :update if resource.actions.include?(:update)
                  delete :destroy if resource.actions.include?(:destroy)
                  get    :edit, :as => resource.singular if resource.actions.include?(:edit)
J
Joshua Peek 已提交
505
                end
506 507 508 509
              end
            end
          end

J
Joshua Peek 已提交
510
          self
511 512
        end

J
Joshua Peek 已提交
513 514 515
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
516 517
          end

J
Joshua Peek 已提交
518
          with_scope_level(:collection) do
519
            scope(:name_prefix => parent_resource.collection_name, :as => "") do
520 521
              yield
            end
J
Joshua Peek 已提交
522
          end
523
        end
J
Joshua Peek 已提交
524

J
Joshua Peek 已提交
525 526 527 528
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
529

J
Joshua Peek 已提交
530
          with_scope_level(:member) do
531
            scope(':id', :name_prefix => parent_resource.member_name, :as => "") do
J
Joshua Peek 已提交
532 533 534
              yield
            end
          end
J
Joshua Peek 已提交
535 536
        end

537 538 539 540 541 542
        def nested
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use nested outside resources scope"
          end

          with_scope_level(:nested) do
543
            scope(parent_resource.id_segment, :name_prefix => parent_resource.member_name) do
544 545 546 547 548
              yield
            end
          end
        end

549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
        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

J
Joshua Peek 已提交
564
        def match(*args)
J
Joshua Peek 已提交
565
          options = args.extract_options!
566

567 568
          options[:anchor] = true unless options.key?(:anchor)

569 570 571 572 573
          if args.length > 1
            args.each { |path| match(path, options) }
            return self
          end

574 575
          resources_path_names = options.delete(:path_names)

576
          if args.first.is_a?(Symbol)
577 578
            action = args.first
            if CRUD_ACTIONS.include?(action)
579 580 581
              begin
                old_path = @scope[:path]
                @scope[:path] = "#{@scope[:path]}(.:format)"
582 583 584 585
                return match(options.reverse_merge(
                  :to => action,
                  :as => parent_resource.name_for_action(action)
                ))
586 587 588
              ensure
                @scope[:path] = old_path
              end
589 590
            else
              with_exclusive_name_prefix(action) do
591
                return match("#{action_path(action, resources_path_names)}(.:format)", options.reverse_merge(:to => action))
592
              end
593 594 595
            end
          end

J
Joshua Peek 已提交
596
          args.push(options)
J
Joshua Peek 已提交
597

J
Joshua Peek 已提交
598 599 600 601 602 603
          case options.delete(:on)
          when :collection
            return collection { match(*args) }
          when :member
            return member { match(*args) }
          end
J
Joshua Peek 已提交
604

J
Joshua Peek 已提交
605 606 607
          if @scope[:scope_level] == :resources
            raise ArgumentError, "can't define route directly in resources scope"
          end
J
Joshua Peek 已提交
608

J
Joshua Peek 已提交
609
          super
J
Joshua Peek 已提交
610 611
        end

612 613 614 615 616
        protected
          def parent_resource
            @scope[:scope_level_resource]
          end

J
Joshua Peek 已提交
617
        private
618 619 620 621 622
          def action_path(name, path_names = nil)
            path_names ||= @scope[:resources_path_names]
            path_names[name.to_sym] || name.to_s
          end

623
          def apply_common_behavior_for(method, resources, options, &block)
624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
            if resources.length > 1
              resources.each { |r| send(method, r, options, &block) }
              return true
            end

            if path_names = options.delete(:path_names)
              scope(:resources_path_names => path_names) do
                send(method, resources.pop, options, &block)
              end
              return true
            end

            if @scope[:scope_level] == :resources
              nested do
                send(method, resources.pop, options, &block)
              end
              return true
            end

            false
          end

646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
          def with_exclusive_name_prefix(prefix)
            begin
              old_name_prefix = @scope[:name_prefix]

              if !old_name_prefix.blank?
                @scope[:name_prefix] = "#{prefix}_#{@scope[:name_prefix]}"
              else
                @scope[:name_prefix] = prefix.to_s
              end

              yield
            ensure
              @scope[:name_prefix] = old_name_prefix
            end
          end

662
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
663
            old, @scope[:scope_level] = @scope[:scope_level], kind
664
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
665 666 667
            yield
          ensure
            @scope[:scope_level] = old
668
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
669 670
          end
      end
J
Joshua Peek 已提交
671

672 673 674 675
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
676 677
    end
  end
J
Joshua Peek 已提交
678
end