mapper.rb 19.2 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 54
            when using_match_shorthand?(args, options)
              path = args.first
              options = { :to => path.gsub("/", "#"), :as => path.gsub("/", "_") }
55 56 57
            else
              path = args.first
            end
J
Joshua Peek 已提交
58

59
            [ normalize_path(path), options ]
60
          end
61

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

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

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

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

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

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

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

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

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

118 119 120
              defaults
            end
          end
121

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

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

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

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

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

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

155 156
          def default_controller
            @scope[:controller].to_s if @scope[:controller]
157
          end
158
      end
159

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

169 170 171 172
      module Base
        def initialize(set)
          @set = set
        end
173

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

178
        def match(*args)
179 180
          mapping = Mapping.new(@set, @scope, args).to_route
          @set.add_route(*mapping)
181 182
          self
        end
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
      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

202 203 204
        def redirect(*args, &block)
          options = args.last.is_a?(Hash) ? args.pop : {}

205 206 207
          path      = args.shift || block
          path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
          status    = options[:status] || 301
208
          body      = 'Moved Permanently'
209 210

          lambda do |env|
211
            req = Request.new(env)
212
            uri = URI.parse(path_proc.call(req.symbolized_path_parameters))
213 214
            uri.scheme ||= req.scheme
            uri.host   ||= req.host
215
            uri.port   ||= req.port unless req.port == 80
216 217 218 219 220 221 222

            headers = {
              'Location' => uri.to_s,
              'Content-Type' => 'text/html',
              'Content-Length' => body.length.to_s
            }
            [ status, headers, [body] ]
223
          end
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
        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

252
          recover = {}
253

254 255 256
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
            block, options[:constraints] = options[:constraints], {}
257
          end
258

259 260 261 262 263
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
264 265
          end

266 267
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
268

269 270
          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)
271 272 273 274

          yield
          self
        ensure
275 276 277 278 279 280
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
281 282 283 284 285 286 287
        end

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

        def namespace(path)
288
          scope(path.to_s, :name_prefix => path.to_s, :controller_namespace => path.to_s) { yield }
289 290 291 292 293 294 295 296 297 298
        end

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

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

          options = (@scope[:options] || {}).merge(options)
299
          options[:anchor] = true unless options.key?(:anchor)
300

301 302
          if @scope[:name_prefix] && !options[:as].blank?
            options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}"
303
          elsif @scope[:name_prefix] && options[:as] == ""
304
            options[:as] = @scope[:name_prefix].to_s
305 306 307 308 309
          end

          args.push(options)
          super(*args)
        end
310 311 312 313 314 315 316

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

          def merge_path_scope(parent, child)
317
            Mapper.normalize_path("#{parent}/#{child}")
318 319 320 321 322 323
          end

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

324
          def merge_controller_namespace_scope(parent, child)
325 326 327 328
            parent ? "#{parent}/#{child}" : child
          end

          def merge_controller_scope(parent, child)
329
            @scope[:controller_namespace] ? "#{@scope[:controller_namespace]}/#{child}" : child
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
          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
347 348
      end

J
Joshua Peek 已提交
349
      module Resources
350
        CRUD_ACTIONS = [:index, :show, :create, :update, :destroy]
351

352
        class Resource #:nodoc:
353 354 355 356 357
          def self.default_actions
            [:index, :create, :new, :show, :update, :destroy, :edit]
          end

          attr_reader :plural, :singular, :options
358 359 360

          def initialize(entities, options = {})
            entities = entities.to_s
361
            @options = options
362 363 364 365 366

            @plural   = entities.pluralize
            @singular = entities.singularize
          end

367 368 369 370 371 372
          def default_actions
            self.class.default_actions
          end

          def actions
            if only = options[:only]
373
              Array(only).map(&:to_sym)
374
            elsif except = options[:except]
375
              default_actions - Array(except).map(&:to_sym)
376 377 378 379 380
            else
              default_actions
            end
          end

381 382 383 384 385 386 387 388 389
          def action_type(action)
            case action
            when :index, :create
              :collection
            when :show, :update, :destroy
              :member
            end
          end

390
          def name
391
            options[:as] || plural
392 393 394
          end

          def controller
395
            options[:controller] || plural
396 397 398
          end

          def member_name
399
            singular
400 401 402
          end

          def collection_name
403
            plural
404
          end
405

406 407 408 409 410 411 412 413 414
          def name_for_action(action)
            case action_type(action)
            when :collection
              collection_name
            when :member
              member_name
            end
          end

415 416 417
          def id_segment
            ":#{singular}_id"
          end
418 419 420
        end

        class SingletonResource < Resource #:nodoc:
421 422 423 424
          def self.default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

425
          def initialize(entity, options = {})
426
            super
427 428
          end

429 430 431 432 433 434 435
          def action_type(action)
            case action
            when :show, :create, :update, :destroy
              :member
            end
          end

436
          def name
437
            options[:as] || singular
438 439 440
          end
        end

441 442 443 444 445
        def initialize(*args)
          super
          @scope[:resources_path_names] = @set.resources_path_names
        end

J
Joshua Peek 已提交
446
        def resource(*resources, &block)
J
Joshua Peek 已提交
447
          options = resources.extract_options!
J
Joshua Peek 已提交
448

449
          if apply_common_behavior_for(:resource, resources, options, &block)
450 451 452
            return self
          end

453
          resource = SingletonResource.new(resources.pop, options)
454

455
          scope(:path => resource.name.to_s, :controller => resource.controller) do
456
            with_scope_level(:resource, resource) do
457 458 459 460

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

462
              get    :show if resource.actions.include?(:show)
463 464 465
              post   :create if resource.actions.include?(:create)
              put    :update if resource.actions.include?(:update)
              delete :destroy if resource.actions.include?(:destroy)
466 467
              get    :new, :as => resource.singular if resource.actions.include?(:new)
              get    :edit, :as => resource.singular if resource.actions.include?(:edit)
468 469 470
            end
          end

J
Joshua Peek 已提交
471
          self
472 473
        end

J
Joshua Peek 已提交
474
        def resources(*resources, &block)
J
Joshua Peek 已提交
475
          options = resources.extract_options!
476

477
          if apply_common_behavior_for(:resources, resources, options, &block)
478 479 480
            return self
          end

481
          resource = Resource.new(resources.pop, options)
482

483
          scope(:path => resource.name.to_s, :controller => resource.controller) do
484 485
            with_scope_level(:resources, resource) do
              yield if block_given?
J
Joshua Peek 已提交
486

487
              with_scope_level(:collection) do
488
                get  :index if resource.actions.include?(:index)
489 490
                post :create if resource.actions.include?(:create)
                get  :new, :as => resource.singular if resource.actions.include?(:new)
491
              end
492

493
              with_scope_level(:member) do
494
                scope(':id') do
495
                  get    :show if resource.actions.include?(:show)
496 497 498
                  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 已提交
499
                end
500 501 502 503
              end
            end
          end

J
Joshua Peek 已提交
504
          self
505 506
        end

J
Joshua Peek 已提交
507 508 509
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
510 511
          end

J
Joshua Peek 已提交
512
          with_scope_level(:collection) do
513
            scope(:name_prefix => parent_resource.collection_name, :as => "") do
514 515
              yield
            end
J
Joshua Peek 已提交
516
          end
517
        end
J
Joshua Peek 已提交
518

J
Joshua Peek 已提交
519 520 521 522
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
523

J
Joshua Peek 已提交
524
          with_scope_level(:member) do
525
            scope(':id', :name_prefix => parent_resource.member_name, :as => "") do
J
Joshua Peek 已提交
526 527 528
              yield
            end
          end
J
Joshua Peek 已提交
529 530
        end

531 532 533 534 535 536
        def nested
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use nested outside resources scope"
          end

          with_scope_level(:nested) do
537
            scope(parent_resource.id_segment, :name_prefix => parent_resource.member_name) do
538 539 540 541 542
              yield
            end
          end
        end

543 544 545 546 547 548 549 550 551 552 553 554 555 556 557
        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 已提交
558
        def match(*args)
J
Joshua Peek 已提交
559
          options = args.extract_options!
560 561 562 563 564 565

          if args.length > 1
            args.each { |path| match(path, options) }
            return self
          end

566 567
          resources_path_names = options.delete(:path_names)

568
          if args.first.is_a?(Symbol)
569 570
            action = args.first
            if CRUD_ACTIONS.include?(action)
571 572 573
              begin
                old_path = @scope[:path]
                @scope[:path] = "#{@scope[:path]}(.:format)"
574 575 576 577
                return match(options.reverse_merge(
                  :to => action,
                  :as => parent_resource.name_for_action(action)
                ))
578 579 580
              ensure
                @scope[:path] = old_path
              end
581 582
            else
              with_exclusive_name_prefix(action) do
583
                return match("#{action_path(action, resources_path_names)}(.:format)", options.reverse_merge(:to => action))
584
              end
585 586 587
            end
          end

J
Joshua Peek 已提交
588
          args.push(options)
J
Joshua Peek 已提交
589

J
Joshua Peek 已提交
590 591 592 593 594 595
          case options.delete(:on)
          when :collection
            return collection { match(*args) }
          when :member
            return member { match(*args) }
          end
J
Joshua Peek 已提交
596

J
Joshua Peek 已提交
597 598 599
          if @scope[:scope_level] == :resources
            raise ArgumentError, "can't define route directly in resources scope"
          end
J
Joshua Peek 已提交
600

J
Joshua Peek 已提交
601
          super
J
Joshua Peek 已提交
602 603
        end

604 605 606 607 608
        protected
          def parent_resource
            @scope[:scope_level_resource]
          end

J
Joshua Peek 已提交
609
        private
610 611 612 613 614
          def action_path(name, path_names = nil)
            path_names ||= @scope[:resources_path_names]
            path_names[name.to_sym] || name.to_s
          end

615
          def apply_common_behavior_for(method, resources, options, &block)
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
            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

638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
          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

654
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
655
            old, @scope[:scope_level] = @scope[:scope_level], kind
656
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
657 658 659
            yield
          ensure
            @scope[:scope_level] = old
660
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
661 662
          end
      end
J
Joshua Peek 已提交
663

664 665 666 667
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
668 669
    end
  end
J
Joshua Peek 已提交
670
end