mapper.rb 20.1 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
      class Mapping
35 36
        IGNORE_OPTIONS = [:to, :as, :controller, :action, :via, :on, :constraints, :defaults, :only, :except, :anchor]

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

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

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

50
            if using_to_shorthand?(args, options)
51 52 53 54 55
              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 已提交
56

57 58 59
            path = normalize_path(path)

            if using_match_shorthand?(path, options)
60 61
              options[:to] ||= path[1..-1].sub(%r{/([^/]*)$}, '#\1')
              options[:as] ||= path[1..-1].gsub("/", "_")
62 63 64
            end

            [ path, options ]
65
          end
66

67 68 69 70
          # match "account" => "account#index"
          def using_to_shorthand?(args, options)
            args.empty? && options.present?
          end
71

72
          # match "account/overview"
73
          def using_match_shorthand?(path, options)
74
            path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w\/]+$}
75
          end
76

77
          def normalize_path(path)
78 79
            raise ArgumentError, "path is required" if @scope[:path].blank? && path.blank?
            Mapper.normalize_path("#{@scope[:path]}/#{path}")
80
          end
81

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

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

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

100
          def defaults
101 102 103 104 105 106 107 108 109
            @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)
110 111 112 113 114 115 116
              { }
            else
              defaults = case to
              when String
                controller, action = to.split('#')
                { :controller => controller, :action => action }
              when Symbol
117
                { :action => to.to_s }.merge(default_controller ? { :controller => default_controller } : {})
118
              else
119
                default_controller ? { :controller => default_controller } : {}
120
              end
J
Joshua Peek 已提交
121

122 123 124
              if defaults[:controller].blank? && segment_keys.exclude?("controller")
                raise ArgumentError, "missing :controller"
              end
J
Joshua Peek 已提交
125

126 127 128
              if defaults[:action].blank? && segment_keys.exclude?("action")
                raise ArgumentError, "missing :action"
              end
J
Joshua Peek 已提交
129

130 131 132
              defaults
            end
          end
133

134 135 136 137 138 139
          def blocks
            if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
              block = @options[:constraints]
            else
              block = nil
            end
J
Joshua Peek 已提交
140 141

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

144 145 146
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
147

148 149 150 151 152 153
          def request_method_condition
            if via = @options[:via]
              via = Array(via).map { |m| m.to_s.upcase }
              { :request_method => Regexp.union(*via) }
            else
              { }
154
            end
155
          end
J
Joshua Peek 已提交
156

157 158
          def segment_keys
            @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
159 160
              Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
            ).names
161
          end
162

163 164 165
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
166

167 168
          def default_controller
            @scope[:controller].to_s if @scope[:controller]
169
          end
170
      end
171

172
      # Invokes Rack::Mount::Utils.normalize path and ensure that
173 174
      # (:locale) becomes (/:locale) instead of /(:locale). Except
      # for root cases, where the latter is the correct one.
175 176
      def self.normalize_path(path)
        path = Rack::Mount::Utils.normalize_path(path)
177
        path.sub!(%r{/(\(+)/?:}, '\1/:') unless path =~ %r{^/\(+:.*\)$}
178 179 180
        path
      end

181 182 183 184
      module Base
        def initialize(set)
          @set = set
        end
185

186 187 188
        def root(options = {})
          match '/', options.reverse_merge(:as => :root)
        end
189

190
        def match(*args)
191 192
          mapping = Mapping.new(@set, @scope, args).to_route
          @set.add_route(*mapping)
193 194
          self
        end
195 196 197 198 199

        def default_url_options=(options)
          @set.default_url_options = options
        end
        alias_method :default_url_options, :default_url_options=
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
      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

219 220 221
        def redirect(*args, &block)
          options = args.last.is_a?(Hash) ? args.pop : {}

222 223 224
          path      = args.shift || block
          path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
          status    = options[:status] || 301
225
          body      = 'Moved Permanently'
226 227

          lambda do |env|
228
            req = Request.new(env)
229
            uri = URI.parse(path_proc.call(req.symbolized_path_parameters))
230 231
            uri.scheme ||= req.scheme
            uri.host   ||= req.host
232
            uri.port   ||= req.port unless req.port == 80
233 234 235 236 237 238 239

            headers = {
              'Location' => uri.to_s,
              'Content-Type' => 'text/html',
              'Content-Length' => body.length.to_s
            }
            [ status, headers, [body] ]
240
          end
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
        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

269
          recover = {}
270

271 272 273
          options[:constraints] ||= {}
          unless options[:constraints].is_a?(Hash)
            block, options[:constraints] = options[:constraints], {}
274
          end
275

276 277 278 279 280
          scope_options.each do |option|
            if value = options.delete(option)
              recover[option] = @scope[option]
              @scope[option]  = send("merge_#{option}_scope", @scope[option], value)
            end
281 282
          end

283 284
          recover[:block] = @scope[:blocks]
          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
285

286 287
          recover[:options] = @scope[:options]
          @scope[:options]  = merge_options_scope(@scope[:options], options)
288 289 290 291

          yield
          self
        ensure
292 293 294 295 296 297
          scope_options.each do |option|
            @scope[option] = recover[option] if recover.has_key?(option)
          end

          @scope[:options] = recover[:options]
          @scope[:blocks]  = recover[:block]
298 299 300 301 302 303 304
        end

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

        def namespace(path)
305
          scope(path.to_s, :name_prefix => path.to_s, :controller_namespace => path.to_s) { yield }
306 307 308 309 310 311
        end

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

312 313 314 315
        def defaults(defaults = {})
          scope(:defaults => defaults) { yield }
        end

316 317 318 319 320
        def match(*args)
          options = args.extract_options!

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

321 322
          if @scope[:name_prefix] && !options[:as].blank?
            options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}"
323
          elsif @scope[:name_prefix] && options[:as] == ""
324
            options[:as] = @scope[:name_prefix].to_s
325 326 327 328 329
          end

          args.push(options)
          super(*args)
        end
330 331 332 333 334 335 336

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

          def merge_path_scope(parent, child)
337
            Mapper.normalize_path("#{parent}/#{child}")
338 339 340 341 342 343
          end

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

344
          def merge_controller_namespace_scope(parent, child)
345 346 347 348
            parent ? "#{parent}/#{child}" : child
          end

          def merge_controller_scope(parent, child)
349
            @scope[:controller_namespace] ? "#{@scope[:controller_namespace]}/#{child}" : child
350 351 352 353 354 355 356 357 358 359
          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

360 361 362 363
          def merge_defaults_scope(parent, child)
            merge_options_scope(parent, child)
          end

364 365 366 367 368 369 370
          def merge_blocks_scope(parent, child)
            (parent || []) + [child]
          end

          def merge_options_scope(parent, child)
            (parent || {}).merge(child)
          end
371 372
      end

J
Joshua Peek 已提交
373
      module Resources
374
        CRUD_ACTIONS = [:index, :show, :create, :update, :destroy]
375

376
        class Resource #:nodoc:
377 378 379 380 381
          def self.default_actions
            [:index, :create, :new, :show, :update, :destroy, :edit]
          end

          attr_reader :plural, :singular, :options
382 383

          def initialize(entities, options = {})
384
            @name = entities.to_s
385
            @options = options
386

387 388
            @plural   = @name.pluralize
            @singular = @name.singularize
389 390
          end

391 392 393 394 395 396
          def default_actions
            self.class.default_actions
          end

          def actions
            if only = options[:only]
397
              Array(only).map(&:to_sym)
398
            elsif except = options[:except]
399
              default_actions - Array(except).map(&:to_sym)
400 401 402 403 404
            else
              default_actions
            end
          end

405 406 407 408 409 410 411 412 413
          def action_type(action)
            case action
            when :index, :create
              :collection
            when :show, :update, :destroy
              :member
            end
          end

414
          def name
415
            options[:as] || @name
416 417 418
          end

          def controller
419
            options[:controller] || plural
420 421 422
          end

          def member_name
423
            singular
424 425 426
          end

          def collection_name
427
            plural
428
          end
429

430 431 432 433 434 435 436 437 438
          def name_for_action(action)
            case action_type(action)
            when :collection
              collection_name
            when :member
              member_name
            end
          end

439 440 441
          def id_segment
            ":#{singular}_id"
          end
442 443 444
        end

        class SingletonResource < Resource #:nodoc:
445 446 447 448
          def self.default_actions
            [:show, :create, :update, :destroy, :new, :edit]
          end

449
          def initialize(entity, options = {})
450
            super
451 452
          end

453 454 455 456 457 458 459
          def action_type(action)
            case action
            when :show, :create, :update, :destroy
              :member
            end
          end

460 461
          def member_name
            name
462 463 464
          end
        end

465 466 467 468 469
        def initialize(*args)
          super
          @scope[:resources_path_names] = @set.resources_path_names
        end

J
Joshua Peek 已提交
470
        def resource(*resources, &block)
J
Joshua Peek 已提交
471
          options = resources.extract_options!
J
Joshua Peek 已提交
472

473
          if apply_common_behavior_for(:resource, resources, options, &block)
474 475 476
            return self
          end

477
          resource = SingletonResource.new(resources.pop, options)
478

479
          scope(:path => resource.name.to_s, :controller => resource.controller) do
480
            with_scope_level(:resource, resource) do
481

482
              scope(:name_prefix => resource.name.to_s, :as => "") do
483 484
                yield if block_given?
              end
485

486
              get    :show if resource.actions.include?(:show)
487 488 489
              post   :create if resource.actions.include?(:create)
              put    :update if resource.actions.include?(:update)
              delete :destroy if resource.actions.include?(:destroy)
490 491
              get    :new, :as => resource.name if resource.actions.include?(:new)
              get    :edit, :as => resource.name if resource.actions.include?(:edit)
492 493 494
            end
          end

J
Joshua Peek 已提交
495
          self
496 497
        end

J
Joshua Peek 已提交
498
        def resources(*resources, &block)
J
Joshua Peek 已提交
499
          options = resources.extract_options!
500

501
          if apply_common_behavior_for(:resources, resources, options, &block)
502 503 504
            return self
          end

505
          resource = Resource.new(resources.pop, options)
506

507
          scope(:path => resource.name.to_s, :controller => resource.controller) do
508 509
            with_scope_level(:resources, resource) do
              yield if block_given?
J
Joshua Peek 已提交
510

511
              with_scope_level(:collection) do
512
                get  :index if resource.actions.include?(:index)
513 514
                post :create if resource.actions.include?(:create)
                get  :new, :as => resource.singular if resource.actions.include?(:new)
515
              end
516

517
              with_scope_level(:member) do
518
                scope(':id') do
519
                  get    :show if resource.actions.include?(:show)
520 521 522
                  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 已提交
523
                end
524 525 526 527
              end
            end
          end

J
Joshua Peek 已提交
528
          self
529 530
        end

J
Joshua Peek 已提交
531 532 533
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
534 535
          end

J
Joshua Peek 已提交
536
          with_scope_level(:collection) do
537
            scope(:name_prefix => parent_resource.collection_name, :as => "") do
538 539
              yield
            end
J
Joshua Peek 已提交
540
          end
541
        end
J
Joshua Peek 已提交
542

J
Joshua Peek 已提交
543 544 545 546
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
547

J
Joshua Peek 已提交
548
          with_scope_level(:member) do
549
            scope(':id', :name_prefix => parent_resource.member_name, :as => "") do
J
Joshua Peek 已提交
550 551 552
              yield
            end
          end
J
Joshua Peek 已提交
553 554
        end

555 556 557 558 559 560
        def nested
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use nested outside resources scope"
          end

          with_scope_level(:nested) do
561
            scope(parent_resource.id_segment, :name_prefix => parent_resource.member_name) do
562 563 564 565 566
              yield
            end
          end
        end

567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
        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 已提交
582
        def match(*args)
J
Joshua Peek 已提交
583
          options = args.extract_options!
584

585 586
          options[:anchor] = true unless options.key?(:anchor)

587 588 589 590 591
          if args.length > 1
            args.each { |path| match(path, options) }
            return self
          end

592 593
          resources_path_names = options.delete(:path_names)

594
          if args.first.is_a?(Symbol)
595 596
            action = args.first
            if CRUD_ACTIONS.include?(action)
597 598 599
              begin
                old_path = @scope[:path]
                @scope[:path] = "#{@scope[:path]}(.:format)"
600 601 602 603
                return match(options.reverse_merge(
                  :to => action,
                  :as => parent_resource.name_for_action(action)
                ))
604 605 606
              ensure
                @scope[:path] = old_path
              end
607 608
            else
              with_exclusive_name_prefix(action) do
609
                return match("#{action_path(action, resources_path_names)}(.:format)", options.reverse_merge(:to => action))
610
              end
611 612 613
            end
          end

J
Joshua Peek 已提交
614
          args.push(options)
J
Joshua Peek 已提交
615

J
Joshua Peek 已提交
616 617 618 619 620 621
          case options.delete(:on)
          when :collection
            return collection { match(*args) }
          when :member
            return member { match(*args) }
          end
J
Joshua Peek 已提交
622

J
Joshua Peek 已提交
623 624 625
          if @scope[:scope_level] == :resources
            raise ArgumentError, "can't define route directly in resources scope"
          end
J
Joshua Peek 已提交
626

J
Joshua Peek 已提交
627
          super
J
Joshua Peek 已提交
628 629
        end

630 631 632 633 634
        protected
          def parent_resource
            @scope[:scope_level_resource]
          end

J
Joshua Peek 已提交
635
        private
636 637 638 639 640
          def action_path(name, path_names = nil)
            path_names ||= @scope[:resources_path_names]
            path_names[name.to_sym] || name.to_s
          end

641
          def apply_common_behavior_for(method, resources, options, &block)
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
            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

664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679
          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

680
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
681
            old, @scope[:scope_level] = @scope[:scope_level], kind
682
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
683 684 685
            yield
          ensure
            @scope[:scope_level] = old
686
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
687 688
          end
      end
J
Joshua Peek 已提交
689

690 691 692 693
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
694 695
    end
  end
J
Joshua Peek 已提交
696
end