rackful 0.0.1

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.
data/lib/rackful.rb ADDED
@@ -0,0 +1,934 @@
1
+ # Copyright ©2011-2012 Pieter van Beek <pieterb@sara.nl>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require 'rexml/document' # Used by HTTPStatus
17
+ require 'rack'
18
+
19
+ =begin markdown
20
+ Library for creating Rackful web services
21
+
22
+ Rackful
23
+ =======
24
+
25
+ Rationale
26
+ ---------
27
+
28
+ Confronted with the task of implementing a Rackful web service in Ruby, I
29
+ checked out a number of existing libraries and frameworks, including
30
+ Ruby-on-Rails, and then decided to brew my own, the reason being that I couldn't
31
+ find a library or framework with all of the following properties:
32
+
33
+ * **Small** Some of these frameworks are really big. I need to get a job done in
34
+ time. If understanding the framework takes more time than writing my own, I
35
+ must at least feel confident that the framework I'm learning is more powerful
36
+ that what I can come up with by myself. Ruby-on-Rails is probably the biggest
37
+ framework out there, and it still lacks many features that are essential to
38
+ Rackful web service programming.
39
+
40
+ This library is small. You could read _all_ the source code in less than an
41
+ hour, and understand every detail.
42
+
43
+ * **No extensive tooling or code generation** Code generation has been the
44
+ subject of more than one flame-war over the years. Not much I can add to the
45
+ debate. <em>But still,</em> with a language as dynamic as Ruby, you just
46
+ shouldn't need code generation. Ever.
47
+
48
+ * **Full support for conditional requests** using `If-*:` request headers. Most
49
+ libraries' support is limited to `If-None-Match:` and `If-Modified-Since:`
50
+ headers, and only for `GET` and `HEAD` requests. For Rackful web services,
51
+ the `If-Match:` and `If-Unmodified-Since:` headers are at least as important,
52
+ particularly for unsafe methods like `PUT`, `POST`, `PATCH`, and `DELETE`.
53
+
54
+ This library fully supports the `ETag:` and `Last-Modified:` headers, and all
55
+ `If-*:` headers.
56
+
57
+ * **Resource centered** Some libraries claim Rackfulness, but at the same
58
+ time have a servet-like interface, which requires you to implement method
59
+ handles such as `doPOST(url)`. In these method handlers you have to find out
60
+ what resource is posted to, depending on the URL.
61
+
62
+ This library requires that you implement a Resource Factory which maps URIs
63
+ to resource Objects. These objects will then receive HTTP requests.
64
+
65
+ Hello World!
66
+ ------------
67
+
68
+ Here's a working example of a simple Rackful server:
69
+
70
+ {include:file:example/config.ru}
71
+
72
+ This file is included in the distribution as `example/config.ru`.
73
+ If you go to the `example` directory and run `rackup`, you should see
74
+ something like this:
75
+
76
+ $> rackup
77
+ [2012-07-10 11:45:32] INFO WEBrick 1.3.1
78
+ [2012-07-10 11:45:32] INFO ruby 1.9.2 (2011-12-27) [java]
79
+ [2012-07-10 11:45:32] INFO WEBrick::HTTPServer#start: pid=5994 port=9292
80
+
81
+ Go with your browser to {http://localhost:9292/} and be greeted.
82
+
83
+ In this example, we implement `GET` and `PUT` requests for the resource at '/'. but
84
+ we get a few things for free:
85
+
86
+ ### Free `OPTIONS` response:
87
+
88
+ Request:
89
+
90
+ OPTIONS / HTTP/1.1
91
+ Host: localhost:9292
92
+
93
+ Response:
94
+
95
+ HTTP/1.1 204 No Content
96
+ Allow: PUT, GET, HEAD, OPTIONS
97
+ Date: Tue, 10 Jul 2012 10:22:52 GMT
98
+
99
+ As you can see, the server accurately reports all available methods for the
100
+ resource. Notice the availability of the `HEAD` method; if you implement the
101
+ `GET` method, you'll get `HEAD` for free. It's still a good idea to explicitly
102
+ implement your own `HEAD` request handler, especially for expensive resources,
103
+ when responding to a `HEAD` request should be much more efficient than generating
104
+ a full `GET` response, and strip off the response body.
105
+
106
+ ### Free conditional request handling:
107
+
108
+ Let's first get the current state of the resource, with this request:
109
+
110
+ GET / HTTP/1.1
111
+ Host: localhost:9292
112
+
113
+ Response:
114
+
115
+ HTTP/1.1 200 OK
116
+ Content-Type: text/plain
117
+ Content-Length: 12
118
+ ETag: "86fb269d190d2c85f6e0468ceca42a20"
119
+ Date: Tue, 10 Jul 2012 10:34:36 GMT
120
+
121
+ Hello world!
122
+
123
+ Now, we'd like to change the state of the resource, but only if it's still in
124
+ the state we last saw, to avoid the "lost update problem". To do that, we
125
+ produce an `If-Match:` header, with the entity tag of our last version:
126
+
127
+ PUT / HTTP/1.1
128
+ Host: localhost:9292
129
+ Content-Type: text/plain
130
+ Content-Length: 31
131
+ If-Match: "86fb269d190d2c85f6e0468ceca42a20"
132
+
133
+ All your base are belong to us.
134
+
135
+ Response:
136
+
137
+ HTTP/1.1 204 No Content
138
+ ETag: "920c1e9267f923c62b55a471c1d8a528"
139
+ Date: Tue, 10 Jul 2012 10:58:57 GMT
140
+
141
+ The response contains an `ETag:` header, with the _new_ entity tag of this
142
+ resource. When we replay this request, we get the following response:
143
+
144
+ HTTP/1.1 412 Precondition Failed
145
+ Content-Type: text/html; charset="UTF-8"
146
+ Date: Tue, 10 Jul 2012 11:06:54 GMT
147
+
148
+ [...]
149
+ <h1>HTTP/1.1 412 Precondition Failed</h1>
150
+ <p>If-Match: "86fb269d190d2c85f6e0468ceca42a20"</p>
151
+ [...]
152
+
153
+ The server returns with status <tt>412 Precondition Failed</tt>. In the HTML
154
+ response body, the server kindly points out exactly which precondition.
155
+
156
+ Further reading
157
+ ---------------
158
+ * {Rackful::Server#initialize} for more information about your Resource Factory.
159
+ * {Rackful::Resource#etag} and {Rackful::Resource#last_modified} for more information on
160
+ conditional requests.
161
+ * {Rackful::Resource#do_METHOD} for more information about writing your own request
162
+ handlers.
163
+ * {Rackful::RelativeLocation} for more information about this piece of Rack middleware
164
+ which allows you to return relative and absolute paths in the `Location:`
165
+ response header, and why you'd want that.
166
+
167
+ Licensing
168
+ ---------
169
+ Copyright ©2011-2012 Pieter van Beek <pieterb@sara.nl>
170
+
171
+ Licensed under the Apache License 2.0. You should have received a copy of the
172
+ license as part of this distribution.
173
+ @author Pieter van Beek <rackful@djinnit.com>
174
+ =end
175
+ module Rackful
176
+
177
+
178
+ =begin markdown
179
+ Subclass of {Rack::Request}, augmented for Rackful requests.
180
+ @since 0.0.1
181
+ =end
182
+ class Request < Rack::Request
183
+
184
+
185
+ =begin markdown
186
+ The resource factory for the current request.
187
+ @return [#[]]
188
+ @see Server#initialize
189
+ @since 0.0.1
190
+ =end
191
+ attr_reader :resource_factory
192
+
193
+
194
+ def initialize resource_factory, *args
195
+ super *args
196
+ @resource_factory = resource_factory
197
+ end
198
+
199
+
200
+ =begin markdown
201
+ The request currently being processed in the current thread.
202
+
203
+ In a multi-threaded server, multiple requests can be handled at one time.
204
+ This method returns the request object, created (and registered) by
205
+ {Server#call!}
206
+ @return [Request]
207
+ @since 0.0.1
208
+ =end
209
+ def self.current
210
+ Thread.current[:djinn_request]
211
+ end
212
+
213
+
214
+ =begin markdown
215
+ Assert all <tt>If-*</tt> request headers.
216
+ @return [void]
217
+ @raise [HTTPStatus] with one of the following status codes:
218
+
219
+ - `304 Not Modified`
220
+ - `400 Bad Request` Couldn't parse one or more <tt>If-*</tt> headers, or a
221
+ weak validator comparison was requested for methods other than `GET` or
222
+ `HEAD`.
223
+ - `404 Not Found`
224
+ - `412 Precondition Failed`
225
+ - `501 Not Implemented` in case of `If-Range:` header.
226
+ @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616, section 13.3.3
227
+ for details about weak and strong validator comparison.
228
+ @todo Implement support for the `If-Range:` header.
229
+ @since 0.0.1
230
+ =end
231
+ def assert_if_headers resource
232
+ raise HTTPStatus, 'NOT_IMPLEMENTED If-Range: request header is not supported.' \
233
+ if @env.key? 'HTTP_IF_RANGE'
234
+ empty = resource.empty?
235
+ etag = ( ! empty && resource.respond_to?(:etag) ) ? resource.etag : nil
236
+ last_modified = ( ! empty && resource.respond_to?(:last_modified) ) ? resource.last_modified : nil
237
+ cond = {
238
+ :match => self.if_match,
239
+ :none_match => self.if_none_match,
240
+ :modified_since => self.if_modified_since,
241
+ :unmodified_since => self.if_unmodified_since
242
+ }
243
+ allow_weak = ['GET', 'HEAD'].include? self.request_method
244
+ if empty
245
+ if cond[:match]
246
+ raise HTTPStatus, "PRECONDITION_FAILED If-Match: #{@env['HTTP_IF_MATCH']}"
247
+ elsif cond[:unmodified_since]
248
+ raise HTTPStatus, "PRECONDITION_FAILED If-Unmodified-Since: #{@env['HTTP_IF_UNMODIFIED_SINCE']}"
249
+ elsif cond[:modified_since]
250
+ raise HTTPStatus, 'NOT_FOUND'
251
+ end
252
+ else
253
+ if cond[:none_match] && self.validate_etag( etag, cond[:none_match] )
254
+ raise HTTPStatus, "PRECONDITION_FAILED If-None-Match: #{@env['HTTP_IF_NONE_MATCH']}"
255
+ elsif cond[:match] && ! self.validate_etag( etag, cond[:match] )
256
+ raise HTTPStatus, "PRECONDITION_FAILED If-Match: #{@env['HTTP_IF_MATCH']}"
257
+ elsif cond[:unmodified_since]
258
+ if ! last_modified || cond[:unmodified_since] < last_modified[0]
259
+ raise HTTPStatus, "PRECONDITION_FAILED If-Unmodified-Since: #{@env['HTTP_IF_UNMODIFIED_SINCE']}"
260
+ elsif last_modified && ! last_modified[1] && ! allow_weak &&
261
+ cond[:unmodified_since] == last_modified[0]
262
+ raise HTTPStatus,
263
+ "PRECONDITION_FAILED If-Unmodified-Since: #{@env['HTTP_IF_UNMODIFIED_SINCE']}<br/>" +
264
+ "Modification time is a weak validator for this resource."
265
+ end
266
+ elsif cond[:modified_since]
267
+ if ! last_modified || cond[:modified_since] >= last_modified[0]
268
+ raise HTTPStatus, 'NOT_MODIFIED'
269
+ elsif last_modified && ! last_modified[1] && !allow_weak &&
270
+ cond[:modified_since] == last_modified[0]
271
+ raise HTTPStatus,
272
+ "PRECONDITION_FAILED If-Modified-Since: #{@env['HTTP_IF_MODIFIED_SINCE']}<br/>" +
273
+ "Modification time is a weak validator for this resource."
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+
280
+ =begin markdown
281
+ Hash of acceptable media types and their qualities.
282
+
283
+ This method parses the HTTP/1.1 `Accept:` header. If no acceptable media
284
+ types are provided, an empty Hash is returned.
285
+ @return [Hash{media_type => quality}]
286
+ @since 0.0.1
287
+ =end
288
+ def accept
289
+ @env['djinn.accept'] ||= begin
290
+ Hash[
291
+ @env['HTTP_ACCEPT'].to_s.split(',').collect do
292
+ |entry|
293
+ type, *options = entry.delete(' ').split(';')
294
+ quality = 1
295
+ options.each { |e|
296
+ quality = e[2..-1].to_f if e.start_with? 'q='
297
+ }
298
+ [type, quality]
299
+ end
300
+ ]
301
+ rescue
302
+ {}
303
+ end
304
+ end # def accept
305
+
306
+
307
+ =begin markdown
308
+ The best media type for the response body...
309
+
310
+ ...given the client's `Accept:` header(s) and the available representations
311
+ in the server.
312
+ @param content_types [Hash{media_type => quality}]
313
+ indicating what media types can be provided by the server, with their
314
+ relative qualities.
315
+ @param require_match [Boolean]
316
+ Should this method throw an {HTTPStatus} exception
317
+ `406 Not Acceptable` if there's no match.
318
+ @return [String]
319
+ @raise [HTTPStatus] `406 Not Acceptable`
320
+ @todo This method and its documentation seem to mix **content type** and
321
+ **media type**. I think the implementation is good, only comparing
322
+ **media types**, so all references to **content types** should be
323
+ removed.
324
+ @since 0.0.1
325
+ =end
326
+ def best_content_type content_types, require_match = true
327
+ return content_types.sort_by(&:last).last[0] if self.accept.empty?
328
+ matches = []
329
+ self.accept.each {
330
+ |accept_type, accept_quality|
331
+ content_types.each {
332
+ |response_type, response_quality|
333
+ mime_type = response_type.split(';').first.strip
334
+ if File.fnmatch( accept_type, mime_type )
335
+ matches.push [ response_type, accept_quality * response_quality ]
336
+ end
337
+ }
338
+ }
339
+ if matches.empty?
340
+ raise HTTPStatus, 'NOT_ACCEPTABLE' if require_match
341
+ nil
342
+ else
343
+ matches.sort_by(&:last).last[0]
344
+ end
345
+ end
346
+
347
+
348
+ =begin markdown
349
+ @deprecated This method seems to be unused...
350
+ @since 0.0.1
351
+ =end
352
+ def truthy? parameter
353
+ value = self.GET[parameter] || ''
354
+ %r{\A(1|t(rue)?|y(es)?|on)\z}i === value
355
+ end
356
+
357
+
358
+ =begin markdown
359
+ @deprecated This method seems to be unused...
360
+ @since 0.0.1
361
+ =end
362
+ def falsy? parameter
363
+ value = self.GET[parameter] || ''
364
+ %r{\A(0|f(alse)?|n(o)?|off)\z}i === value
365
+ end
366
+
367
+
368
+ =begin markdown
369
+ @!method if_match()
370
+ Parses the HTTP/1.1 `If-Match:` header.
371
+ @return [nil, Array<String>]
372
+ @see http://tools.ietf.org/html/rfc2616#section-14.24 RFC2616, section 14.24
373
+ @see #if_none_match
374
+ @since 0.0.1
375
+ =end
376
+ def if_match none = false
377
+ header = @env["HTTP_IF_#{ none ? 'NONE_' : '' }MATCH"]
378
+ return nil unless header
379
+ envkey = "djinn.if_#{ none ? 'none_' : '' }match"
380
+ if %r{\A\s*\*\s*\z} === header
381
+ return [ '*' ]
382
+ elsif %r{\A(\s*(W/)?"([^"\\]|\\.)*"\s*,)+\z}m === ( header + ',' )
383
+ return header.scan( %r{(?:W/)?"(?:[^"\\]|\\.)*"}m )
384
+ end
385
+ raise HTTPStatus, "BAD_REQUEST Couldn't parse If-#{ none ? 'None-' : '' }Match: #{header}"
386
+ end
387
+
388
+
389
+ =begin markdown
390
+ Parses the HTTP/1.1 `If-None-Match:` header.
391
+ @return [nil, Array<String>]
392
+ @see http://tools.ietf.org/html/rfc2616#section-14.26 RFC2616, section 14.26
393
+ @see #if_match
394
+ @since 0.0.1
395
+ =end
396
+ def if_none_match
397
+ self.if_match true
398
+ end
399
+
400
+
401
+ =begin markdown
402
+ @!method if_modified_since()
403
+ @return [nil, Time]
404
+ @see http://tools.ietf.org/html/rfc2616#section-14.25 RFC2616, section 14.25
405
+ @see #if_unmodified_since
406
+ @since 0.0.1
407
+ =end
408
+ def if_modified_since unmodified = false
409
+ header = @env["HTTP_IF_#{ unmodified ? 'UN' : '' }MODIFIED_SINCE"]
410
+ return nil unless header
411
+ begin
412
+ header = Time.httpdate( header )
413
+ rescue ArgumentError
414
+ raise HTTPStatus, "BAD_REQUEST Couldn't parse If-#{ unmodified ? 'Unmodified' : 'Modified' }-Since: #{header}"
415
+ end
416
+ header
417
+ end
418
+
419
+
420
+ =begin markdown
421
+ @return [nil, Time]
422
+ @see http://tools.ietf.org/html/rfc2616#section-14.28 RFC2616, section 14.28
423
+ @see #if_modified_since
424
+ @since 0.0.1
425
+ =end
426
+ def if_unmodified_since
427
+ self.if_modified_since true
428
+ end
429
+
430
+
431
+ =begin markdown
432
+ Does any of the tags in `etags` match `etag`?
433
+ @param etag [#to_s]
434
+ @param etags [#to_a]
435
+ @example
436
+ etag = '"foo"'
437
+ etags = [ 'W/"foo"', '"bar"' ]
438
+ validate_etag etag, etags
439
+ #> true
440
+ @return [Boolean]
441
+ @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616 section 13.3.3
442
+ for details about weak and strong validator comparison.
443
+ @since 0.0.1
444
+ =end
445
+ def validate_etag etag, etags
446
+ etag = etag.to_s
447
+ match = etags.to_a.detect do
448
+ |tag|
449
+ tag = tag.to_s
450
+ tag == '*' or
451
+ tag == etag or
452
+ 'W/' + tag == etag or
453
+ 'W/' + etag == tag
454
+ end
455
+ if match and
456
+ '*' != match and
457
+ 'W/' == etag[0,2] || 'W/' == match[0,2] and
458
+ ! [ 'HEAD', 'GET' ].include? self.request_method
459
+ raise HTTPStatus, "BAD_REQUEST Weak validators are only allowed for GET and HEAD requests."
460
+ end
461
+ !!match
462
+ end
463
+
464
+
465
+ end # class Request
466
+
467
+
468
+ =begin markdown
469
+ Mixin for resources served by {Server}.
470
+
471
+ {Server} helps you implement Rackful resource objects quickly in a couple
472
+ of ways.
473
+ Classes that include this module may implement a method `content_types`
474
+ for content negotiation. This method must return a Hash of
475
+ `mime-type => quality` pairs.
476
+ @see Server, ResourceFactory
477
+ @since 0.0.1
478
+ =end
479
+ module Resource
480
+
481
+
482
+ include Rack::Utils
483
+
484
+
485
+ =begin markdown
486
+ @!method do_METHOD( Request, Rack::Response )
487
+ HTTP/1.1 method handler.
488
+
489
+ To handle certain HTTP/1.1 request methods, resources must implement methods
490
+ called `do_<HTTP_METHOD>`.
491
+ @example Handling `GET` requests
492
+ def do_GET request, response
493
+ response['Content-Type'] = 'text/plain'
494
+ response.body = [ 'Hello world!' ]
495
+ end
496
+ @abstract
497
+ @return [void]
498
+ @raise [HTTPStatus, RuntimeError]
499
+ @since 0.0.1
500
+ =end
501
+
502
+
503
+ =begin markdown
504
+ The path of this resource.
505
+ @return [String]
506
+ @see #initialize
507
+ @since 0.0.1
508
+ =end
509
+ attr_reader :path
510
+
511
+
512
+ =begin markdown
513
+ @param path [#to_s] The path of this resource. This is a `path-absolute` as
514
+ defined in {http://tools.ietf.org/html/rfc3986#section-3.3 RFC3986, section 3.3}.
515
+ @see #path
516
+ @since 0.0.1
517
+ =end
518
+ def initialize path
519
+ @path = path.to_s
520
+ end
521
+
522
+
523
+ =begin markdown
524
+ Does this resource _exists_?
525
+
526
+ For example, a client can `PUT` to a URL that doesn't refer to a resource
527
+ yet. In that case, your {Server#resource_factory resource factory} can
528
+ produce an empty resource to to handle the `PUT` request. `HEAD` and `GET`
529
+ requests will still yield `404 Not Found`.
530
+
531
+ @return [Boolean] The default implementation returns `false`.
532
+ @since 0.0.1
533
+ =end
534
+ def empty?
535
+ false
536
+ end
537
+
538
+
539
+ =begin markdown
540
+ List of all HTTP/1.1 methods implemented by this resource.
541
+
542
+ This works by inspecting all the {#do_METHOD} methods this object implements.
543
+ @return [Array<Symbol>]
544
+ @since 0.0.1
545
+ =end
546
+ def http_methods
547
+ unless @djinn_resource_http_methods
548
+ @djinn_resource_http_methods = []
549
+ self.public_methods.each do
550
+ |public_method|
551
+ if ( match = /\Ado_([A-Z]+)\z/.match( public_method ) )
552
+ @djinn_resource_http_methods << match[1].to_sym
553
+ end
554
+ end
555
+ @djinn_resource_http_methods.delete :HEAD \
556
+ unless @djinn_resource_http_methods.include? :GET
557
+ end
558
+ @djinn_resource_http_methods
559
+ end
560
+
561
+
562
+ =begin markdown
563
+ Handles a HEAD request.
564
+
565
+ As a courtesy, this module implements a default handler for HEAD requests,
566
+ which calls {#do\_METHOD #do\_GET}, and then strips of the response body.
567
+
568
+ If this resource implements method `content_types`, then `response['Content-Type']`
569
+ will be set in the response object passed to {#do\_METHOD #do\_GET}.
570
+
571
+ Feel free to override this method at will.
572
+ @return [void]
573
+ @raise [HTTPStatus] `405 Method Not Allowed` if the resource doesn't implement the `GET` method.
574
+ @since 0.0.1
575
+ =end
576
+ def do_HEAD request, response
577
+ raise Rackful::HTTPStatus, 'METHOD_NOT_ALLOWED ' + self.http_methods.join( ' ' ) \
578
+ unless self.respond_to? :do_GET
579
+ self.do_GET request, response
580
+ response['Content-Length'] =
581
+ response.body.reduce(0) do
582
+ |memo, s| memo + bytesize(s)
583
+ end.to_s
584
+ response.body = []
585
+ end
586
+
587
+
588
+ =begin markdown
589
+ Handles an OPTIONS request.
590
+
591
+ As a courtesy, this module implements a default handler for OPTIONS
592
+ requests. It creates an `Allow:` header, listing all implemented HTTP/1.1
593
+ methods for this resource. By default, an `HTTP/1.1 204 No Content` is
594
+ returned (without an entity body).
595
+
596
+ Feel free to override this method at will.
597
+ @return [void]
598
+ @raise [HTTPStatus] `404 Not Found` if this resource is empty.
599
+ @since 0.0.1
600
+ =end
601
+ def do_OPTIONS request, response
602
+ raise Rackful::HTTPStatus, 'NOT_FOUND' if self.empty?
603
+ response.status = status_code :no_content
604
+ response.header['Allow'] = self.http_methods.join ', '
605
+ end
606
+
607
+
608
+ =begin markdown
609
+ @!attribute [r] etag
610
+ The ETag of this resource.
611
+
612
+ If your classes implement this method, then an `ETag:` response
613
+ header is generated automatically when appropriate. This allows clients to
614
+ perform conditional requests, by sending an `If-Match:` or
615
+ `If-None-Match:` request header. These conditions are then asserted
616
+ for you automatically.
617
+
618
+ Make sure your entity tag is a properly formatted string. In ABNF:
619
+
620
+ entity-tag = [ "W/" ] quoted-string
621
+
622
+ @abstract
623
+ @return [String]
624
+ @see http://tools.ietf.org/html/rfc2616#section-14.19 RFC2616 section 14.19
625
+ @since 0.0.1
626
+ =end
627
+
628
+
629
+ =begin markdown
630
+ @!attribute [r] last_modified
631
+ Last modification of this resource.
632
+
633
+ If your classes implement this method, then a `Last-Modified:` response
634
+ header is generated automatically when appropriate. This allows clients to
635
+ perform conditional requests, by sending an `If-Modified-Since:` or
636
+ `If-Unmodified-Since:` request header. These conditions are then asserted
637
+ for you automatically.
638
+ @abstract
639
+ @return [Array<(Time, Boolean)>] The timestamp, and a flag indicating if the
640
+ timestamp is a strong validator.
641
+ @see http://tools.ietf.org/html/rfc2616#section-14.29 RFC2616 section 14.29
642
+ @since 0.0.1
643
+ =end
644
+
645
+
646
+ =begin markdown
647
+ Wrapper around {#do_HEAD}
648
+ @private
649
+ @return [void]
650
+ @raise [HTTPStatus] `404 Not Found` if this resource is empty.
651
+ @since 0.0.1
652
+ =end
653
+ def http_HEAD request, response
654
+ raise Rackful::HTTPStatus, 'NOT_FOUND' if self.empty?
655
+ self.do_HEAD request, response
656
+ end
657
+
658
+
659
+ =begin markdown
660
+ Wrapper around {#do_METHOD #do_GET}
661
+ @private
662
+ @return [void]
663
+ @raise [HTTPStatus]
664
+
665
+ - `404 Not Found` if this resource is empty.
666
+ - `405 Method Not Allowed` if the resource doesn't implement the `GET` method.
667
+ @since 0.0.1
668
+ =end
669
+ def http_GET request, response
670
+ raise Rackful::HTTPStatus, 'NOT_FOUND' if self.empty?
671
+ raise Rackful::HTTPStatus, 'METHOD_NOT_ALLOWED ' + self.http_methods.join( ' ' ) \
672
+ unless self.respond_to? :do_GET
673
+ self.do_GET request, response
674
+ end
675
+
676
+
677
+ =begin markdown
678
+ Wrapper around {#do_METHOD #do_PUT}
679
+ @private
680
+ @return [void]
681
+ @raise [HTTPStatus] `405 Method Not Allowed` if the resource doesn't implement the `PUT` method.
682
+ @since 0.0.1
683
+ =end
684
+ def http_PUT request, response
685
+ raise Rackful::HTTPStatus, 'METHOD_NOT_ALLOWED ' + self.http_methods.join( ' ' ) \
686
+ unless self.respond_to? :do_PUT
687
+ self.do_PUT request, response
688
+ end
689
+
690
+
691
+ end # module Resource
692
+
693
+
694
+ =begin markdown
695
+ @todo documentation
696
+ @since 0.0.1
697
+ =end
698
+ class HTTPStatus < RuntimeError
699
+
700
+
701
+ include Rack::Utils
702
+
703
+
704
+ attr_reader :response
705
+
706
+
707
+ =begin markdown
708
+ @param message [String] `<status> [ <space> <message> ]`
709
+ @since 0.0.1
710
+ =end
711
+ def initialize( message )
712
+ @response = Rack::Response.new
713
+ matches = %r{\A(\S+)\s*(.*)\z}m.match(message.to_s)
714
+ status = status_code(matches[1].downcase.to_sym)
715
+ @response.status = status
716
+ message = matches[2]
717
+ case status
718
+ when 201, 301, 302, 303, 305, 307
719
+ message = message.split /\s+/
720
+ case message.length
721
+ when 0
722
+ message = ''
723
+ when 1
724
+ @response.header['Location'] = message[0]
725
+ message = "<p><a href=\"#{message[0]}\">#{escape_html(unescape message[0])}</a></p>"
726
+ else
727
+ message = '<ul>' + message.collect {
728
+ |url|
729
+ "\n<li><a href=\"#{url}\">#{escape_html(unescape url)}</a></li>"
730
+ }.join + '</ul>'
731
+ end
732
+ when 405 # Method not allowed
733
+ message = message.split /\s+/
734
+ @response.header['Allow'] = message.join ', '
735
+ message = '<h2>Allowed methods:</h2><ul><li>' +
736
+ message.join('</li><li>') + '</li></ul>'
737
+ when 406 # Unacceptable
738
+ message = message.split /\s+/
739
+ message = '<h2>Available representations:</h2><ul><li>' +
740
+ message.join('</li><li>') + '</li></ul>'
741
+ when 415 # Unsupported Media Type
742
+ message = message.split /\s+/
743
+ message = '<h2>Supported Media Types:</h2><ul><li>' +
744
+ message.join('</li><li>') + '</li></ul>'
745
+ end
746
+ super message
747
+ begin
748
+ REXML::Document.new \
749
+ '<?xml version="1.0" encoding="UTF-8" ?>' +
750
+ '<div>' + message + '</div>'
751
+ rescue
752
+ message = escape_html message
753
+ end
754
+ message = "<p>#{message}</p>" unless '<' == message[0, 1]
755
+ message = message.gsub( %r{\n}, "<br/>\n" )
756
+ @response.header['Content-Type'] = 'text/html; charset="UTF-8"'
757
+ @response.body = [ self.class.template.call( status, message ) ]
758
+ end
759
+
760
+
761
+ DEFAULT_TEMPLATE = lambda do
762
+ | status_code, xhtml_message |
763
+ status_code = status_code.to_i
764
+ xhtml_message = xhtml_message.to_s
765
+ <<EOS
766
+ <?xml version="1.0" encoding="UTF-8"?>
767
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
768
+ <html xmlns="http://www.w3.org/1999/xhtml">
769
+ <head>
770
+ <title>HTTP/1.1 #{status_code.to_s} #{HTTP_STATUS_CODES[status_code]}</title>
771
+ </head>
772
+ <body>
773
+ <h1>HTTP/1.1 #{status_code.to_s} #{HTTP_STATUS_CODES[status_code]}</h1>
774
+ #{xhtml_message}
775
+ </body>
776
+ </html>
777
+ EOS
778
+ end
779
+
780
+
781
+ =begin markdown
782
+ Sets/gets the current template.
783
+
784
+ The passed block must accept two arguments:
785
+
786
+ 1. *int* a status code
787
+ 2. *string* an xhtml fragment
788
+
789
+ and return a string
790
+ @since 0.0.1
791
+ =end
792
+ def self.template(&block)
793
+ @template ||= ( block || DEFAULT_TEMPLATE )
794
+ end
795
+
796
+
797
+ end
798
+
799
+
800
+ =begin markdown
801
+ Rack compliant server class for implementing RESTful web services.
802
+ @since 0.0.1
803
+ =end
804
+ class Server
805
+
806
+
807
+ =begin markdown
808
+ An object responding thread safely to method `#[]`.
809
+
810
+ A {Server} has no knowledge, and makes no presumptions, about your URI namespace.
811
+ It requires a _Resource Factory_ which produces {Resource Resources} given
812
+ a certain absolute path.
813
+
814
+ The Resource Factory you provide need only implement one method, with signature
815
+ `Resource #[]( String path )`.
816
+ This method will be called with a URI-encoded path string, and must return a
817
+ {Resource}, or `nil` if there's no resource at the given path.
818
+
819
+ For example, if a Rackful client
820
+ tries to access a resource with URI {http://example.com/your/resource http://example.com/some/resource},
821
+ then your Resource Factory can expect to be called like this:
822
+
823
+ resource = resource_factory[ '/your/resource' ]
824
+
825
+ If there's no resource at the given path, but you'd still like to respond to
826
+ `POST` or `PUT` requests to this path, you must return an
827
+ {Resource#empty? empty resource}.
828
+ @return [#[]]
829
+ @see #initialize
830
+ @since 0.0.1
831
+ =end
832
+ attr_reader :resource_factory
833
+
834
+
835
+ =begin markdown
836
+ {include:Server#resource_factory}
837
+ @since 0.0.1
838
+ =end
839
+ def initialize(resource_factory)
840
+ super()
841
+ @resource_factory = resource_factory
842
+ end
843
+
844
+
845
+ =begin markdown
846
+ As required by the Rack specification.
847
+
848
+ For thread safety, this method clones `self`, which handles the request in
849
+ {#call!}. A similar approach is taken by the Sinatra library.
850
+ @return [Array<(status_code, response_headers, response_body)>]
851
+ @since 0.0.1
852
+ =end
853
+ def call(p_env)
854
+ start = Time.now
855
+ retval = dup.call! p_env
856
+ #$stderr.puts( 'Duration: ' + ( Time.now - start ).to_s )
857
+ retval
858
+ end
859
+
860
+
861
+ =begin markdown
862
+ @return [Array<(status_code, response_headers, response_body)>]
863
+ @since 0.0.1
864
+ =end
865
+ def call!(p_env)
866
+ request = Rackful::Request.new( resource_factory, p_env )
867
+ # See also Request::current():
868
+ Thread.current[:djinn_request] = request
869
+ begin
870
+ response = Rack::Response.new
871
+ begin
872
+ raise HTTPStatus, 'NOT_FOUND' \
873
+ unless resource = self.resource_factory[request.path]
874
+ response.header['Content-Location'] = request.base_url + resource.path \
875
+ unless resource.path == request.path
876
+ request.assert_if_headers resource
877
+ if resource.respond_to? :"http_#{request.request_method}"
878
+ resource.__send__( :"http_#{request.request_method}", request, response )
879
+ elsif resource.respond_to? :"do_#{request.request_method}"
880
+ resource.__send__( :"do_#{request.request_method}", request, response )
881
+ else
882
+ raise HTTPStatus, 'METHOD_NOT_ALLOWED ' + resource.http_methods.join( ' ' )
883
+ end
884
+ rescue HTTPStatus
885
+ response = $!.response
886
+ raise if 500 == response.status
887
+ # The next line fixes a small peculiarity in RFC2616: the response body of
888
+ # a `HEAD` request _must_ be empty, even for responses outside 2xx.
889
+ if request.head?
890
+ response.body = []
891
+ response['Content-Length'] = '0'
892
+ end
893
+ end
894
+ if 201 == response.status &&
895
+ ( location = response['Location'] ) &&
896
+ ( new_resource = request.resource_factory[location] ) &&
897
+ ! new_resource.empty? \
898
+ or ( (200...300) === response.status ||
899
+ 304 == response.status ) &&
900
+ ! response['Location'] &&
901
+ ( new_resource = request.resource_factory[request.path] ) &&
902
+ ! new_resource.empty?
903
+ set_default_headers new_resource, response
904
+ end
905
+ response.finish
906
+ ensure
907
+ Thread.current[:djinn_request] = nil
908
+ end # begin
909
+ end
910
+
911
+
912
+ private
913
+
914
+
915
+ =begin markdown
916
+ Adds `ETag:` and `Last-Modified:` response headers.
917
+ @since 0.0.1
918
+ =end
919
+ def set_default_headers resource, response
920
+ if ! response.include?( 'ETag' ) &&
921
+ resource.respond_to?( :etag )
922
+ response['ETag'] = resource.etag
923
+ end
924
+ if ! response.include?( 'Last-Modified' ) &&
925
+ resource.respond_to?( :last_modified )
926
+ response['Last-Modified'] = resource.last_modified[0].httpdate
927
+ end
928
+ end
929
+
930
+
931
+ end # class Server
932
+
933
+
934
+ end # module Rackful