rackful 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,454 @@
1
+ # Required for parsing:
2
+ require 'rack'
3
+
4
+ # Required for running:
5
+
6
+
7
+ module Rackful
8
+
9
+ =begin markdown
10
+ Mixin for resources served by {Server}.
11
+
12
+ {Server} helps you implement Rackful resource objects quickly in a couple
13
+ of ways.
14
+ Classes that include this module may implement a method `content_types`
15
+ for content negotiation. This method must return a Hash of
16
+ `media-type => quality` pairs.
17
+ @see Server, ResourceFactory
18
+ @since 0.0.1
19
+ =end
20
+ module Resource
21
+
22
+
23
+ include Rack::Utils
24
+
25
+
26
+ def self.included(base)
27
+ base.extend ClassMethods
28
+ end
29
+
30
+
31
+ module ClassMethods
32
+
33
+
34
+ =begin markdown
35
+ Meta-programmer method.
36
+ @example Have your resource rendered in XML and JSON
37
+ class MyResource
38
+ add_serializer MyResource2XML
39
+ add_serializer MyResource2JSON, 0.5
40
+ end
41
+ @param serializer [Serializer]
42
+ @param quality [Float]
43
+ @return [self]
44
+ =end
45
+ def add_serializer serializer, quality = 1.0
46
+ quality = quality.to_f
47
+ quality = 1.0 if quality > 1.0
48
+ quality = 0.0 if quality < 0.0
49
+ # The single '@' on the following line is on purpose!
50
+ s = [serializer, quality]
51
+ serializer::CONTENT_TYPES.each {
52
+ |content_type|
53
+ self.serializers[content_type.to_s] = s
54
+ }
55
+ self
56
+ end
57
+
58
+
59
+ def serializers
60
+ @rackful_resource_serializers ||= {}
61
+ end
62
+
63
+
64
+ def all_serializers
65
+ @rackful_resource_all_serializers ||=
66
+ if self.superclass.respond_to?(:all_serializers)
67
+ self.superclass.all_serializers.merge( self.serializers ) do
68
+ |key, oldval, newval|
69
+ newval[1] >= oldval[1] ? newval : oldval
70
+ end
71
+ else
72
+ self.serializers
73
+ end
74
+ end
75
+
76
+
77
+ =begin markdown
78
+ Meta-programmer method.
79
+ @example Have your resource accept XML and JSON in `PUT` requests
80
+ class MyResource
81
+ add_parser XML2MyResource, :PUT
82
+ add_parser JSON2MyResource, :PUT
83
+ end
84
+ @param parser [Parser]
85
+ @param method [#to_sym]
86
+ @return [self]
87
+ =end
88
+ def add_media_type media_type, method = :PUT
89
+ method = method.to_sym
90
+ self.media_types[method] ||= []
91
+ self.media_types[method] << media_type
92
+ self
93
+ end
94
+
95
+
96
+ def media_types
97
+ @rackful_resource_media_types ||= {}
98
+ end
99
+
100
+
101
+ def all_media_types
102
+ @rackful_resource_all_media_types ||=
103
+ if self.superclass.respond_to?(:all_media_types)
104
+ self.superclass.all_media_types.merge( self.media_types ) do
105
+ |key, oldval, newval|
106
+ oldval + newval
107
+ end
108
+ else
109
+ self.media_types
110
+ end
111
+ end
112
+
113
+
114
+ =begin markdown
115
+ The best media type for the response body, given the current HTTP request.
116
+ @param accept [Hash]
117
+ @param require_match [Boolean]
118
+ @return [String] content-type
119
+ @raise [HTTP406NotAcceptable] if `require_match` is `true` and no match was found.
120
+ @since 0.1.0
121
+ =end
122
+ def best_content_type accept, require_match = true
123
+ if accept.empty?
124
+ return self.all_serializers.values.sort_by(&:last).last[0]::CONTENT_TYPES[0]
125
+ end
126
+ matches = []
127
+ accept.each_pair {
128
+ |accept_media_type, accept_quality|
129
+ self.all_serializers.each_pair {
130
+ |content_type, v|
131
+ quality = v[1]
132
+ media_type = content_type.split(';').first.strip
133
+ if File.fnmatch( accept_media_type, media_type )
134
+ matches << [ content_type, accept_quality * quality ]
135
+ end
136
+ }
137
+ }
138
+ if matches.empty?
139
+ if require_match
140
+ raise( HTTP406NotAcceptable, self.all_serializers.keys() )
141
+ else
142
+ return self.all_serializers.values.sort_by(&:last).last[0]::CONTENT_TYPES[0]
143
+ end
144
+ end
145
+ matches.sort_by(&:last).last[0]
146
+ end
147
+
148
+
149
+ # =begin markdown
150
+ # @param content_type [String]
151
+ # @param method [#to_s]
152
+ # @return [Serializer]
153
+ # =end
154
+ # def parser request
155
+ # method = request.request_method.upcase.to_sym
156
+ # if !parsers[method] || !parsers[method][request.media_type]
157
+ # raise HTTP415UnsupportedMediaType, ( parsers[method] ? parsers[method].keys : [] )
158
+ # end
159
+ # parsers[method][request.media_type].new( request )
160
+ # end
161
+
162
+
163
+ end # module ClassMethods
164
+
165
+
166
+ =begin markdown
167
+ @return [Serializer]
168
+ =end
169
+ def serializer content_type
170
+ @rackful_resource_serializers ||= {}
171
+ @rackful_resource_serializers[content_type] ||=
172
+ self.class.all_serializers[content_type][0].new( self, content_type )
173
+ end
174
+
175
+
176
+ =begin markdown
177
+ @!method to_struct()
178
+ @return [#to_json, #each_pair]
179
+ =end
180
+
181
+ =begin markdown
182
+ @!method do_METHOD( Request, Rack::Response )
183
+ HTTP/1.1 method handler.
184
+
185
+ To handle certain HTTP/1.1 request methods, resources must implement methods
186
+ called `do_<HTTP_METHOD>`.
187
+ @example Handling `PATCH` requests
188
+ def do_PATCH request, response
189
+ response['Content-Type'] = 'text/plain'
190
+ response.body = [ 'Hello world!' ]
191
+ end
192
+ @abstract
193
+ @return [void]
194
+ @raise [HTTPStatus, RuntimeError]
195
+ @since 0.0.1
196
+ =end
197
+
198
+
199
+ =begin markdown
200
+ The path of this resource.
201
+ @return [Rackful::Path]
202
+ @see #initialize
203
+ @since 0.0.1
204
+ =end
205
+ attr_reader :path
206
+
207
+
208
+ def path= path
209
+ @path = Path.new(path.to_s)
210
+ end
211
+
212
+
213
+ def title
214
+ ( '/' == self.path ) ?
215
+ Request.current.host :
216
+ File.basename(self.path).to_path.unescape
217
+ end
218
+
219
+
220
+ def requested?
221
+ self.path.slashify == Request.current.path.slashify
222
+ end
223
+
224
+ =begin markdown
225
+ Does this resource _exists_?
226
+
227
+ For example, a client can `PUT` to a URL that doesn't refer to a resource
228
+ yet. In that case, your {Server#resource_factory resource factory} can
229
+ produce an empty resource to to handle the `PUT` request. `HEAD` and `GET`
230
+ requests will still yield `404 Not Found`.
231
+
232
+ @return [Boolean] The default implementation returns `false`.
233
+ @since 0.0.1
234
+ =end
235
+ def empty?
236
+ false
237
+ end
238
+
239
+
240
+ def to_rackful
241
+ self
242
+ end
243
+
244
+
245
+ =begin markdown
246
+ @!attribute [r] get_etag
247
+ The ETag of this resource.
248
+
249
+ If your classes implement this method, then an `ETag:` response
250
+ header is generated automatically when appropriate. This allows clients to
251
+ perform conditional requests, by sending an `If-Match:` or
252
+ `If-None-Match:` request header. These conditions are then asserted
253
+ for you automatically.
254
+
255
+ Make sure your entity tag is a properly formatted string. In ABNF:
256
+
257
+ entity-tag = [ "W/" ] quoted-string
258
+ quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
259
+ qdtext = <any TEXT except <">>
260
+ quoted-pair = "\" CHAR
261
+
262
+ @abstract
263
+ @return [String]
264
+ @see http://tools.ietf.org/html/rfc2616#section-14.19 RFC2616 section 14.19
265
+ @since 0.0.1
266
+ =end
267
+
268
+
269
+ =begin markdown
270
+ @!attribute [r] get_last_modified
271
+ Last modification of this resource.
272
+
273
+ If your classes implement this method, then a `Last-Modified:` response
274
+ header is generated automatically when appropriate. This allows clients to
275
+ perform conditional requests, by sending an `If-Modified-Since:` or
276
+ `If-Unmodified-Since:` request header. These conditions are then asserted
277
+ for you automatically.
278
+ @abstract
279
+ @return [Array<(Time, Boolean)>] The timestamp, and a flag indicating if the
280
+ timestamp is a strong validator.
281
+ @see http://tools.ietf.org/html/rfc2616#section-14.29 RFC2616 section 14.29
282
+ @since 0.0.1
283
+ =end
284
+
285
+
286
+ =begin markdown
287
+ @!method destroy()
288
+ @return [Hash, nil] an optional header hash.
289
+ =end
290
+
291
+
292
+ =begin markdown
293
+ List of all HTTP/1.1 methods implemented by this resource.
294
+
295
+ This works by inspecting all the {#do_METHOD} methods this object implements.
296
+ @return [Array<Symbol>]
297
+ @since 0.0.1
298
+ @private
299
+ =end
300
+ def http_methods
301
+ r = []
302
+ if self.empty?
303
+ self.class.all_media_types
304
+ else
305
+ r.merge! [ :OPTIONS, :HEAD, :GET ]
306
+ r << :DELETE if self.respond_to?( :destroy )
307
+ end
308
+ self.public_instance_methods.each do
309
+ |instance_method|
310
+ if /\Ado_([A-Z])+\z/ === instance_method
311
+ r << $1.to_sym
312
+ end
313
+ end
314
+ r
315
+ end
316
+
317
+
318
+ =begin markdown
319
+ Handles an OPTIONS request.
320
+
321
+ As a courtesy, this module implements a default handler for OPTIONS
322
+ requests. It creates an `Allow:` header, listing all implemented HTTP/1.1
323
+ methods for this resource. By default, an `HTTP/1.1 204 No Content` is
324
+ returned (without an entity body).
325
+
326
+ Feel free to override this method at will.
327
+ @return [void]
328
+ @raise [HTTP404NotFound] `404 Not Found` if this resource is empty.
329
+ @since 0.0.1
330
+ =end
331
+ def http_OPTIONS request, response
332
+ raise HTTP404NotFound if self.empty?
333
+ response.status = status_code :no_content
334
+ response.header['Allow'] = self.http_methods.join ', '
335
+ end
336
+
337
+
338
+ =begin markdown
339
+ Handles a HEAD request.
340
+
341
+ This default handler for HEAD requests calls {#http\_GET}, and
342
+ then strips off the response body.
343
+
344
+ Feel free to override this method at will.
345
+ @return [self]
346
+ @since 0.0.1
347
+ =end
348
+ def http_HEAD request, response
349
+ self.http_GET request, response
350
+ response['Content-Length'] =
351
+ response.body.reduce(0) do
352
+ |memo, s| memo + bytesize(s)
353
+ end.to_s
354
+ # Is this really necessary? Doesn't Rack automatically strip the response
355
+ # body for HEAD requests?
356
+ response.body = []
357
+ end
358
+
359
+
360
+ =begin markdown
361
+ @private
362
+ @return [void]
363
+ @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
364
+ @since 0.0.1
365
+ =end
366
+ def http_GET request, response
367
+ raise HTTP404NotFound if self.empty?
368
+ # May throw HTTP406NotAcceptable:
369
+ content_type = self.class.best_content_type( request.accept )
370
+ response['Content-Type'] = content_type
371
+ response.status = status_code( :ok )
372
+ response.headers.merge! self.default_headers
373
+ # May throw HTTP405MethodNotAllowed:
374
+ serializer = self.serializer( content_type )
375
+ if serializer.respond_to? :headers
376
+ response.headers.merge!( serializer.headers )
377
+ end
378
+ response.body = serializer
379
+ end
380
+
381
+
382
+ =begin markdown
383
+ Wrapper around {#do_METHOD #do_GET}
384
+ @private
385
+ @return [void]
386
+ @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
387
+ @since 0.0.1
388
+ =end
389
+ def http_DELETE request, response
390
+ raise HTTP404NotFound if self.empty?
391
+ response.status = status_code( :no_content )
392
+ if headers = self.destroy
393
+ response.headers.merge! headers
394
+ end
395
+ end
396
+
397
+
398
+ =begin markdown
399
+ @private
400
+ @return [void]
401
+ @raise [HTTP415UnsupportedMediaType] `405 Method Not Allowed` if the resource doesn't implement the `PUT` method.
402
+ @since 0.0.1
403
+ =end
404
+ def http_PUT request, response
405
+ raise HTTP405MethodNotAllowed unless self.respond_to? :do_PUT
406
+ unless self.class.media_types[:PUT] &&
407
+ self.class.media_types[:PUT].include?( request.media_type )
408
+ raise HTTP415UnsupportedMediaType, self.class.media_types[:PUT]
409
+ end
410
+ response.status = status_code( self.empty? ? :created : :no_content )
411
+ self.do_PUT( request, response )
412
+ response.headers.merge! self.default_headers
413
+ end
414
+
415
+
416
+ =begin markdown
417
+ Wrapper around {#do_METHOD #do_PUT}
418
+ @private
419
+ @return [void]
420
+ @raise [HTTPStatus] `405 Method Not Allowed` if the resource doesn't implement the `PUT` method.
421
+ @since 0.0.1
422
+ =end
423
+ def http_method request, response
424
+ method = request.request_method.to_sym
425
+ if ! self.respond_to?( :"do_#{method}" )
426
+ raise HTTP405MethodNotAllowed, self.http_methods
427
+ end
428
+ if ( request.content_length ||
429
+ 'chunked' == request.env['HTTP_TRANSFER_ENCODING'] ) and
430
+ ! self.class.media_types[method] ||
431
+ ! self.class.media_types[method].include?( request.media_type )
432
+ raise HTTP415UnsupportedMediaType, self.class.media_types[method]
433
+ end
434
+ self.send( :"do_#{method}", request, response )
435
+ end
436
+
437
+
438
+ =begin markdown
439
+ Adds `ETag:` and `Last-Modified:` response headers.
440
+ @since 0.0.1
441
+ =end
442
+ def default_headers
443
+ r = {}
444
+ r['ETag'] = self.get_etag \
445
+ if self.respond_to?( :get_etag )
446
+ r['Last-Modified'] = self.get_last_modified[0].httpdate \
447
+ if self.respond_to?( :get_last_modified )
448
+ r
449
+ end
450
+
451
+
452
+ end # module Resource
453
+
454
+ end # module Rackful