mapper.rb 15.4 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 64 65 66 67 68
          
          # 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
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 204

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

            [ status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently'] ]
210
          end
211 212 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
        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 已提交
241
            path, @scope[:path] = @scope[:path], Rack::Mount::Utils.normalize_path(@scope[:path].to_s + path.to_s)
242 243 244 245 246 247
          else
            path_set = false
          end

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

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

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

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

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

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

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

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

J
Joshua Peek 已提交
309
      module Resources
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
        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
329
            singular
330 331 332
          end

          def collection_name
333
            plural
334
          end
335 336 337 338

          def id_segment
            ":#{singular}_id"
          end
339 340 341 342
        end

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

          def name
            singular
          end
        end

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

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

360
          resource = SingletonResource.new(resources.pop)
361

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

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

373 374 375 376 377 378
              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}"
379 380 381
            end
          end

J
Joshua Peek 已提交
382
          self
383 384
        end

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

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

394
          resource = Resource.new(resources.pop)
395

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

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

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

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

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

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

J
Joshua Peek 已提交
430
          self
431 432
        end

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

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

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

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

457 458 459 460 461 462
        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 已提交
463
            scope("/#{parent_resource.id_segment}", :name_prefix => parent_resource.member_name) do
464 465 466 467 468
              yield
            end
          end
        end

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

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

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

J
Joshua Peek 已提交
483
          args.push(options)
J
Joshua Peek 已提交
484

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

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

J
Joshua Peek 已提交
496
          super
J
Joshua Peek 已提交
497 498
        end

499 500 501 502 503
        protected
          def parent_resource
            @scope[:scope_level_resource]
          end

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

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

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