mapper.rb 14.9 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)
22
              return [ 417, {}, [] ]
23
            elsif constraint.respond_to?(:call) && !constraint.call(req)
24
              return [ 417, {}, [] ]
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
37 38 39
        
        def to_route
          [ app, conditions, requirements, defaults, @options[:as] ]
40
        end
41 42 43
        
        private
          def extract_path_and_options(args)
44
            options = args.extract_options!
45

46 47 48 49 50 51 52 53
            if args.empty?
              path, to = options.find { |name, value| name.is_a?(String) }
              options.merge!(:to => to).delete(path) if path
            else
              path = args.first
            end
          
            [ normalize_path(path), options ]
54 55
          end

56 57 58 59
          def normalize_path(path)
            path = nil if path == ""
            path = "#{@scope[:path]}#{path}" if @scope[:path]
            path = Rack::Mount::Utils.normalize_path(path) if path   
60

61 62 63 64
            raise ArgumentError, "path is required" unless path
          
            path         
          end
65 66


67 68 69 70 71
          def app
            Constraints.new(
              to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
              blocks
            )
72 73
          end

74 75 76 77 78 79 80 81 82 83 84
          def conditions
            { :path_info => @path }.merge(constraints).merge(request_method_condition)
          end
          
          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
85

86 87 88 89 90 91 92 93 94
          def defaults
            @defaults ||= if to.respond_to?(:call)
              { }
            else
              defaults = case to
              when String
                controller, action = to.split('#')
                { :controller => controller, :action => action }
              when Symbol
95
                { :action => to.to_s }.merge(default_controller ? { :controller => default_controller } : {})
96
              else
97
                default_controller ? { :controller => default_controller } : {}
98 99 100 101 102 103 104 105 106 107 108 109 110
              end
              
              if defaults[:controller].blank? && segment_keys.exclude?("controller")
                raise ArgumentError, "missing :controller"
              end
              
              if defaults[:action].blank? && segment_keys.exclude?("action")
                raise ArgumentError, "missing :action"
              end
              
              defaults
            end
          end
111

112 113 114 115 116 117 118 119 120 121 122 123 124 125
          
          def blocks
            if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
              block = @options[:constraints]
            else
              block = nil
            end
            
            ((@scope[:blocks] || []) + [ block ]).compact              
          end
        
          def constraints
            @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
          end
126

127 128 129 130 131 132
          def request_method_condition
            if via = @options[:via]
              via = Array(via).map { |m| m.to_s.upcase }
              { :request_method => Regexp.union(*via) }
            else
              { }
133
            end
134 135 136 137 138 139 140
          end
          
          def segment_keys
            @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
                Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
              ).names
          end
141

142 143 144 145 146 147
          def to
            @options[:to]
          end
          
          def default_controller
            @scope[:controller].to_s if @scope[:controller]
148
          end
149
      end
150

151 152 153 154
      module Base
        def initialize(set)
          @set = set
        end
155

156 157 158
        def root(options = {})
          match '/', options.reverse_merge(:as => :root)
        end
159

160 161 162 163
        def match(*args)
          @set.add_route(*Mapping.new(@set, @scope, args).to_route)
          self
        end
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
      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

183 184 185
        def redirect(*args, &block)
          options = args.last.is_a?(Hash) ? args.pop : {}

186 187 188
          path      = args.shift || block
          path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
          status    = options[:status] || 301
189 190

          lambda do |env|
191
            req    = Rack::Request.new(env)
192
            params = path_proc.call(env["action_dispatch.request.path_parameters"])
193 194 195
            url    = req.scheme + '://' + req.host + params

            [ status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently'] ]
196
          end
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
        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 已提交
227
            path, @scope[:path] = @scope[:path], Rack::Mount::Utils.normalize_path(@scope[:path].to_s + path.to_s)
228 229 230 231 232 233
          else
            path_set = false
          end

          if name_prefix = options.delete(:name_prefix)
            name_prefix_set = true
234
            name_prefix, @scope[:name_prefix] = @scope[:name_prefix], (@scope[:name_prefix] ? "#{@scope[:name_prefix]}_#{name_prefix}" : name_prefix)
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
          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
259
          @scope[:path]        = path        if path_set
260
          @scope[:name_prefix] = name_prefix if name_prefix_set
261 262 263
          @scope[:controller]  = controller  if controller_set
          @scope[:options]     = options
          @scope[:blocks]      = blocks
264 265 266 267 268 269 270 271
          @scope[:constraints] = constraints
        end

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

        def namespace(path)
J
Joshua Peek 已提交
272
          scope("/#{path}") { yield }
273 274 275 276 277 278 279 280 281 282 283
        end

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

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

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

284 285
          if @scope[:name_prefix] && !options[:as].blank?
            options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}"
286
          elsif @scope[:name_prefix] && options[:as] == ""
287
            options[:as] = @scope[:name_prefix].to_s
288 289 290 291 292 293 294
          end

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

J
Joshua Peek 已提交
295
      module Resources
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
        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
315
            singular
316 317 318
          end

          def collection_name
319
            plural
320
          end
321 322 323 324

          def id_segment
            ":#{singular}_id"
          end
325 326 327 328
        end

        class SingletonResource < Resource #:nodoc:
          def initialize(entity, options = {})
329
            super
330 331 332 333 334 335 336
          end

          def name
            singular
          end
        end

J
Joshua Peek 已提交
337
        def resource(*resources, &block)
J
Joshua Peek 已提交
338
          options = resources.extract_options!
J
Joshua Peek 已提交
339 340 341 342 343 344

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

346
          resource = SingletonResource.new(resources.pop)
347

J
Joshua Peek 已提交
348
          if @scope[:scope_level] == :resources
349 350
            nested do
              resource(resource.name, options, &block)
351
            end
J
Joshua Peek 已提交
352
            return self
353 354
          end

J
Joshua Peek 已提交
355
          scope(:path => "/#{resource.name}", :controller => resource.controller) do
356 357 358
            with_scope_level(:resource, resource) do
              yield if block_given?

359 360 361 362 363 364
              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}"
365 366 367
            end
          end

J
Joshua Peek 已提交
368
          self
369 370
        end

J
Joshua Peek 已提交
371
        def resources(*resources, &block)
J
Joshua Peek 已提交
372
          options = resources.extract_options!
373

J
Joshua Peek 已提交
374 375 376 377
          if resources.length > 1
            raise ArgumentError if block_given?
            resources.each { |r| resources(r, options) }
            return self
378 379
          end

380
          resource = Resource.new(resources.pop)
381

J
Joshua Peek 已提交
382
          if @scope[:scope_level] == :resources
383 384
            nested do
              resources(resource.name, options, &block)
385
            end
J
Joshua Peek 已提交
386
            return self
387 388
          end

J
Joshua Peek 已提交
389
          scope(:path => "/#{resource.name}", :controller => resource.controller) do
390 391
            with_scope_level(:resources, resource) do
              yield if block_given?
J
Joshua Peek 已提交
392

393
              with_scope_level(:collection) do
394
                get  "(.:format)", :to => :index, :as => resource.collection_name
J
Joshua Peek 已提交
395
                post "(.:format)", :to => :create
396

397 398 399
                with_exclusive_name_prefix :new do
                  get "/new(.:format)", :to => :new, :as => resource.singular
                end
400
              end
401

402
              with_scope_level(:member) do
J
Joshua Peek 已提交
403
                scope("/:id") do
404 405
                  get    "(.:format)", :to => :show, :as => resource.member_name
                  put    "(.:format)", :to => :update
J
Joshua Peek 已提交
406
                  delete "(.:format)", :to => :destroy
407

408 409 410
                  with_exclusive_name_prefix :edit do
                    get "/edit(.:format)", :to => :edit, :as => resource.singular
                  end
J
Joshua Peek 已提交
411
                end
412 413 414 415
              end
            end
          end

J
Joshua Peek 已提交
416
          self
417 418
        end

J
Joshua Peek 已提交
419 420 421
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
422 423
          end

J
Joshua Peek 已提交
424
          with_scope_level(:collection) do
425
            scope(:name_prefix => parent_resource.collection_name, :as => "") do
426 427
              yield
            end
J
Joshua Peek 已提交
428
          end
429
        end
J
Joshua Peek 已提交
430

J
Joshua Peek 已提交
431 432 433 434
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
435

J
Joshua Peek 已提交
436
          with_scope_level(:member) do
J
Joshua Peek 已提交
437
            scope("/:id", :name_prefix => parent_resource.member_name, :as => "") do
J
Joshua Peek 已提交
438 439 440
              yield
            end
          end
J
Joshua Peek 已提交
441 442
        end

443 444 445 446 447 448
        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 已提交
449
            scope("/#{parent_resource.id_segment}", :name_prefix => parent_resource.member_name) do
450 451 452 453 454
              yield
            end
          end
        end

J
Joshua Peek 已提交
455
        def match(*args)
J
Joshua Peek 已提交
456
          options = args.extract_options!
457 458 459 460 461 462 463

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

          if args.first.is_a?(Symbol)
464
            with_exclusive_name_prefix(args.first) do
J
Joshua Peek 已提交
465
              return match("/#{args.first}(.:format)", options.merge(:to => args.first.to_sym))
466 467 468
            end
          end

J
Joshua Peek 已提交
469
          args.push(options)
J
Joshua Peek 已提交
470

J
Joshua Peek 已提交
471 472 473 474 475 476
          case options.delete(:on)
          when :collection
            return collection { match(*args) }
          when :member
            return member { match(*args) }
          end
J
Joshua Peek 已提交
477

J
Joshua Peek 已提交
478 479 480
          if @scope[:scope_level] == :resources
            raise ArgumentError, "can't define route directly in resources scope"
          end
J
Joshua Peek 已提交
481

J
Joshua Peek 已提交
482
          super
J
Joshua Peek 已提交
483 484
        end

485 486 487 488 489
        protected
          def parent_resource
            @scope[:scope_level_resource]
          end

J
Joshua Peek 已提交
490
        private
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
          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

507
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
508
            old, @scope[:scope_level] = @scope[:scope_level], kind
509
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
510 511 512
            yield
          ensure
            @scope[:scope_level] = old
513
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
514 515
          end
      end
J
Joshua Peek 已提交
516

517 518 519 520
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
521 522
    end
  end
523
end