mapper.rb 15.4 KB
Newer Older
1 2
require 'active_support/core_ext/enumberable'

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 41
        def to_route
          [ app, conditions, requirements, defaults, @options[:as] ]
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 66 67 68 69 70
          
          # match "account" => "account#index"
          def using_to_shorthand?(args, options)
            args.empty? && options.present?
          end
          
          # match "account/overview"
          def using_match_shorthand?(args, options)
            args.present? && options.except(:via).empty? && args.first.exclude?(":")
          end
71

72 73 74
          def normalize_path(path)
            path = nil if path == ""
            path = "#{@scope[:path]}#{path}" if @scope[:path]
J
Joshua Peek 已提交
75
            path = Rack::Mount::Utils.normalize_path(path) if path
76

77
            raise ArgumentError, "path is required" unless path
J
Joshua Peek 已提交
78 79

            path
80
          end
81 82


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

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

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

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

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

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

124 125 126
              defaults
            end
          end
127

J
Joshua Peek 已提交
128

129 130 131 132 133 134
          def blocks
            if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
              block = @options[:constraints]
            else
              block = nil
            end
J
Joshua Peek 已提交
135 136

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

139 140 141
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
142

143 144 145 146 147 148
          def request_method_condition
            if via = @options[:via]
              via = Array(via).map { |m| m.to_s.upcase }
              { :request_method => Regexp.union(*via) }
            else
              { }
149
            end
150
          end
J
Joshua Peek 已提交
151

152 153 154 155 156
          def segment_keys
            @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
                Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
              ).names
          end
157

158 159 160
          def to
            @options[:to]
          end
J
Joshua Peek 已提交
161

162 163
          def default_controller
            @scope[:controller].to_s if @scope[:controller]
164
          end
165
      end
166

167 168 169 170
      module Base
        def initialize(set)
          @set = set
        end
171

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

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

199 200 201
        def redirect(*args, &block)
          options = args.last.is_a?(Hash) ? args.pop : {}

202 203 204
          path      = args.shift || block
          path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
          status    = options[:status] || 301
205 206

          lambda do |env|
207
            req    = Rack::Request.new(env)
208
            params = path_proc.call(env["action_dispatch.request.path_parameters"])
209 210 211
            url    = req.scheme + '://' + req.host + params

            [ status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently'] ]
212
          end
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
        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

          if path = options.delete(:path)
            path_set = true
J
Joshua Peek 已提交
243
            path, @scope[:path] = @scope[:path], Rack::Mount::Utils.normalize_path(@scope[:path].to_s + path.to_s)
244 245 246 247 248 249
          else
            path_set = false
          end

          if name_prefix = options.delete(:name_prefix)
            name_prefix_set = true
250
            name_prefix, @scope[:name_prefix] = @scope[:name_prefix], (@scope[:name_prefix] ? "#{@scope[:name_prefix]}_#{name_prefix}" : name_prefix)
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
          else
            name_prefix_set = false
          end

          if controller = options.delete(:controller)
            controller_set = true
            controller, @scope[:controller] = @scope[:controller], controller
          else
            controller_set = false
          end

          constraints = options.delete(:constraints) || {}
          unless constraints.is_a?(Hash)
            block, constraints = constraints, {}
          end
          constraints, @scope[:constraints] = @scope[:constraints], (@scope[:constraints] || {}).merge(constraints)
          blocks, @scope[:blocks] = @scope[:blocks], (@scope[:blocks] || []) + [block]

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

          yield

          self
        ensure
275
          @scope[:path]        = path        if path_set
276
          @scope[:name_prefix] = name_prefix if name_prefix_set
277 278 279
          @scope[:controller]  = controller  if controller_set
          @scope[:options]     = options
          @scope[:blocks]      = blocks
280 281 282 283 284 285 286 287
          @scope[:constraints] = constraints
        end

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

        def namespace(path)
J
Joshua Peek 已提交
288
          scope("/#{path}") { yield }
289 290 291 292 293 294 295 296 297 298 299
        end

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

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

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

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

          args.push(options)
          super(*args)
        end
      end

J
Joshua Peek 已提交
311
      module Resources
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
        class Resource #:nodoc:
          attr_reader :plural, :singular

          def initialize(entities, options = {})
            entities = entities.to_s

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

          def name
            plural
          end

          def controller
            plural
          end

          def member_name
331
            singular
332 333 334
          end

          def collection_name
335
            plural
336
          end
337 338 339 340

          def id_segment
            ":#{singular}_id"
          end
341 342 343 344
        end

        class SingletonResource < Resource #:nodoc:
          def initialize(entity, options = {})
345
            super
346 347 348 349 350 351 352
          end

          def name
            singular
          end
        end

J
Joshua Peek 已提交
353
        def resource(*resources, &block)
J
Joshua Peek 已提交
354
          options = resources.extract_options!
J
Joshua Peek 已提交
355 356 357 358 359 360

          if resources.length > 1
            raise ArgumentError if block_given?
            resources.each { |r| resource(r, options) }
            return self
          end
361

362
          resource = SingletonResource.new(resources.pop)
363

J
Joshua Peek 已提交
364
          if @scope[:scope_level] == :resources
365 366
            nested do
              resource(resource.name, options, &block)
367
            end
J
Joshua Peek 已提交
368
            return self
369 370
          end

J
Joshua Peek 已提交
371
          scope(:path => "/#{resource.name}", :controller => resource.controller) do
372 373 374
            with_scope_level(:resource, resource) do
              yield if block_given?

375 376 377 378 379 380
              get    "(.:format)",      :to => :show, :as => resource.member_name
              post   "(.:format)",      :to => :create
              put    "(.:format)",      :to => :update
              delete "(.:format)",      :to => :destroy
              get    "/new(.:format)",  :to => :new,  :as => "new_#{resource.singular}"
              get    "/edit(.:format)", :to => :edit, :as => "edit_#{resource.singular}"
381 382 383
            end
          end

J
Joshua Peek 已提交
384
          self
385 386
        end

J
Joshua Peek 已提交
387
        def resources(*resources, &block)
J
Joshua Peek 已提交
388
          options = resources.extract_options!
389

J
Joshua Peek 已提交
390 391 392 393
          if resources.length > 1
            raise ArgumentError if block_given?
            resources.each { |r| resources(r, options) }
            return self
394 395
          end

396
          resource = Resource.new(resources.pop)
397

J
Joshua Peek 已提交
398
          if @scope[:scope_level] == :resources
399 400
            nested do
              resources(resource.name, options, &block)
401
            end
J
Joshua Peek 已提交
402
            return self
403 404
          end

J
Joshua Peek 已提交
405
          scope(:path => "/#{resource.name}", :controller => resource.controller) do
406 407
            with_scope_level(:resources, resource) do
              yield if block_given?
J
Joshua Peek 已提交
408

409
              with_scope_level(:collection) do
410
                get  "(.:format)", :to => :index, :as => resource.collection_name
J
Joshua Peek 已提交
411
                post "(.:format)", :to => :create
412

413 414 415
                with_exclusive_name_prefix :new do
                  get "/new(.:format)", :to => :new, :as => resource.singular
                end
416
              end
417

418
              with_scope_level(:member) do
J
Joshua Peek 已提交
419
                scope("/:id") do
420 421
                  get    "(.:format)", :to => :show, :as => resource.member_name
                  put    "(.:format)", :to => :update
J
Joshua Peek 已提交
422
                  delete "(.:format)", :to => :destroy
423

424 425 426
                  with_exclusive_name_prefix :edit do
                    get "/edit(.:format)", :to => :edit, :as => resource.singular
                  end
J
Joshua Peek 已提交
427
                end
428 429 430 431
              end
            end
          end

J
Joshua Peek 已提交
432
          self
433 434
        end

J
Joshua Peek 已提交
435 436 437
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
438 439
          end

J
Joshua Peek 已提交
440
          with_scope_level(:collection) do
441
            scope(:name_prefix => parent_resource.collection_name, :as => "") do
442 443
              yield
            end
J
Joshua Peek 已提交
444
          end
445
        end
J
Joshua Peek 已提交
446

J
Joshua Peek 已提交
447 448 449 450
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
451

J
Joshua Peek 已提交
452
          with_scope_level(:member) do
J
Joshua Peek 已提交
453
            scope("/:id", :name_prefix => parent_resource.member_name, :as => "") do
J
Joshua Peek 已提交
454 455 456
              yield
            end
          end
J
Joshua Peek 已提交
457 458
        end

459 460 461 462 463 464
        def nested
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use nested outside resources scope"
          end

          with_scope_level(:nested) do
J
Joshua Peek 已提交
465
            scope("/#{parent_resource.id_segment}", :name_prefix => parent_resource.member_name) do
466 467 468 469 470
              yield
            end
          end
        end

J
Joshua Peek 已提交
471
        def match(*args)
J
Joshua Peek 已提交
472
          options = args.extract_options!
473 474 475 476 477 478 479

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

          if args.first.is_a?(Symbol)
480
            with_exclusive_name_prefix(args.first) do
J
Joshua Peek 已提交
481
              return match("/#{args.first}(.:format)", options.merge(:to => args.first.to_sym))
482 483 484
            end
          end

J
Joshua Peek 已提交
485
          args.push(options)
J
Joshua Peek 已提交
486

J
Joshua Peek 已提交
487 488 489 490 491 492
          case options.delete(:on)
          when :collection
            return collection { match(*args) }
          when :member
            return member { match(*args) }
          end
J
Joshua Peek 已提交
493

J
Joshua Peek 已提交
494 495 496
          if @scope[:scope_level] == :resources
            raise ArgumentError, "can't define route directly in resources scope"
          end
J
Joshua Peek 已提交
497

J
Joshua Peek 已提交
498
          super
J
Joshua Peek 已提交
499 500
        end

501 502 503 504 505
        protected
          def parent_resource
            @scope[:scope_level_resource]
          end

J
Joshua Peek 已提交
506
        private
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
          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

523
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
524
            old, @scope[:scope_level] = @scope[:scope_level], kind
525
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
526 527 528
            yield
          ensure
            @scope[:scope_level] = old
529
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
530 531
          end
      end
J
Joshua Peek 已提交
532

533 534 535 536
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
537 538
    end
  end
J
Joshua Peek 已提交
539
end