mapper.rb 11.3 KB
Newer Older
J
Joshua Peek 已提交
1 2
module ActionDispatch
  module Routing
J
Joshua Peek 已提交
3 4
    class Mapper
      module Resources
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
        class Resource #:nodoc:
          attr_reader :plural, :singular
          attr_reader :path_prefix, :name_prefix

          def initialize(entities, options = {})
            entities = entities.to_s

            @plural   = entities.pluralize
            @singular = entities.singularize

            @path_prefix = options[:path_prefix]
            @name_prefix = options[:name_prefix]
          end

          def name
            plural
          end

          def controller
            plural
          end

          def member_name
            if name_prefix
              "#{name_prefix}_#{singular}"
            else
              singular
            end
          end

          def collection_name
            if name_prefix
              "#{name_prefix}_#{plural}"
            else
              plural
            end
          end

          def new_name
            if name_prefix
              "new_#{name_prefix}_#{singular}"
            else
              "new_#{singular}"
            end
          end

          def edit_name
            if name_prefix
              "edit_#{name_prefix}_#{singular}"
            else
              "edit_#{singular}"
            end
          end
        end

        class SingletonResource < Resource #:nodoc:
          def initialize(entity, options = {})
            super(entity)
          end

          def name
            singular
          end
        end

J
Joshua Peek 已提交
70 71 72 73 74 75 76 77
        def resource(*resources, &block)
          options = resources.last.is_a?(Hash) ? resources.pop : {}

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

79 80
          name_prefix = @scope[:options][:name_prefix] if @scope[:options]
          resource = SingletonResource.new(resources.pop, :name_prefix => name_prefix)
81

J
Joshua Peek 已提交
82 83
          if @scope[:scope_level] == :resources
            member do
84
              resource(resource.name, options, &block)
85
            end
J
Joshua Peek 已提交
86
            return self
87 88
          end

89 90 91
          controller(resource.controller) do
            namespace(resource.name) do
              with_scope_level(:resource, :name => resource.singular) do
J
Joshua Peek 已提交
92
                yield if block_given?
93

94
                get "", :to => :show, :as => resource.member_name
J
Joshua Peek 已提交
95 96
                post "", :to => :create
                put "", :to => :update
97
                delete "", :to => :destroy
98 99
                get "new", :to => :new, :as => resource.new_name
                get "edit", :to => :edit, :as => resource.edit_name
J
Joshua Peek 已提交
100
              end
101 102 103
            end
          end

J
Joshua Peek 已提交
104
          self
105 106
        end

J
Joshua Peek 已提交
107 108
        def resources(*resources, &block)
          options = resources.last.is_a?(Hash) ? resources.pop : {}
109

J
Joshua Peek 已提交
110 111 112 113
          if resources.length > 1
            raise ArgumentError if block_given?
            resources.each { |r| resources(r, options) }
            return self
114 115
          end

116 117
          name_prefix = @scope[:options][:name_prefix] if @scope[:options]
          resource = Resource.new(resources.pop, :name_prefix => name_prefix)
118

J
Joshua Peek 已提交
119
          if @scope[:scope_level] == :resources
120 121 122
            parent_resource = @scope[:scope_level_options][:name]
            with_scope_level(:member) do
              scope(":#{parent_resource}_id", :name_prefix => parent_resource) do
123
                resources(resource.name, options, &block)
124
              end
125
            end
J
Joshua Peek 已提交
126
            return self
127 128
          end

129 130 131
          controller(resource.controller) do
            namespace(resource.name) do
              with_scope_level(:resources, :name => resource.singular) do
J
Joshua Peek 已提交
132 133
                yield if block_given?

134
                collection do
135
                  get "", :to => :index, :as => resource.collection_name
136
                  post "", :to => :create
137
                  get "new", :to => :new, :as => resource.new_name
138 139
                end

J
Joshua Peek 已提交
140
                member do
141
                  get "", :to => :show, :as => resource.member_name
J
Joshua Peek 已提交
142
                  put "", :to => :update
143
                  delete "", :to => :destroy
144
                  get "edit", :to => :edit, :as => resource.edit_name
J
Joshua Peek 已提交
145
                end
146 147 148 149
              end
            end
          end

J
Joshua Peek 已提交
150
          self
151 152
        end

J
Joshua Peek 已提交
153 154 155
        def collection
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use collection outside resources scope"
156 157
          end

J
Joshua Peek 已提交
158 159 160
          with_scope_level(:collection) do
            yield
          end
161
        end
J
Joshua Peek 已提交
162

J
Joshua Peek 已提交
163 164 165 166
        def member
          unless @scope[:scope_level] == :resources
            raise ArgumentError, "can't use member outside resources scope"
          end
J
Joshua Peek 已提交
167

J
Joshua Peek 已提交
168 169 170 171 172
          with_scope_level(:member) do
            scope(":id") do
              yield
            end
          end
J
Joshua Peek 已提交
173 174
        end

J
Joshua Peek 已提交
175 176 177
        def match(*args)
          options = args.last.is_a?(Hash) ? args.pop : {}
          args.push(options)
J
Joshua Peek 已提交
178

J
Joshua Peek 已提交
179 180 181 182 183 184
          case options.delete(:on)
          when :collection
            return collection { match(*args) }
          when :member
            return member { match(*args) }
          end
J
Joshua Peek 已提交
185

J
Joshua Peek 已提交
186 187 188
          if @scope[:scope_level] == :resources
            raise ArgumentError, "can't define route directly in resources scope"
          end
J
Joshua Peek 已提交
189

J
Joshua Peek 已提交
190
          super
J
Joshua Peek 已提交
191 192
        end

J
Joshua Peek 已提交
193
        private
194
          def with_scope_level(kind, options = {})
J
Joshua Peek 已提交
195
            old, @scope[:scope_level] = @scope[:scope_level], kind
196
            old_options, @scope[:scope_level_options] = @scope[:scope_level_options], options
J
Joshua Peek 已提交
197 198 199
            yield
          ensure
            @scope[:scope_level] = old
200
            @scope[:scope_level_options] = old_options
J
Joshua Peek 已提交
201 202
          end
      end
J
Joshua Peek 已提交
203

J
Joshua Peek 已提交
204 205 206
      module Scoping
        def scope(*args)
          options = args.last.is_a?(Hash) ? args.pop : {}
J
Joshua Peek 已提交
207

J
Joshua Peek 已提交
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
          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)

          path_set = controller_set = false

          case args.first
          when String
            path_set = true
            path = args.first
            path, @scope[:path] = @scope[:path], "#{@scope[:path]}#{Rack::Mount::Utils.normalize_path(path)}"
          when Symbol
            controller_set = true
            controller = args.first
            controller, @scope[:controller] = @scope[:controller], controller
          end
J
Joshua Peek 已提交
229

J
Joshua Peek 已提交
230
          yield
J
Joshua Peek 已提交
231

J
Joshua Peek 已提交
232 233 234 235 236 237 238
          self
        ensure
          @scope[:path] = path if path_set
          @scope[:controller] = controller if controller_set
          @scope[:options] = options
          @scope[:blocks] = blocks
          @scope[:constraints] = constraints
J
Joshua Peek 已提交
239 240
        end

J
Joshua Peek 已提交
241 242
        def controller(controller)
          scope(controller.to_sym) { yield }
J
Joshua Peek 已提交
243 244
        end

J
Joshua Peek 已提交
245 246
        def namespace(path)
          scope(path.to_s) { yield }
J
Joshua Peek 已提交
247 248
        end

J
Joshua Peek 已提交
249 250
        def constraints(constraints = {})
          scope(:constraints => constraints) { yield }
J
Joshua Peek 已提交
251
        end
J
Joshua Peek 已提交
252
      end
J
Joshua Peek 已提交
253

J
Joshua Peek 已提交
254 255 256
      class Constraints
        def initialize(app, constraints = [])
          @app, @constraints = app, constraints
J
Joshua Peek 已提交
257 258
        end

J
Joshua Peek 已提交
259 260
        def call(env)
          req = Rack::Request.new(env)
J
Joshua Peek 已提交
261

J
Joshua Peek 已提交
262 263
          @constraints.each { |constraint|
            if constraint.respond_to?(:matches?) && !constraint.matches?(req)
J
Joshua Peek 已提交
264
              return [417, {}, []]
J
Joshua Peek 已提交
265
            elsif constraint.respond_to?(:call) && !constraint.call(req)
J
Joshua Peek 已提交
266
              return [417, {}, []]
J
Joshua Peek 已提交
267
            end
J
Joshua Peek 已提交
268
          }
J
Joshua Peek 已提交
269

J
Joshua Peek 已提交
270 271
          @app.call(env)
        end
J
Joshua Peek 已提交
272 273
      end

J
Joshua Peek 已提交
274 275 276
      def initialize(set)
        @set = set
        @scope = {}
J
Joshua Peek 已提交
277

J
Joshua Peek 已提交
278 279
        extend Scoping
        extend Resources
J
Joshua Peek 已提交
280 281
      end

J
Joshua Peek 已提交
282 283
      def get(*args, &block)
        map_method(:get, *args, &block)
J
Joshua Peek 已提交
284 285
      end

J
Joshua Peek 已提交
286 287
      def post(*args, &block)
        map_method(:post, *args, &block)
J
Joshua Peek 已提交
288 289
      end

J
Joshua Peek 已提交
290 291 292
      def put(*args, &block)
        map_method(:put, *args, &block)
      end
J
Joshua Peek 已提交
293

J
Joshua Peek 已提交
294 295 296
      def delete(*args, &block)
        map_method(:delete, *args, &block)
      end
J
Joshua Peek 已提交
297

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

J
Joshua Peek 已提交
302 303
      def match(*args)
        options = args.last.is_a?(Hash) ? args.pop : {}
J
Joshua Peek 已提交
304

J
Joshua Peek 已提交
305 306 307
        if args.length > 1
          args.each { |path| match(path, options) }
          return self
J
Joshua Peek 已提交
308 309
        end

J
Joshua Peek 已提交
310 311
        if args.first.is_a?(Symbol)
          return match(args.first.to_s, options.merge(:to => args.first.to_sym))
J
Joshua Peek 已提交
312 313
        end

J
Joshua Peek 已提交
314
        path = args.first
J
Joshua Peek 已提交
315

J
Joshua Peek 已提交
316 317
        options = (@scope[:options] || {}).merge(options)
        conditions, defaults = {}, {}
J
Joshua Peek 已提交
318

J
Joshua Peek 已提交
319 320 321
        path = nil if path == ""
        path = Rack::Mount::Utils.normalize_path(path) if path
        path = "#{@scope[:path]}#{path}" if @scope[:path]
J
Joshua Peek 已提交
322

J
Joshua Peek 已提交
323
        raise ArgumentError, "path is required" unless path
J
Joshua Peek 已提交
324

J
Joshua Peek 已提交
325 326 327
        constraints = options[:constraints] || {}
        unless constraints.is_a?(Hash)
          block, constraints = constraints, {}
J
Joshua Peek 已提交
328
        end
J
Joshua Peek 已提交
329 330 331
        blocks = ((@scope[:blocks] || []) + [block]).compact
        constraints = (@scope[:constraints] || {}).merge(constraints)
        options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) }
J
Joshua Peek 已提交
332

333 334
        conditions[:path_info] = path
        requirements = constraints.dup
J
Joshua Peek 已提交
335

336 337
        path_regexp = Rack::Mount::Strexp.compile(path, constraints, SEPARATORS)
        segment_keys = Rack::Mount::RegexpWithNamedGroups.new(path_regexp).names
J
Joshua Peek 已提交
338 339
        constraints.reject! { |k, v| segment_keys.include?(k.to_s) }
        conditions.merge!(constraints)
J
Joshua Peek 已提交
340

341 342
        requirements[:controller] ||= Routing.controller_constraints

J
Joshua Peek 已提交
343 344 345
        if via = options[:via]
          via = Array(via).map { |m| m.to_s.upcase }
          conditions[:request_method] = Regexp.union(*via)
J
Joshua Peek 已提交
346 347
        end

J
Joshua Peek 已提交
348
        defaults[:controller] = @scope[:controller].to_s if @scope[:controller]
J
Joshua Peek 已提交
349

J
Joshua Peek 已提交
350 351 352 353 354 355 356 357
        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
J
Joshua Peek 已提交
358
        end
J
Joshua Peek 已提交
359
        app ||= Routing::RouteSet::Dispatcher.new(:defaults => defaults)
J
Joshua Peek 已提交
360

J
Joshua Peek 已提交
361 362 363 364 365 366
        if 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"
J
Joshua Peek 已提交
367 368 369
          end
        end

J
Joshua Peek 已提交
370
        app = Constraints.new(app, blocks) if blocks.any?
371
        @set.add_route(app, conditions, requirements, defaults, options[:as])
J
Joshua Peek 已提交
372

J
Joshua Peek 已提交
373 374
        self
      end
J
Joshua Peek 已提交
375

J
Joshua Peek 已提交
376 377 378 379 380 381 382 383
      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
J
Joshua Peek 已提交
384

J
Joshua Peek 已提交
385 386 387 388 389 390 391
      private
        def map_method(method, *args, &block)
          options = args.last.is_a?(Hash) ? args.pop : {}
          options[:via] = method
          args.push(options)
          match(*args, &block)
          self
J
Joshua Peek 已提交
392 393 394 395
        end
    end
  end
end