mapper.rb 15.6 KB
Newer Older
J
Joshua Peek 已提交
1 2
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
3
    class Mapper
4
      class Constraints
5
        def self.new(app, constraints = [])
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
          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 已提交
22
              return [ 404, {'X-Cascade' => 'pass'}, [] ]
23
            elsif constraint.respond_to?(:call) && !constraint.call(req)
J
Joshua Peek 已提交
24
              return [ 404, {'X-Cascade' => 'pass'}, [] ]
25 26 27 28 29 30 31
            end
          }

          @app.call(env)
        end
      end

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

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

42 43
        private
          def extract_path_and_options(args)
44
            options = args.extract_options!
45

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

57
            [ normalize_path(path), options ]
58
          end
59

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

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

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

75
            raise ArgumentError, "path is required" unless path
J
Joshua Peek 已提交
76 77

            path
78
          end
79 80


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

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

92 93 94 95 96 97 98
          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
99

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

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

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

122 123 124
              defaults
            end
          end
125

J
Joshua Peek 已提交
126

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

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

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

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

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

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

160 161
          def default_controller
            @scope[:controller].to_s if @scope[:controller]
162
          end
163
      end
164

165 166 167 168
      module Base
        def initialize(set)
          @set = set
        end
169

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

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

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

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

          lambda do |env|
206 207
            req = Request.new(env)

208
            uri = URI.parse(path_proc.call(req.params.symbolize_keys))
209 210
            uri.scheme ||= req.scheme
            uri.host   ||= req.host
211
            uri.port   ||= req.port unless req.port == 80
212 213 214 215 216 217 218

            headers = {
              'Location' => uri.to_s,
              'Content-Type' => 'text/html',
              'Content-Length' => body.length.to_s
            }
            [ status, headers, [body] ]
219
          end
220 221 222 223 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
        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 已提交
250
            path, @scope[:path] = @scope[:path], Rack::Mount::Utils.normalize_path(@scope[:path].to_s + path.to_s)
251 252 253 254 255 256
          else
            path_set = false
          end

          if name_prefix = options.delete(:name_prefix)
            name_prefix_set = true
257
            name_prefix, @scope[:name_prefix] = @scope[:name_prefix], (@scope[:name_prefix] ? "#{@scope[:name_prefix]}_#{name_prefix}" : name_prefix)
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
          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
282
          @scope[:path]        = path        if path_set
283
          @scope[:name_prefix] = name_prefix if name_prefix_set
284 285 286
          @scope[:controller]  = controller  if controller_set
          @scope[:options]     = options
          @scope[:blocks]      = blocks
287 288 289 290 291 292 293 294
          @scope[:constraints] = constraints
        end

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

        def namespace(path)
295
          scope("/#{path}", :name_prefix => 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 316 317
          end

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

J
Joshua Peek 已提交
318
      module Resources
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
        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
338
            singular
339 340 341
          end

          def collection_name
342
            plural
343
          end
344 345 346 347

          def id_segment
            ":#{singular}_id"
          end
348 349 350 351
        end

        class SingletonResource < Resource #:nodoc:
          def initialize(entity, options = {})
352
            super
353 354 355 356 357 358 359
          end

          def name
            singular
          end
        end

J
Joshua Peek 已提交
360
        def resource(*resources, &block)
J
Joshua Peek 已提交
361
          options = resources.extract_options!
J
Joshua Peek 已提交
362 363 364 365 366 367

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

369
          resource = SingletonResource.new(resources.pop)
370

J
Joshua Peek 已提交
371
          if @scope[:scope_level] == :resources
372 373
            nested do
              resource(resource.name, options, &block)
374
            end
J
Joshua Peek 已提交
375
            return self
376 377
          end

J
Joshua Peek 已提交
378
          scope(:path => "/#{resource.name}", :controller => resource.controller) do
379 380 381
            with_scope_level(:resource, resource) do
              yield if block_given?

382 383 384 385 386 387
              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}"
388 389 390
            end
          end

J
Joshua Peek 已提交
391
          self
392 393
        end

J
Joshua Peek 已提交
394
        def resources(*resources, &block)
J
Joshua Peek 已提交
395
          options = resources.extract_options!
396

J
Joshua Peek 已提交
397 398 399 400
          if resources.length > 1
            raise ArgumentError if block_given?
            resources.each { |r| resources(r, options) }
            return self
401 402
          end

403
          resource = Resource.new(resources.pop)
404

J
Joshua Peek 已提交
405
          if @scope[:scope_level] == :resources
406 407
            nested do
              resources(resource.name, options, &block)
408
            end
J
Joshua Peek 已提交
409
            return self
410 411
          end

J
Joshua Peek 已提交
412
          scope(:path => "/#{resource.name}", :controller => resource.controller) do
413 414
            with_scope_level(:resources, resource) do
              yield if block_given?
J
Joshua Peek 已提交
415

416
              with_scope_level(:collection) do
417
                get  "(.:format)", :to => :index, :as => resource.collection_name
J
Joshua Peek 已提交
418
                post "(.:format)", :to => :create
419

420 421 422
                with_exclusive_name_prefix :new do
                  get "/new(.:format)", :to => :new, :as => resource.singular
                end
423
              end
424

425
              with_scope_level(:member) do
J
Joshua Peek 已提交
426
                scope("/:id") do
427 428
                  get    "(.:format)", :to => :show, :as => resource.member_name
                  put    "(.:format)", :to => :update
J
Joshua Peek 已提交
429
                  delete "(.:format)", :to => :destroy
430

431 432 433
                  with_exclusive_name_prefix :edit do
                    get "/edit(.:format)", :to => :edit, :as => resource.singular
                  end
J
Joshua Peek 已提交
434
                end
435 436 437 438
              end
            end
          end

J
Joshua Peek 已提交
439
          self
440 441
        end

J
Joshua Peek 已提交
442 443 444
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
445 446
          end

J
Joshua Peek 已提交
447
          with_scope_level(:collection) do
448
            scope(:name_prefix => parent_resource.collection_name, :as => "") do
449 450
              yield
            end
J
Joshua Peek 已提交
451
          end
452
        end
J
Joshua Peek 已提交
453

J
Joshua Peek 已提交
454 455 456 457
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
458

J
Joshua Peek 已提交
459
          with_scope_level(:member) do
J
Joshua Peek 已提交
460
            scope("/:id", :name_prefix => parent_resource.member_name, :as => "") do
J
Joshua Peek 已提交
461 462 463
              yield
            end
          end
J
Joshua Peek 已提交
464 465
        end

466 467 468 469 470 471
        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 已提交
472
            scope("/#{parent_resource.id_segment}", :name_prefix => parent_resource.member_name) do
473 474 475 476 477
              yield
            end
          end
        end

J
Joshua Peek 已提交
478
        def match(*args)
J
Joshua Peek 已提交
479
          options = args.extract_options!
480 481 482 483 484 485 486

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

          if args.first.is_a?(Symbol)
487
            with_exclusive_name_prefix(args.first) do
J
Joshua Peek 已提交
488
              return match("/#{args.first}(.:format)", options.merge(:to => args.first.to_sym))
489 490 491
            end
          end

J
Joshua Peek 已提交
492
          args.push(options)
J
Joshua Peek 已提交
493

J
Joshua Peek 已提交
494 495 496 497 498 499
          case options.delete(:on)
          when :collection
            return collection { match(*args) }
          when :member
            return member { match(*args) }
          end
J
Joshua Peek 已提交
500

J
Joshua Peek 已提交
501 502 503
          if @scope[:scope_level] == :resources
            raise ArgumentError, "can't define route directly in resources scope"
          end
J
Joshua Peek 已提交
504

J
Joshua Peek 已提交
505
          super
J
Joshua Peek 已提交
506 507
        end

508 509 510 511 512
        protected
          def parent_resource
            @scope[:scope_level_resource]
          end

J
Joshua Peek 已提交
513
        private
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
          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

530
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
531
            old, @scope[:scope_level] = @scope[:scope_level], kind
532
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
533 534 535
            yield
          ensure
            @scope[:scope_level] = old
536
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
537 538
          end
      end
J
Joshua Peek 已提交
539

540 541 542 543
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
544 545
    end
  end
J
Joshua Peek 已提交
546
end