mapper.rb 12.8 KB
Newer Older
J
Joshua Peek 已提交
1 2
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
3
    class Mapper
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
      class Constraints
        def new(app, constraints = [])
          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)
              return [417, {}, []]
            elsif constraint.respond_to?(:call) && !constraint.call(req)
              return [417, {}, []]
            end
          }

          @app.call(env)
        end
      end

      module Base
        def initialize(set)
          @set = set
        end

        def root(options = {})
          match '/', options.merge(:as => :root)
        end

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

          path = args.first

          conditions, defaults = {}, {}

          path = nil if path == ""
          path = Rack::Mount::Utils.normalize_path(path) if path
          path = "#{@scope[:path]}#{path}" if @scope[:path]

          raise ArgumentError, "path is required" unless path

          constraints = options[:constraints] || {}
          unless constraints.is_a?(Hash)
            block, constraints = constraints, {}
          end
          blocks = ((@scope[:blocks] || []) + [block]).compact
          constraints = (@scope[:constraints] || {}).merge(constraints)
          options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) }

          conditions[:path_info] = path
          requirements = constraints.dup

          path_regexp = Rack::Mount::Strexp.compile(path, constraints, SEPARATORS)
          segment_keys = Rack::Mount::RegexpWithNamedGroups.new(path_regexp).names
          constraints.reject! { |k, v| segment_keys.include?(k.to_s) }
          conditions.merge!(constraints)

          requirements[:controller] ||= @set.controller_constraints

          if via = options[:via]
            via = Array(via).map { |m| m.to_s.upcase }
            conditions[:request_method] = Regexp.union(*via)
          end

          defaults[:controller] ||= @scope[:controller].to_s if @scope[:controller]

          app = initialize_app_endpoint(options, defaults)
          validate_defaults!(app, defaults, segment_keys)
          app = Constraints.new(app, blocks)

          @set.add_route(app, conditions, requirements, defaults, options[:as])

          self
        end

        private
          def initialize_app_endpoint(options, defaults)
            app = nil

            if options[:to].respond_to?(:call)
              app = options[:to]
              defaults.delete(:controller)
              defaults.delete(:action)
            elsif options[:to].is_a?(String)
              defaults[:controller], defaults[:action] = options[:to].split('#')
            elsif options[:to].is_a?(Symbol)
              defaults[:action] = options[:to].to_s
            end

            app || Routing::RouteSet::Dispatcher.new(:defaults => defaults)
          end

          def validate_defaults!(app, defaults, segment_keys)
            return unless app.is_a?(Routing::RouteSet::Dispatcher)

            unless defaults.include?(:controller) || segment_keys.include?("controller")
              raise ArgumentError, "missing :controller"
            end

            unless defaults.include?(:action) || segment_keys.include?("action")
              raise ArgumentError, "missing :action"
            end
          end
      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

        def redirect(path, options = {})
          status = options[:status] || 301
          lambda { |env|
            req = Rack::Request.new(env)
            url = req.scheme + '://' + req.host + path
            [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']]
          }
        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
            path, @scope[:path] = @scope[:path], "#{@scope[:path]}#{Rack::Mount::Utils.normalize_path(path)}"
          else
            path_set = false
          end

          if name_prefix = options.delete(:name_prefix)
            name_prefix_set = true
179
            name_prefix, @scope[:name_prefix] = @scope[:name_prefix], (@scope[:name_prefix] ? "#{@scope[:name_prefix]}_#{name_prefix}" : name_prefix)
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 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 227 228
          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
          @scope[:path] = path if path_set
          @scope[:name_prefix] = name_prefix if name_prefix_set
          @scope[:controller] = controller if controller_set
          @scope[:options] = options
          @scope[:blocks] = blocks
          @scope[:constraints] = constraints
        end

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

        def namespace(path)
          scope(path.to_s) { yield }
        end

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

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

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

229 230
          if @scope[:name_prefix] && !options[:as].blank?
            options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}"
231
          elsif @scope[:name_prefix] && options[:as] == ""
232
            options[:as] = @scope[:name_prefix].to_s
233 234 235 236 237 238 239
          end

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

J
Joshua Peek 已提交
240
      module Resources
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
        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
260
            singular
261 262 263
          end

          def collection_name
264
            plural
265
          end
266 267 268 269

          def id_segment
            ":#{singular}_id"
          end
270 271 272 273
        end

        class SingletonResource < Resource #:nodoc:
          def initialize(entity, options = {})
274
            super
275 276 277 278 279 280 281
          end

          def name
            singular
          end
        end

J
Joshua Peek 已提交
282
        def resource(*resources, &block)
J
Joshua Peek 已提交
283
          options = resources.extract_options!
J
Joshua Peek 已提交
284 285 286 287 288 289

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

291
          resource = SingletonResource.new(resources.pop)
292

J
Joshua Peek 已提交
293
          if @scope[:scope_level] == :resources
294 295
            nested do
              resource(resource.name, options, &block)
296
            end
J
Joshua Peek 已提交
297
            return self
298 299
          end

300 301 302 303 304 305 306 307 308 309
          scope(:path => resource.name, :controller => resource.controller) do
            with_scope_level(:resource, resource) do
              yield if block_given?

              get "", :to => :show, :as => resource.member_name
              post "", :to => :create
              put "", :to => :update
              delete "", :to => :destroy
              get "new", :to => :new, :as => "new_#{resource.singular}"
              get "edit", :to => :edit, :as => "edit_#{resource.singular}"
310 311 312
            end
          end

J
Joshua Peek 已提交
313
          self
314 315
        end

J
Joshua Peek 已提交
316
        def resources(*resources, &block)
J
Joshua Peek 已提交
317
          options = resources.extract_options!
318

J
Joshua Peek 已提交
319 320 321 322
          if resources.length > 1
            raise ArgumentError if block_given?
            resources.each { |r| resources(r, options) }
            return self
323 324
          end

325
          resource = Resource.new(resources.pop)
326

J
Joshua Peek 已提交
327
          if @scope[:scope_level] == :resources
328 329
            nested do
              resources(resource.name, options, &block)
330
            end
J
Joshua Peek 已提交
331
            return self
332 333
          end

334 335 336
          scope(:path => resource.name, :controller => resource.controller) do
            with_scope_level(:resources, resource) do
              yield if block_given?
J
Joshua Peek 已提交
337

338 339 340 341 342
              with_scope_level(:collection) do
                get "", :to => :index, :as => resource.collection_name
                post "", :to => :create
                get "new", :to => :new, :as => "new_#{resource.singular}"
              end
343

344 345 346 347 348 349
              with_scope_level(:member) do
                scope(":id") do
                  get "", :to => :show, :as => resource.member_name
                  put "", :to => :update
                  delete "", :to => :destroy
                  get "edit", :to => :edit, :as => "edit_#{resource.singular}"
J
Joshua Peek 已提交
350
                end
351 352 353 354
              end
            end
          end

J
Joshua Peek 已提交
355
          self
356 357
        end

J
Joshua Peek 已提交
358 359 360
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
361 362
          end

J
Joshua Peek 已提交
363
          with_scope_level(:collection) do
364
            scope(:name_prefix => parent_resource.collection_name, :as => "") do
365 366
              yield
            end
J
Joshua Peek 已提交
367
          end
368
        end
J
Joshua Peek 已提交
369

J
Joshua Peek 已提交
370 371 372 373
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
374

J
Joshua Peek 已提交
375
          with_scope_level(:member) do
376
            scope(":id", :name_prefix => parent_resource.member_name, :as => "") do
J
Joshua Peek 已提交
377 378 379
              yield
            end
          end
J
Joshua Peek 已提交
380 381
        end

382 383 384 385 386 387 388 389 390 391 392 393
        def nested
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use nested outside resources scope"
          end

          with_scope_level(:nested) do
            scope(parent_resource.id_segment, :name_prefix => parent_resource.member_name) do
              yield
            end
          end
        end

J
Joshua Peek 已提交
394
        def match(*args)
J
Joshua Peek 已提交
395
          options = args.extract_options!
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410

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

          if args.first.is_a?(Symbol)
            begin
              old_name_prefix, @scope[:name_prefix] = @scope[:name_prefix], "#{args.first}_#{@scope[:name_prefix]}"
              return match(args.first.to_s, options.merge(:to => args.first.to_sym))
            ensure
              @scope[:name_prefix] = old_name_prefix
            end
          end

J
Joshua Peek 已提交
411
          args.push(options)
J
Joshua Peek 已提交
412

J
Joshua Peek 已提交
413 414 415 416 417 418
          case options.delete(:on)
          when :collection
            return collection { match(*args) }
          when :member
            return member { match(*args) }
          end
J
Joshua Peek 已提交
419

J
Joshua Peek 已提交
420 421 422
          if @scope[:scope_level] == :resources
            raise ArgumentError, "can't define route directly in resources scope"
          end
J
Joshua Peek 已提交
423

J
Joshua Peek 已提交
424
          super
J
Joshua Peek 已提交
425 426
        end

427 428 429 430 431
        protected
          def parent_resource
            @scope[:scope_level_resource]
          end

J
Joshua Peek 已提交
432
        private
433
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
434
            old, @scope[:scope_level] = @scope[:scope_level], kind
435
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
436 437 438
            yield
          ensure
            @scope[:scope_level] = old
439
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
440 441
          end
      end
J
Joshua Peek 已提交
442

443 444 445 446
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
447 448 449
    end
  end
end