rackful 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,6 @@ module Rackful
8
8
 
9
9
  =begin markdown
10
10
  Subclass of {Rack::Request}, augmented for Rackful requests.
11
- @since 0.0.1
12
11
  =end
13
12
  class Request < Rack::Request
14
13
 
@@ -17,7 +16,6 @@ class Request < Rack::Request
17
16
  The resource factory for the current request.
18
17
  @return [#[]]
19
18
  @see Server#initialize
20
- @since 0.0.1
21
19
  =end
22
20
  def resource_factory; self.env['rackful.resource_factory']; end
23
21
  def base_path
@@ -31,18 +29,15 @@ The resource factory for the current request.
31
29
  Similar to the HTTP/1.1 `Content-Location:` header. Contains the canonical path
32
30
  to the requested resource, which may differ from {#path}
33
31
  @return [Path]
34
- @since 0.1.0
35
32
  =end
36
33
  def content_path; self.env['rackful.content_path'] ||= self.path; end
37
34
  =begin markdown
38
35
  Set by {Rackful::Server#call!}
39
36
  @return [Path]
40
- @since 0.1.0
41
37
  =end
42
38
  def content_path= bp; self.env['rackful.content_path'] = bp.to_path; end
43
39
  =begin markdown
44
40
  @return [Path]
45
- @since 0.1.0
46
41
  =end
47
42
  def path; super.to_path; end
48
43
 
@@ -60,7 +55,6 @@ In a multi-threaded server, multiple requests can be handled at one time.
60
55
  This method returns the request object, created (and registered) by
61
56
  {Server#call!}
62
57
  @return [Request]
63
- @since 0.0.1
64
58
  =end
65
59
  def self.current
66
60
  Thread.current[:rackful_request]
@@ -82,7 +76,6 @@ Assert all <tt>If-*</tt> request headers.
82
76
  @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616, section 13.3.3
83
77
  for details about weak and strong validator comparison.
84
78
  @todo Implement support for the `If-Range:` header.
85
- @since 0.0.1
86
79
  =end
87
80
  def assert_if_headers resource
88
81
  #raise HTTP501NotImplemented, 'If-Range: request header is not supported.' \
@@ -113,11 +106,15 @@ Assert all <tt>If-*</tt> request headers.
113
106
  elsif cond[:unmodified_since]
114
107
  raise HTTP412PreconditionFailed, 'If-Unmodified-Since'
115
108
  elsif cond[:modified_since]
116
- raise HTTP404NotFound
109
+ raise HTTP404NotFound, resource.path
117
110
  end
118
111
  else
119
112
  if cond[:none_match] && self.validate_etag( etag, cond[:none_match] )
120
- raise HTTP412PreconditionFailed, 'If-None-Match'
113
+ if allow_weak
114
+ raise HTTP304NotModified
115
+ else
116
+ raise HTTP412PreconditionFailed, 'If-None-Match'
117
+ end
121
118
  elsif cond[:match] && ! self.validate_etag( etag, cond[:match] )
122
119
  raise HTTP412PreconditionFailed, 'If-Match'
123
120
  elsif cond[:unmodified_since]
@@ -145,7 +142,6 @@ Hash of acceptable media types and their qualities.
145
142
  This method parses the HTTP/1.1 `Accept:` header. If no acceptable media
146
143
  types are provided, an empty Hash is returned.
147
144
  @return [Hash{media_type => quality}]
148
- @since 0.0.1
149
145
  =end
150
146
  def accept
151
147
  @env['rackful.accept'] ||= begin
@@ -172,7 +168,6 @@ Parses the HTTP/1.1 `If-Match:` header.
172
168
  @return [nil, Array<String>]
173
169
  @see http://tools.ietf.org/html/rfc2616#section-14.24 RFC2616, section 14.24
174
170
  @see #if_none_match
175
- @since 0.0.1
176
171
  =end
177
172
  def if_match none = false
178
173
  header = @env["HTTP_IF_#{ none ? 'NONE_' : '' }MATCH"]
@@ -192,7 +187,6 @@ Parses the HTTP/1.1 `If-None-Match:` header.
192
187
  @return [nil, Array<String>]
193
188
  @see http://tools.ietf.org/html/rfc2616#section-14.26 RFC2616, section 14.26
194
189
  @see #if_match
195
- @since 0.0.1
196
190
  =end
197
191
  def if_none_match
198
192
  self.if_match true
@@ -204,7 +198,6 @@ Parses the HTTP/1.1 `If-None-Match:` header.
204
198
  @return [nil, Time]
205
199
  @see http://tools.ietf.org/html/rfc2616#section-14.25 RFC2616, section 14.25
206
200
  @see #if_unmodified_since
207
- @since 0.0.1
208
201
  =end
209
202
  def if_modified_since unmodified = false
210
203
  header = @env["HTTP_IF_#{ unmodified ? 'UN' : '' }MODIFIED_SINCE"]
@@ -222,7 +215,6 @@ Parses the HTTP/1.1 `If-None-Match:` header.
222
215
  @return [nil, Time]
223
216
  @see http://tools.ietf.org/html/rfc2616#section-14.28 RFC2616, section 14.28
224
217
  @see #if_modified_since
225
- @since 0.0.1
226
218
  =end
227
219
  def if_unmodified_since
228
220
  self.if_modified_since true
@@ -241,7 +233,6 @@ Does any of the tags in `etags` match `etag`?
241
233
  @return [Boolean]
242
234
  @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616 section 13.3.3
243
235
  for details about weak and strong validator comparison.
244
- @since 0.0.1
245
236
  =end
246
237
  def validate_etag etag, etags
247
238
  etag = etag.to_s
@@ -15,7 +15,6 @@ Classes that include this module may implement a method `content_types`
15
15
  for content negotiation. This method must return a Hash of
16
16
  `media-type => quality` pairs.
17
17
  @see Server, ResourceFactory
18
- @since 0.0.1
19
18
  =end
20
19
  module Resource
21
20
 
@@ -23,6 +22,14 @@ module Resource
23
22
  include Rack::Utils
24
23
 
25
24
 
25
+ =begin
26
+ Normally, when a module is included, all the instance methods of the included
27
+ module become available as instance methods to the including module/class. But
28
+ class methods of the included module don't become available as class methods to
29
+ the including class.
30
+
31
+
32
+ =end
26
33
  def self.included(base)
27
34
  base.extend ClassMethods
28
35
  end
@@ -30,21 +37,21 @@ module Resource
30
37
 
31
38
  module ClassMethods
32
39
 
33
-
34
- #Meta-programmer method.
35
- #@example Have your resource rendered in XML and JSON
36
- # class MyResource
37
- # add_serializer MyResource2XML
38
- # add_serializer MyResource2JSON, 0.5
39
- # end
40
- #@param serializer [Serializer]
41
- #@param quality [Float]
42
- #@return [self]
40
+ =begin
41
+ Meta-programmer method.
42
+ @example Have your resource rendered in XML and JSON
43
+ class MyResource
44
+ add_serializer MyResource2XML
45
+ add_serializer MyResource2JSON, 0.5
46
+ end
47
+ @param serializer [Serializer]
48
+ @param quality [Float]
49
+ @return [self]
50
+ =end
43
51
  def add_serializer serializer, quality = 1.0
44
52
  quality = quality.to_f
45
53
  quality = 1.0 if quality > 1.0
46
54
  quality = 0.0 if quality < 0.0
47
- # The single '@' on the following line is on purpose!
48
55
  s = [serializer, quality]
49
56
  serializer::CONTENT_TYPES.each {
50
57
  |content_type|
@@ -55,11 +62,13 @@ module Resource
55
62
 
56
63
 
57
64
  def serializers
65
+ # The single '@' on the following line is on purpose!
58
66
  @rackful_resource_serializers ||= {}
59
67
  end
60
68
 
61
69
 
62
70
  def all_serializers
71
+ # The single '@' on the following line is on purpose!
63
72
  @rackful_resource_all_serializers ||=
64
73
  if self.superclass.respond_to?(:all_serializers)
65
74
  self.superclass.all_serializers.merge( self.serializers ) do
@@ -72,28 +81,36 @@ module Resource
72
81
  end
73
82
 
74
83
 
75
- #Meta-programmer method.
76
- #@example Have your resource accept XML and JSON in `PUT` requests
77
- # class MyResource
78
- # add_parser XML2MyResource, :PUT
79
- # add_parser JSON2MyResource, :PUT
80
- # end
81
- #@param parser [Parser]
82
- #@param method [#to_sym]
83
- #@return [self]
84
+ =begin
85
+ Meta-programmer method.
86
+ @example Have your resource accept XML and JSON in `PUT` requests
87
+ class MyResource
88
+ add_media_type 'text/xml', :PUT
89
+ add_media_type 'application/json', :PUT
90
+ end
91
+ @param [#to_s] media_type
92
+ @param [#to_sym] method
93
+ @return [self]
94
+ =end
84
95
  def add_media_type media_type, method = :PUT
85
96
  method = method.to_sym
86
97
  self.media_types[method] ||= []
87
- self.media_types[method] << media_type
98
+ self.media_types[method] << media_type.to_s
88
99
  self
89
100
  end
90
101
 
91
102
 
103
+ =begin
104
+ @todo Documentation
105
+ =end
92
106
  def media_types
93
107
  @rackful_resource_media_types ||= {}
94
108
  end
95
109
 
96
110
 
111
+ =begin
112
+ @todo Documentation
113
+ =end
97
114
  def all_media_types
98
115
  @rackful_resource_all_media_types ||=
99
116
  if self.superclass.respond_to?(:all_media_types)
@@ -113,7 +130,6 @@ The best media type for the response body, given the current HTTP request.
113
130
  @param require_match [Boolean]
114
131
  @return [String] content-type
115
132
  @raise [HTTP406NotAcceptable] if `require_match` is `true` and no match was found.
116
- @since 0.1.0
117
133
  =end
118
134
  def best_content_type accept, require_match = true
119
135
  if accept.empty?
@@ -142,18 +158,6 @@ The best media type for the response body, given the current HTTP request.
142
158
  end
143
159
 
144
160
 
145
- #~ # @param content_type [String]
146
- #~ # @param method [#to_s]
147
- #~ # @return [Serializer]
148
- #~ def parser request
149
- #~ method = request.request_method.upcase.to_sym
150
- #~ if !parsers[method] || !parsers[method][request.media_type]
151
- #~ raise HTTP415UnsupportedMediaType, ( parsers[method] ? parsers[method].keys : [] )
152
- #~ end
153
- #~ parsers[method][request.media_type].new( request )
154
- #~ end
155
-
156
-
157
161
  end # module ClassMethods
158
162
 
159
163
 
@@ -167,39 +171,33 @@ The best media type for the response body, given the current HTTP request.
167
171
  end
168
172
 
169
173
 
170
- =begin markdown
171
- @!method to_struct()
172
- @return [#to_json, #each_pair]
173
- =end
174
-
174
+ =begin
175
+ @!method do_METHOD( Request, Rack::Response )
176
+ HTTP/1.1 method handler.
175
177
 
176
- # @!method do_METHOD( Request, Rack::Response )
177
- # HTTP/1.1 method handler.
178
- #
179
- # To handle certain HTTP/1.1 request methods, resources must implement methods
180
- # called `do_<HTTP_METHOD>`.
181
- # @example Handling `PATCH` requests
182
- # def do_PATCH request, response
183
- # response['Content-Type'] = 'text/plain'
184
- # response.body = [ 'Hello world!' ]
185
- # end
186
- # @abstract
187
- # @return [void]
188
- # @raise [HTTPStatus, RuntimeError]
189
- # @since 0.0.1
178
+ To handle certain HTTP/1.1 request methods, resources must implement methods
179
+ called `do_<HTTP_METHOD>`.
180
+ @example Handling `PATCH` requests
181
+ def do_PATCH request, response
182
+ response['Content-Type'] = 'text/plain'
183
+ response.body = [ 'Hello world!' ]
184
+ end
185
+ @abstract
186
+ @return [void]
187
+ @raise [HTTPStatus, RuntimeError]
188
+ =end
190
189
 
191
190
 
192
191
  =begin markdown
193
192
  The path of this resource.
194
193
  @return [Rackful::Path]
195
194
  @see #initialize
196
- @since 0.0.1
197
195
  =end
198
- attr_reader :path
196
+ def path; @rackful_resource_path; end
199
197
 
200
198
 
201
199
  def path= path
202
- @path = path.to_s.to_path
200
+ @rackful_resource_path = Path.new(path)
203
201
  end
204
202
 
205
203
 
@@ -214,6 +212,7 @@ The path of this resource.
214
212
  self.path.slashify == Request.current.path.slashify
215
213
  end
216
214
 
215
+
217
216
  =begin markdown
218
217
  Does this resource _exists_?
219
218
 
@@ -223,13 +222,15 @@ produce an empty resource to to handle the `PUT` request. `HEAD` and `GET`
223
222
  requests will still yield `404 Not Found`.
224
223
 
225
224
  @return [Boolean] The default implementation returns `false`.
226
- @since 0.0.1
227
225
  =end
228
226
  def empty?
229
227
  false
230
228
  end
231
229
 
232
230
 
231
+ =begin markdown
232
+
233
+ =end
233
234
  def to_rackful
234
235
  self
235
236
  end
@@ -237,48 +238,46 @@ requests will still yield `404 Not Found`.
237
238
 
238
239
  =begin markdown
239
240
  @!attribute [r] get_etag
240
- The ETag of this resource.
241
+ The ETag of this resource.
241
242
 
242
- If your classes implement this method, then an `ETag:` response
243
- header is generated automatically when appropriate. This allows clients to
244
- perform conditional requests, by sending an `If-Match:` or
245
- `If-None-Match:` request header. These conditions are then asserted
246
- for you automatically.
243
+ If your classes implement this method, then an `ETag:` response
244
+ header is generated automatically when appropriate. This allows clients to
245
+ perform conditional requests, by sending an `If-Match:` or
246
+ `If-None-Match:` request header. These conditions are then asserted
247
+ for you automatically.
247
248
 
248
- Make sure your entity tag is a properly formatted string. In ABNF:
249
+ Make sure your entity tag is a properly formatted string. In ABNF:
249
250
 
250
- entity-tag = [ "W/" ] quoted-string
251
- quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
252
- qdtext = <any TEXT except <">>
253
- quoted-pair = "\" CHAR
251
+ entity-tag = [ "W/" ] quoted-string
252
+ quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
253
+ qdtext = <any TEXT except <">>
254
+ quoted-pair = "\" CHAR
254
255
 
255
- @abstract
256
- @return [String]
257
- @see http://tools.ietf.org/html/rfc2616#section-14.19 RFC2616 section 14.19
258
- @since 0.0.1
256
+ @abstract
257
+ @return [String]
258
+ @see http://tools.ietf.org/html/rfc2616#section-14.19 RFC2616 section 14.19
259
259
  =end
260
260
 
261
261
 
262
262
  =begin markdown
263
263
  @!attribute [r] get_last_modified
264
- Last modification of this resource.
265
-
266
- If your classes implement this method, then a `Last-Modified:` response
267
- header is generated automatically when appropriate. This allows clients to
268
- perform conditional requests, by sending an `If-Modified-Since:` or
269
- `If-Unmodified-Since:` request header. These conditions are then asserted
270
- for you automatically.
271
- @abstract
272
- @return [Array<(Time, Boolean)>] The timestamp, and a flag indicating if the
273
- timestamp is a strong validator.
274
- @see http://tools.ietf.org/html/rfc2616#section-14.29 RFC2616 section 14.29
275
- @since 0.0.1
264
+ Last modification of this resource.
265
+
266
+ If your classes implement this method, then a `Last-Modified:` response
267
+ header is generated automatically when appropriate. This allows clients to
268
+ perform conditional requests, by sending an `If-Modified-Since:` or
269
+ `If-Unmodified-Since:` request header. These conditions are then asserted
270
+ for you automatically.
271
+ @abstract
272
+ @return [Array<(Time, Boolean)>] The timestamp, and a flag indicating if the
273
+ timestamp is a strong validator.
274
+ @see http://tools.ietf.org/html/rfc2616#section-14.29 RFC2616 section 14.29
276
275
  =end
277
276
 
278
277
 
279
278
  =begin markdown
280
279
  @!method destroy()
281
- @return [Hash, nil] an optional header hash.
280
+ @return [Hash, nil] an optional header hash.
282
281
  =end
283
282
 
284
283
 
@@ -287,7 +286,6 @@ List of all HTTP/1.1 methods implemented by this resource.
287
286
 
288
287
  This works by inspecting all the {#do_METHOD} methods this object implements.
289
288
  @return [Array<Symbol>]
290
- @since 0.0.1
291
289
  @private
292
290
  =end
293
291
  def http_methods
@@ -295,10 +293,10 @@ This works by inspecting all the {#do_METHOD} methods this object implements.
295
293
  if self.empty?
296
294
  self.class.all_media_types
297
295
  else
298
- r.merge! [ :OPTIONS, :HEAD, :GET ]
296
+ r.push( :OPTIONS, :HEAD, :GET )
299
297
  r << :DELETE if self.respond_to?( :destroy )
300
298
  end
301
- self.public_instance_methods.each do
299
+ self.class.public_instance_methods.each do
302
300
  |instance_method|
303
301
  if /\Ado_([A-Z])+\z/ === instance_method
304
302
  r << $1.to_sym
@@ -319,10 +317,9 @@ returned (without an entity body).
319
317
  Feel free to override this method at will.
320
318
  @return [void]
321
319
  @raise [HTTP404NotFound] `404 Not Found` if this resource is empty.
322
- @since 0.0.1
323
320
  =end
324
321
  def http_OPTIONS request, response
325
- raise HTTP404NotFound if self.empty?
322
+ raise HTTP404NotFound, path if self.empty?
326
323
  response.status = status_code :no_content
327
324
  response.header['Allow'] = self.http_methods.join ', '
328
325
  end
@@ -336,7 +333,6 @@ then strips off the response body.
336
333
 
337
334
  Feel free to override this method at will.
338
335
  @return [self]
339
- @since 0.0.1
340
336
  =end
341
337
  def http_HEAD request, response
342
338
  self.http_GET request, response
@@ -352,12 +348,13 @@ Feel free to override this method at will.
352
348
 
353
349
  =begin markdown
354
350
  @private
351
+ @param [Rackful::Request] request
352
+ @param [Rack::Response] response
355
353
  @return [void]
356
354
  @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
357
- @since 0.0.1
358
355
  =end
359
356
  def http_GET request, response
360
- raise HTTP404NotFound if self.empty?
357
+ raise HTTP404NotFound, path if self.empty?
361
358
  # May throw HTTP406NotAcceptable:
362
359
  content_type = self.class.best_content_type( request.accept )
363
360
  response['Content-Type'] = content_type
@@ -377,12 +374,12 @@ Wrapper around {#do_METHOD #do_GET}
377
374
  @private
378
375
  @return [void]
379
376
  @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
380
- @since 0.0.1
381
377
  =end
382
378
  def http_DELETE request, response
383
- raise HTTP404NotFound if self.empty?
379
+ raise HTTP404NotFound, path if self.empty?
380
+ raise HTTP405MethodNotAllowed unless self.respond_to?( :destroy )
384
381
  response.status = status_code( :no_content )
385
- if headers = self.destroy
382
+ if headers = self.destroy( request, response )
386
383
  response.headers.merge! headers
387
384
  end
388
385
  end
@@ -391,8 +388,7 @@ Wrapper around {#do_METHOD #do_GET}
391
388
  =begin markdown
392
389
  @private
393
390
  @return [void]
394
- @raise [HTTP415UnsupportedMediaType] `405 Method Not Allowed` if the resource doesn't implement the `PUT` method.
395
- @since 0.0.1
391
+ @raise [HTTP415UnsupportedMediaType, HTTP405MethodNotAllowed] if the resource doesn't implement the `PUT` method.
396
392
  =end
397
393
  def http_PUT request, response
398
394
  raise HTTP405MethodNotAllowed unless self.respond_to? :do_PUT
@@ -411,7 +407,6 @@ Wrapper around {#do_METHOD #do_PUT}
411
407
  @private
412
408
  @return [void]
413
409
  @raise [HTTPStatus] `405 Method Not Allowed` if the resource doesn't implement the `PUT` method.
414
- @since 0.0.1
415
410
  =end
416
411
  def http_method request, response
417
412
  method = request.request_method.to_sym
@@ -430,7 +425,6 @@ Wrapper around {#do_METHOD #do_PUT}
430
425
 
431
426
  =begin markdown
432
427
  Adds `ETag:` and `Last-Modified:` response headers.
433
- @since 0.0.1
434
428
  =end
435
429
  def default_headers
436
430
  r = {}
@@ -444,4 +438,34 @@ Adds `ETag:` and `Last-Modified:` response headers.
444
438
 
445
439
  end # module Resource
446
440
 
441
+
442
+ =begin unused
443
+ module Collection
444
+
445
+
446
+ include Enumerable
447
+
448
+
449
+ def self.included( modul )
450
+ unless modul.kind_of? Resource
451
+ raise "module #{self} included in #{modul}, which isn't a Rackful::Resource"
452
+ end
453
+ end
454
+
455
+
456
+ def recurse?; false; end
457
+
458
+
459
+ def each_pair
460
+ self.each do
461
+ |path|
462
+ yield [ path, Request.current.resource_factory( path ) ]
463
+ end
464
+ end
465
+
466
+
467
+ end # module Collection
468
+ =end
469
+
470
+
447
471
  end # module Rackful