webmachine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +28 -0
- data/Gemfile +16 -0
- data/Guardfile +11 -0
- data/README.md +89 -0
- data/Rakefile +31 -0
- data/examples/webrick.rb +19 -0
- data/lib/webmachine/adapters/webrick.rb +74 -0
- data/lib/webmachine/adapters.rb +15 -0
- data/lib/webmachine/decision/conneg.rb +304 -0
- data/lib/webmachine/decision/flow.rb +502 -0
- data/lib/webmachine/decision/fsm.rb +79 -0
- data/lib/webmachine/decision/helpers.rb +80 -0
- data/lib/webmachine/decision.rb +12 -0
- data/lib/webmachine/dispatcher/route.rb +85 -0
- data/lib/webmachine/dispatcher.rb +40 -0
- data/lib/webmachine/errors.rb +37 -0
- data/lib/webmachine/headers.rb +16 -0
- data/lib/webmachine/locale/en.yml +28 -0
- data/lib/webmachine/request.rb +56 -0
- data/lib/webmachine/resource/callbacks.rb +362 -0
- data/lib/webmachine/resource/encodings.rb +36 -0
- data/lib/webmachine/resource.rb +48 -0
- data/lib/webmachine/response.rb +49 -0
- data/lib/webmachine/streaming.rb +27 -0
- data/lib/webmachine/translation.rb +11 -0
- data/lib/webmachine/version.rb +4 -0
- data/lib/webmachine.rb +19 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/tests.org +57 -0
- data/spec/webmachine/decision/conneg_spec.rb +152 -0
- data/spec/webmachine/decision/flow_spec.rb +1030 -0
- data/spec/webmachine/dispatcher/route_spec.rb +109 -0
- data/spec/webmachine/dispatcher_spec.rb +34 -0
- data/spec/webmachine/headers_spec.rb +19 -0
- data/spec/webmachine/request_spec.rb +24 -0
- data/webmachine.gemspec +44 -0
- metadata +137 -0
@@ -0,0 +1,502 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'webmachine/decision/conneg'
|
4
|
+
require 'webmachine/translation'
|
5
|
+
|
6
|
+
module Webmachine
|
7
|
+
module Decision
|
8
|
+
# This module encapsulates all of the decisions in Webmachine's
|
9
|
+
# flow-chart. These invoke {Resource} {Callbacks} to determine the
|
10
|
+
# appropriate response code, headers, and body for the response.
|
11
|
+
#
|
12
|
+
# This module is included into {FSM}, which drives the processing
|
13
|
+
# of the chart.
|
14
|
+
# @see http://webmachine.basho.com/images/http-headers-status-v3.png
|
15
|
+
module Flow
|
16
|
+
# Version of the flow diagram
|
17
|
+
VERSION = 3
|
18
|
+
|
19
|
+
# The first state in flow diagram
|
20
|
+
START = :b13
|
21
|
+
|
22
|
+
# Separate content-negotiation logic from flow diagram.
|
23
|
+
include Conneg
|
24
|
+
|
25
|
+
# Extract error strings into locale files
|
26
|
+
include Translation
|
27
|
+
|
28
|
+
# Handles standard decisions where halting is allowed
|
29
|
+
def decision_test(test, value, iftrue, iffalse)
|
30
|
+
case test
|
31
|
+
when value
|
32
|
+
iftrue
|
33
|
+
when Fixnum # Allows callbacks to "halt" with a given response code
|
34
|
+
test
|
35
|
+
else
|
36
|
+
iffalse
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Service available?
|
41
|
+
def b13
|
42
|
+
decision_test(resource.service_available?, true, :b12, 503)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Known method?
|
46
|
+
def b12
|
47
|
+
decision_test(resource.known_methods.include?(request.method), true, :b11, 501)
|
48
|
+
end
|
49
|
+
|
50
|
+
# URI too long?
|
51
|
+
def b11
|
52
|
+
decision_test(resource.uri_too_long?(request.uri), true, 414, :b10)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Method allowed?
|
56
|
+
def b10
|
57
|
+
if resource.allowed_methods.include?(request.method)
|
58
|
+
:b9
|
59
|
+
else
|
60
|
+
response.headers["Allow"] = resource.allowed_methods.join(", ")
|
61
|
+
405
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Content-MD5 present?
|
66
|
+
def b9
|
67
|
+
request.content_md5 ? :b9a : :b9b
|
68
|
+
end
|
69
|
+
|
70
|
+
# Content-MD5 valid?
|
71
|
+
def b9a
|
72
|
+
case valid = resource.validate_content_checksum
|
73
|
+
when Fixnum
|
74
|
+
valid
|
75
|
+
when true
|
76
|
+
:b9b
|
77
|
+
when false
|
78
|
+
response.body = "Content-MD5 header does not match request body."
|
79
|
+
400
|
80
|
+
else # not_validated
|
81
|
+
if request.content_md5 == Digest::MD5.hexdigest(request.body)
|
82
|
+
:b9b
|
83
|
+
else
|
84
|
+
response.body = "Content-MD5 header does not match request body."
|
85
|
+
400
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Malformed?
|
91
|
+
def b9b
|
92
|
+
decision_test(resource.malformed_request?, true, 400, :b8)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Authorized?
|
96
|
+
def b8
|
97
|
+
result = resource.is_authorized?(request.authorization)
|
98
|
+
case result
|
99
|
+
when true
|
100
|
+
:b7
|
101
|
+
when Fixnum
|
102
|
+
result
|
103
|
+
when String
|
104
|
+
response.headers['WWW-Authenticate'] = result
|
105
|
+
401
|
106
|
+
else
|
107
|
+
401
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Forbidden?
|
112
|
+
def b7
|
113
|
+
decision_test(resource.forbidden?, true, 403, :b6)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Okay Content-* Headers?
|
117
|
+
def b6
|
118
|
+
decision_test(resource.valid_content_headers?(request.headers.grep(/content-/)), true, :b5, 501)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Known Content-Type?
|
122
|
+
def b5
|
123
|
+
decision_test(resource.known_content_type?(request.content_type), true, :b4, 415)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Req Entity Too Large?
|
127
|
+
def b4
|
128
|
+
decision_test(resource.valid_entity_length?(request.content_length), true, :b3, 413)
|
129
|
+
end
|
130
|
+
|
131
|
+
# OPTIONS?
|
132
|
+
def b3
|
133
|
+
if request.method == "OPTIONS"
|
134
|
+
response.headers.merge!(resource.options)
|
135
|
+
200
|
136
|
+
else
|
137
|
+
:c3
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Accept exists?
|
142
|
+
def c3
|
143
|
+
if !request.accept
|
144
|
+
metadata['Content-Type'] = MediaType.parse(resource.content_types_provided.first.first)
|
145
|
+
:d4
|
146
|
+
else
|
147
|
+
:c4
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Acceptable media type available?
|
152
|
+
def c4
|
153
|
+
types = resource.content_types_provided.map {|pair| pair.first }
|
154
|
+
chosen_type = choose_media_type(types, request.accept)
|
155
|
+
if !chosen_type
|
156
|
+
406
|
157
|
+
else
|
158
|
+
metadata['Content-Type'] = chosen_type
|
159
|
+
:d4
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Accept-Language exists?
|
164
|
+
def d4
|
165
|
+
if !request.accept_language
|
166
|
+
choose_language(resource.languages_provided, "*") ? :e5 : 406
|
167
|
+
else
|
168
|
+
:d5
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Acceptable language available?
|
173
|
+
def d5
|
174
|
+
choose_language(resource.languages_provided, request.accept_language) ? :e5 : 406
|
175
|
+
end
|
176
|
+
|
177
|
+
# Accept-Charset exists?
|
178
|
+
def e5
|
179
|
+
if !request.accept_charset
|
180
|
+
choose_charset(resource.charsets_provided, "*") ? :f6 : 406
|
181
|
+
else
|
182
|
+
:e6
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Acceptable Charset available?
|
187
|
+
def e6
|
188
|
+
choose_charset(resource.charsets_provided, request.accept_charset) ? :f6 : 406
|
189
|
+
end
|
190
|
+
|
191
|
+
# Accept-Encoding exists?
|
192
|
+
# (also, set content-type header here, now that charset is chosen)
|
193
|
+
def f6
|
194
|
+
chosen_type = metadata['Content-Type']
|
195
|
+
if chosen_charset = metadata['Charset']
|
196
|
+
chosen_type.params['charset'] = chosen_charset
|
197
|
+
end
|
198
|
+
response.headers['Content-Type'] = chosen_type.to_s
|
199
|
+
if !request.accept_encoding
|
200
|
+
choose_encoding(resource.encodings_provided, "identity;q=1.0,*;q=0.5") ? :g7 : 406
|
201
|
+
else
|
202
|
+
:f7
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Acceptable encoding available?
|
207
|
+
def f7
|
208
|
+
choose_encoding(resource.encodings_provided, request.accept_encoding) ? :g7 : 406
|
209
|
+
end
|
210
|
+
|
211
|
+
# Resource exists?
|
212
|
+
def g7
|
213
|
+
# This is the first place after all conneg, so set Vary here
|
214
|
+
response.headers['Vary'] = variances.join(", ") if variances.any?
|
215
|
+
decision_test(resource.resource_exists?, true, :g8, :h7)
|
216
|
+
end
|
217
|
+
|
218
|
+
# If-Match exists?
|
219
|
+
def g8
|
220
|
+
request.if_match ? :g9 : :h10
|
221
|
+
end
|
222
|
+
|
223
|
+
# If-Match: * exists?
|
224
|
+
def g9
|
225
|
+
request.if_match == "*" ? :h10 : :g11
|
226
|
+
end
|
227
|
+
|
228
|
+
# ETag in If-Match
|
229
|
+
def g11
|
230
|
+
request_etags = request.if_match.split(/\s*,\s*/).map {|etag| unquote_header(etag) }
|
231
|
+
request_etags.include?(resource.generate_etag) ? :h10 : 412
|
232
|
+
end
|
233
|
+
|
234
|
+
# If-Match exists?
|
235
|
+
def h7
|
236
|
+
(request.if_match && unquote_header(request.if_match) == '*') ? 412 : :i7
|
237
|
+
end
|
238
|
+
|
239
|
+
# If-Unmodified-Since exists?
|
240
|
+
def h10
|
241
|
+
request.if_unmodified_since ? :h11 : :i12
|
242
|
+
end
|
243
|
+
|
244
|
+
# If-Unmodified-Since is valid date?
|
245
|
+
def h11
|
246
|
+
begin
|
247
|
+
date = Time.httpdate(request.if_unmodified_since)
|
248
|
+
metadata['If-Unmodified-Since'] = date
|
249
|
+
rescue ArgumentError
|
250
|
+
:i12
|
251
|
+
else
|
252
|
+
:h12
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Last-Modified > I-UM-S?
|
257
|
+
def h12
|
258
|
+
resource.last_modified > metadata['If-Unmodified-Since'] ? 412 : :i12
|
259
|
+
end
|
260
|
+
|
261
|
+
# Moved permanently? (apply PUT to different URI)
|
262
|
+
def i4
|
263
|
+
case uri = resource.moved_permanently?
|
264
|
+
when String, URI
|
265
|
+
response.headers["Location"] = uri.to_s
|
266
|
+
301
|
267
|
+
when Fixnum
|
268
|
+
uri
|
269
|
+
else
|
270
|
+
:p3
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# PUT?
|
275
|
+
def i7
|
276
|
+
request.method == "PUT" ? :i4 : :k7
|
277
|
+
end
|
278
|
+
|
279
|
+
# If-none-match exists?
|
280
|
+
def i12
|
281
|
+
request.if_none_match ? :i13 : :l13
|
282
|
+
end
|
283
|
+
|
284
|
+
# If-none-match: * exists?
|
285
|
+
def i13
|
286
|
+
request.if_none_match == "*" ? :j18 : :k13
|
287
|
+
end
|
288
|
+
|
289
|
+
# GET or HEAD?
|
290
|
+
def j18
|
291
|
+
%w{GET HEAD}.include?(request.method) ? 304 : 412
|
292
|
+
end
|
293
|
+
|
294
|
+
# Moved permanently?
|
295
|
+
def k5
|
296
|
+
case uri = resource.moved_permanently?
|
297
|
+
when String, URI
|
298
|
+
response.headers["Location"] = uri.to_s
|
299
|
+
301
|
300
|
+
when Fixnum
|
301
|
+
uri
|
302
|
+
else
|
303
|
+
:l5
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Previously existed?
|
308
|
+
def k7
|
309
|
+
decision_test(resource.previously_existed?, true, :k5, :l7)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Etag in if-none-match?
|
313
|
+
def k13
|
314
|
+
request_etags = request.if_none_match.split(/\s*,\s*/).map {|etag| unquote_header(etag) }
|
315
|
+
request_etags.include?(resource.generate_etag) ? :j18 : :l13
|
316
|
+
end
|
317
|
+
|
318
|
+
# Moved temporarily?
|
319
|
+
def l5
|
320
|
+
case uri = resource.moved_temporarily?
|
321
|
+
when String, URI
|
322
|
+
response.headers["Location"] = uri.to_s
|
323
|
+
307
|
324
|
+
when Fixnum
|
325
|
+
uri
|
326
|
+
else
|
327
|
+
:m5
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# POST?
|
332
|
+
def l7
|
333
|
+
request.method == "POST" ? :m7 : 404
|
334
|
+
end
|
335
|
+
|
336
|
+
# If-Modified-Since exists?
|
337
|
+
def l13
|
338
|
+
request.if_modified_since ? :l14 : :m16
|
339
|
+
end
|
340
|
+
|
341
|
+
# IMS is valid date?
|
342
|
+
def l14
|
343
|
+
begin
|
344
|
+
date = Time.httpdate(request.if_modified_since)
|
345
|
+
metadata['If-Modified-Since'] = date
|
346
|
+
rescue ArgumentError
|
347
|
+
:m16
|
348
|
+
else
|
349
|
+
:l15
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# IMS > Now?
|
354
|
+
def l15
|
355
|
+
metadata['If-Modified-Since'] > Time.now ? :m16 : :l17
|
356
|
+
end
|
357
|
+
|
358
|
+
# Last-Modified > IMS?
|
359
|
+
def l17
|
360
|
+
resource.last_modified.nil? || resource.last_modified > metadata['If-Modified-Since'] ? :m16 : 304
|
361
|
+
end
|
362
|
+
|
363
|
+
# POST?
|
364
|
+
def m5
|
365
|
+
request.method == "POST" ? :n5 : 410
|
366
|
+
end
|
367
|
+
|
368
|
+
# Server allows POST to missing resource?
|
369
|
+
def m7
|
370
|
+
decision_test(resource.allow_missing_post?, true, :n11, 404)
|
371
|
+
end
|
372
|
+
|
373
|
+
# DELETE?
|
374
|
+
def m16
|
375
|
+
request.method == "DELETE" ? :m20 : :n16
|
376
|
+
end
|
377
|
+
|
378
|
+
# DELETE enacted immediately? (Also where DELETE is forced.)
|
379
|
+
def m20
|
380
|
+
decision_test(resource.delete_resource, true, :m20b, 500)
|
381
|
+
end
|
382
|
+
|
383
|
+
def m20b
|
384
|
+
decision_test(resource.delete_completed?, true, :o20, 202)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Server allows POST to missing resource?
|
388
|
+
def n5
|
389
|
+
decision_test(resource.allow_missing_post?, true, :n11, 410)
|
390
|
+
end
|
391
|
+
|
392
|
+
# Redirect?
|
393
|
+
def n11
|
394
|
+
# Stage1
|
395
|
+
if resource.post_is_create?
|
396
|
+
case uri = resource.create_path
|
397
|
+
when nil
|
398
|
+
raise InvalidResource, t('create_path_nil', :class => resource.class)
|
399
|
+
when URI, String
|
400
|
+
base_uri = resource.base_uri || request.base_uri
|
401
|
+
new_uri = URI.join(base_uri.to_s, uri)
|
402
|
+
request.disp_path = new_uri.path
|
403
|
+
response.headers['Location'] = new_uri.to_s
|
404
|
+
result = accept_helper
|
405
|
+
return result if Fixnum === result
|
406
|
+
end
|
407
|
+
else
|
408
|
+
case result = resource.process_post
|
409
|
+
when true
|
410
|
+
encode_body_if_set
|
411
|
+
when Fixnum
|
412
|
+
return result
|
413
|
+
else
|
414
|
+
raise InvalidResource, t('process_post_invalid', :result => result)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
if response.is_redirect?
|
418
|
+
if response.headers['Location']
|
419
|
+
303
|
420
|
+
else
|
421
|
+
raise InvalidResource, t('do_redirect')
|
422
|
+
end
|
423
|
+
else
|
424
|
+
:p11
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# POST?
|
429
|
+
def n16
|
430
|
+
request.method == "POST" ? :n11 : :o16
|
431
|
+
end
|
432
|
+
|
433
|
+
# Conflict?
|
434
|
+
def o14
|
435
|
+
if resource.is_conflict?
|
436
|
+
409
|
437
|
+
else
|
438
|
+
res = accept_helper
|
439
|
+
(Fixnum === res) ? res : :p11
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# PUT?
|
444
|
+
def o16
|
445
|
+
request.method == "PUT" ? :o14 : :o18
|
446
|
+
end
|
447
|
+
|
448
|
+
# Multiple representations?
|
449
|
+
# Also where body generation for GET and HEAD is done.
|
450
|
+
def o18
|
451
|
+
if request.method =~ /^(GET|HEAD)$/
|
452
|
+
if etag = resource.generate_etag
|
453
|
+
response.headers['ETag'] = ensure_quoted_header(etag)
|
454
|
+
end
|
455
|
+
if last_modified = resource.last_modified
|
456
|
+
response.headers['Last-Modified'] = last_modified.httpdate
|
457
|
+
end
|
458
|
+
if expires = resource.expires
|
459
|
+
response.headers['Expires'] = expires.httpdate
|
460
|
+
end
|
461
|
+
content_type = metadata['Content-Type']
|
462
|
+
handler = resource.content_types_provided.find {|ct, _| content_type.type_matches?(MediaType.parse(ct)) }.last
|
463
|
+
result = resource.send(handler)
|
464
|
+
if Fixnum === result
|
465
|
+
result
|
466
|
+
else
|
467
|
+
response.body = result
|
468
|
+
encode_body
|
469
|
+
:o18b
|
470
|
+
end
|
471
|
+
else
|
472
|
+
:o18b
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
# Multiple choices?
|
477
|
+
def o18b
|
478
|
+
decision_test(resource.multiple_choices?, true, 300, 200)
|
479
|
+
end
|
480
|
+
|
481
|
+
# Response includes an entity?
|
482
|
+
def o20
|
483
|
+
has_response_body? ? :o18 : 204
|
484
|
+
end
|
485
|
+
|
486
|
+
# Conflict?
|
487
|
+
def p3
|
488
|
+
if resource.is_conflict?
|
489
|
+
409
|
490
|
+
else
|
491
|
+
res = accept_helper
|
492
|
+
(Fixnum === res) ? res : :p11
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
# New resource?
|
497
|
+
def p11
|
498
|
+
!response.headers["Location"] ? :o20 : 201
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'webmachine/decision/helpers'
|
2
|
+
require 'webmachine/decision/fsm'
|
3
|
+
require 'webmachine/translation'
|
4
|
+
|
5
|
+
module Webmachine
|
6
|
+
module Decision
|
7
|
+
# Implements the finite-state machine described by the Webmachine
|
8
|
+
# sequence diagram.
|
9
|
+
class FSM
|
10
|
+
include Flow
|
11
|
+
include Helpers
|
12
|
+
include Translation
|
13
|
+
|
14
|
+
attr_reader :resource, :request, :response, :metadata
|
15
|
+
|
16
|
+
def initialize(resource, request, response)
|
17
|
+
@resource, @request, @response = resource, request, response
|
18
|
+
@metadata = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Processes the request, iteratively invoking the decision methods in {Flow}.
|
22
|
+
def run
|
23
|
+
begin
|
24
|
+
state = Flow::START
|
25
|
+
loop do
|
26
|
+
response.trace << state
|
27
|
+
result = send(state)
|
28
|
+
case result
|
29
|
+
when Fixnum # Response code
|
30
|
+
respond(result)
|
31
|
+
break
|
32
|
+
when Symbol # Next state
|
33
|
+
state = result
|
34
|
+
else # You bwoke it
|
35
|
+
raise InvalidResource, t('fsm_broke', :state => state, :result => result.inspect)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
rescue MalformedRequest => malformed
|
39
|
+
Webmachine.render_error(400, request, response, :message => malformed.message)
|
40
|
+
respond(400)
|
41
|
+
rescue => e # Handle all exceptions without crashing the server
|
42
|
+
error_response(e, state)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def respond(code, headers={})
|
48
|
+
response.headers.merge!(headers)
|
49
|
+
end_time = Time.now
|
50
|
+
case code
|
51
|
+
when 404
|
52
|
+
Webmachine.render_error(code, request, response)
|
53
|
+
when 304
|
54
|
+
response.headers.delete('Content-Type')
|
55
|
+
if etag = resource.generate_etag
|
56
|
+
response.headers['ETag'] = ensure_quoted_header(etag)
|
57
|
+
end
|
58
|
+
if expires = resource.expires
|
59
|
+
response.headers['Expires'] = expires.httpdate
|
60
|
+
end
|
61
|
+
if modified = resource.last_modified
|
62
|
+
response.headers['Last-Modified'] = modified.httpdate
|
63
|
+
end
|
64
|
+
end
|
65
|
+
response.code = code
|
66
|
+
resource.finish_request
|
67
|
+
# TODO: add logging/tracing
|
68
|
+
end
|
69
|
+
|
70
|
+
# Renders a 500 error by capturing the exception information.
|
71
|
+
def error_response(exception, state)
|
72
|
+
response.error = [exception.message, exception.backtrace].flatten.join("\n ")
|
73
|
+
response.end_state = state
|
74
|
+
Webmachine.render_error(500, request, response)
|
75
|
+
respond(500)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'webmachine/streaming'
|
2
|
+
module Webmachine
|
3
|
+
module Decision
|
4
|
+
# Methods that assist the Decision {Flow}.
|
5
|
+
module Helpers
|
6
|
+
QUOTED = /^"(.*)"$/
|
7
|
+
|
8
|
+
# Determines if the response has a body/entity set.
|
9
|
+
def has_response_body?
|
10
|
+
!response.body.nil? && !response.body.empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
# If the response body exists, encode it.
|
14
|
+
# @see #encode_body
|
15
|
+
def encode_body_if_set
|
16
|
+
encode_body if has_response_body?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Encodes the body in the selected charset and encoding.
|
20
|
+
def encode_body
|
21
|
+
body = response.body
|
22
|
+
chosen_charset = metadata['Charset']
|
23
|
+
chosen_encoding = metadata['Content-Encoding']
|
24
|
+
charsetter = resource.charsets_provided && resource.charsets_provided.find {|c,_| c == chosen_charset }.last || :charset_nop
|
25
|
+
encoder = resource.encodings_provided[chosen_encoding]
|
26
|
+
response.body = case body
|
27
|
+
when String # 1.8 treats Strings as Enumerable
|
28
|
+
resource.send(encoder, resource.send(charsetter, body))
|
29
|
+
when Enumerable
|
30
|
+
EnumerableEncoder.new(resource, encoder, charsetter, body)
|
31
|
+
when body.respond_to?(:call)
|
32
|
+
CallableEncoder.new(resource, encoder, charsetter, body)
|
33
|
+
else
|
34
|
+
resource.send(encoder, resource.send(charsetter, body))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Ensures that a header is quoted (like ETag)
|
39
|
+
def ensure_quoted_header(value)
|
40
|
+
if value =~ QUOTED
|
41
|
+
value
|
42
|
+
else
|
43
|
+
'"' << value << '"'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Unquotes request headers (like ETag)
|
48
|
+
def unquote_header(value)
|
49
|
+
if value =~ QUOTED
|
50
|
+
$1
|
51
|
+
else
|
52
|
+
value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Assists in receiving request bodies
|
57
|
+
def accept_helper
|
58
|
+
content_type = request.content_type || 'application/octet-stream'
|
59
|
+
mt = Conneg::MediaType.parse(content_type)
|
60
|
+
metadata['mediaparams'] = mt.params
|
61
|
+
acceptable = resource.content_types_accepted.find {|ct, _| mt.type_matches?(Conneg::MediaType.parse(ct)) }
|
62
|
+
if acceptable
|
63
|
+
resource.send(acceptable.last)
|
64
|
+
else
|
65
|
+
415
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Computes the entries for the 'Vary' response header
|
70
|
+
def variances
|
71
|
+
resource.variances.tap do |v|
|
72
|
+
v.unshift "Accept-Language" if resource.languages_provided.size > 1
|
73
|
+
v.unshift "Accept-Charset" if resource.charsets_provided && resource.charsets_provided.size > 1
|
74
|
+
v.unshift "Accept-Encoding" if resource.encodings_provided.size > 1
|
75
|
+
v.unshift "Accept" if resource.content_types_provided.size > 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'webmachine/decision/helpers'
|
2
|
+
require 'webmachine/decision/conneg'
|
3
|
+
require 'webmachine/decision/flow'
|
4
|
+
require 'webmachine/decision/fsm'
|
5
|
+
|
6
|
+
module Webmachine
|
7
|
+
# This module encapsulates the logic related to delivering the
|
8
|
+
# proper HTTP response, given the constraints of the {Request} and
|
9
|
+
# the {Resource} which is being processed.
|
10
|
+
module Decision
|
11
|
+
end
|
12
|
+
end
|