mapper.rb 15.5 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 208 209 210 211 212 213 214 215 216 217
            req = Request.new(env)

            uri = URI.parse(path_proc.call(req.params))
            uri.scheme ||= req.scheme
            uri.host   ||= req.host

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

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

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

        def namespace(path)
J
Joshua Peek 已提交
294
          scope("/#{path}") { yield }
295 296 297 298 299 300 301 302 303 304 305
        end

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

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

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

306 307
          if @scope[:name_prefix] && !options[:as].blank?
            options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}"
308
          elsif @scope[:name_prefix] && options[:as] == ""
309
            options[:as] = @scope[:name_prefix].to_s
310 311 312 313 314 315 316
          end

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

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

          def collection_name
341
            plural
342
          end
343 344 345 346

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

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

          def name
            singular
          end
        end

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

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

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

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

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

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

J
Joshua Peek 已提交
390
          self
391 392
        end

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

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

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

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

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

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

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

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

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

J
Joshua Peek 已提交
438
          self
439 440
        end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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