webmachine 0.1.0
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/.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
|