mapper.rb 13.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 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
          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 = "#{@scope[:path]}#{path}" if @scope[:path]
J
Joshua Peek 已提交
50
          path = Rack::Mount::Utils.normalize_path(path) if path
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

          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
J
Joshua Peek 已提交
172
            path, @scope[:path] = @scope[:path], Rack::Mount::Utils.normalize_path(@scope[:path].to_s + path.to_s)
173 174 175 176 177 178
          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
          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)
J
Joshua Peek 已提交
217
          scope("/#{path}") { yield }
218 219 220 221 222 223 224 225 226 227 228
        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

J
Joshua Peek 已提交
300
          scope(:path => "/#{resource.name}", :controller => resource.controller) do
301 302 303
            with_scope_level(:resource, resource) do
              yield if block_given?

J
Joshua Peek 已提交
304 305 306 307 308 309
              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}"
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

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

338
              with_scope_level(:collection) do
J
Joshua Peek 已提交
339 340
                get "(.:format)", :to => :index, :as => resource.collection_name
                post "(.:format)", :to => :create
341 342 343
                with_exclusive_name_prefix :new do
                  get "/new(.:format)", :to => :new, :as => resource.singular
                end
344
              end
345

346
              with_scope_level(:member) do
J
Joshua Peek 已提交
347 348 349 350
                scope("/:id") do
                  get "(.:format)", :to => :show, :as => resource.member_name
                  put "(.:format)", :to => :update
                  delete "(.:format)", :to => :destroy
351 352 353
                  with_exclusive_name_prefix :edit do
                    get "/edit(.:format)", :to => :edit, :as => resource.singular
                  end
J
Joshua Peek 已提交
354
                end
355 356 357 358
              end
            end
          end

J
Joshua Peek 已提交
359
          self
360 361
        end

J
Joshua Peek 已提交
362 363 364
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
365 366
          end

J
Joshua Peek 已提交
367
          with_scope_level(:collection) do
368
            scope(:name_prefix => parent_resource.collection_name, :as => "") do
369 370
              yield
            end
J
Joshua Peek 已提交
371
          end
372
        end
J
Joshua Peek 已提交
373

J
Joshua Peek 已提交
374 375 376 377
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
378

J
Joshua Peek 已提交
379
          with_scope_level(:member) do
J
Joshua Peek 已提交
380
            scope("/:id", :name_prefix => parent_resource.member_name, :as => "") do
J
Joshua Peek 已提交
381 382 383
              yield
            end
          end
J
Joshua Peek 已提交
384 385
        end

386 387 388 389 390 391
        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 已提交
392
            scope("/#{parent_resource.id_segment}", :name_prefix => parent_resource.member_name) do
393 394 395 396 397
              yield
            end
          end
        end

J
Joshua Peek 已提交
398
        def match(*args)
J
Joshua Peek 已提交
399
          options = args.extract_options!
400 401 402 403 404 405 406

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

          if args.first.is_a?(Symbol)
407
            with_exclusive_name_prefix(args.first) do
J
Joshua Peek 已提交
408
              return match("/#{args.first}(.:format)", options.merge(:to => args.first.to_sym))
409 410 411
            end
          end

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

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

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

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

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

J
Joshua Peek 已提交
433
        private
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
          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

450
          def with_scope_level(kind, resource = parent_resource)
J
Joshua Peek 已提交
451
            old, @scope[:scope_level] = @scope[:scope_level], kind
452
            old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
J
Joshua Peek 已提交
453 454 455
            yield
          ensure
            @scope[:scope_level] = old
456
            @scope[:scope_level_resource] = old_resource
J
Joshua Peek 已提交
457 458
          end
      end
J
Joshua Peek 已提交
459

460 461 462 463
      include Base
      include HttpHelpers
      include Scoping
      include Resources
J
Joshua Peek 已提交
464 465 466
    end
  end
end