strelka 0.0.1pre4 → 0.0.1.pre129

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/History.rdoc +1 -1
  2. data/IDEAS.rdoc +62 -0
  3. data/Manifest.txt +38 -7
  4. data/README.rdoc +124 -5
  5. data/Rakefile +22 -6
  6. data/bin/leash +102 -157
  7. data/contrib/hoetemplate/.autotest.erb +23 -0
  8. data/contrib/hoetemplate/History.rdoc.erb +4 -0
  9. data/contrib/hoetemplate/Manifest.txt.erb +8 -0
  10. data/contrib/hoetemplate/README.rdoc.erb +17 -0
  11. data/contrib/hoetemplate/Rakefile.erb +24 -0
  12. data/contrib/hoetemplate/data/file_name/apps/file_name_app +36 -0
  13. data/contrib/hoetemplate/data/file_name/templates/layout.tmpl.erb +13 -0
  14. data/contrib/hoetemplate/data/file_name/templates/top.tmpl.erb +8 -0
  15. data/contrib/hoetemplate/lib/file_name.rb.erb +18 -0
  16. data/contrib/hoetemplate/spec/file_name_spec.rb.erb +21 -0
  17. data/data/strelka/apps/hello-world +30 -0
  18. data/lib/strelka/app/defaultrouter.rb +49 -30
  19. data/lib/strelka/app/errors.rb +121 -0
  20. data/lib/strelka/app/exclusiverouter.rb +40 -0
  21. data/lib/strelka/app/filters.rb +18 -7
  22. data/lib/strelka/app/negotiation.rb +122 -0
  23. data/lib/strelka/app/parameters.rb +171 -14
  24. data/lib/strelka/app/paramvalidator.rb +751 -0
  25. data/lib/strelka/app/plugins.rb +66 -46
  26. data/lib/strelka/app/restresources.rb +499 -0
  27. data/lib/strelka/app/router.rb +73 -0
  28. data/lib/strelka/app/routing.rb +140 -18
  29. data/lib/strelka/app/templating.rb +12 -3
  30. data/lib/strelka/app.rb +174 -24
  31. data/lib/strelka/constants.rb +0 -20
  32. data/lib/strelka/exceptions.rb +29 -0
  33. data/lib/strelka/httprequest/acceptparams.rb +377 -0
  34. data/lib/strelka/httprequest/negotiation.rb +257 -0
  35. data/lib/strelka/httprequest.rb +155 -7
  36. data/lib/strelka/httpresponse/negotiation.rb +579 -0
  37. data/lib/strelka/httpresponse.rb +140 -0
  38. data/lib/strelka/logging.rb +4 -1
  39. data/lib/strelka/mixins.rb +53 -0
  40. data/lib/strelka.rb +22 -1
  41. data/spec/data/error.tmpl +1 -0
  42. data/spec/lib/constants.rb +0 -1
  43. data/spec/lib/helpers.rb +21 -0
  44. data/spec/strelka/app/defaultrouter_spec.rb +41 -35
  45. data/spec/strelka/app/errors_spec.rb +212 -0
  46. data/spec/strelka/app/exclusiverouter_spec.rb +220 -0
  47. data/spec/strelka/app/filters_spec.rb +196 -0
  48. data/spec/strelka/app/negotiation_spec.rb +73 -0
  49. data/spec/strelka/app/parameters_spec.rb +149 -0
  50. data/spec/strelka/app/paramvalidator_spec.rb +1059 -0
  51. data/spec/strelka/app/plugins_spec.rb +26 -19
  52. data/spec/strelka/app/restresources_spec.rb +393 -0
  53. data/spec/strelka/app/router_spec.rb +63 -0
  54. data/spec/strelka/app/routing_spec.rb +183 -9
  55. data/spec/strelka/app/templating_spec.rb +1 -2
  56. data/spec/strelka/app_spec.rb +265 -32
  57. data/spec/strelka/exceptions_spec.rb +53 -0
  58. data/spec/strelka/httprequest/acceptparams_spec.rb +282 -0
  59. data/spec/strelka/httprequest/negotiation_spec.rb +246 -0
  60. data/spec/strelka/httprequest_spec.rb +204 -14
  61. data/spec/strelka/httpresponse/negotiation_spec.rb +464 -0
  62. data/spec/strelka/httpresponse_spec.rb +114 -0
  63. data/spec/strelka/mixins_spec.rb +99 -0
  64. data.tar.gz.sig +1 -0
  65. metadata +175 -79
  66. metadata.gz.sig +2 -0
  67. data/IDEAS.textile +0 -174
  68. data/data/strelka/apps/strelka-admin +0 -65
  69. data/data/strelka/apps/strelka-setup +0 -26
  70. data/data/strelka/bootstrap-config.rb +0 -34
  71. data/data/strelka/templates/admin/console.tmpl +0 -21
  72. data/data/strelka/templates/layout.tmpl +0 -30
  73. data/lib/strelka/process.rb +0 -19
@@ -0,0 +1,579 @@
1
+ #!usr/bin/env ruby
2
+
3
+ require 'set'
4
+ require 'yaml'
5
+ require 'yajl'
6
+
7
+ require 'strelka/constants'
8
+ require 'strelka/exceptions'
9
+ require 'strelka/httpresponse' unless defined?( Strelka::HTTPResponse )
10
+
11
+
12
+ # The mixin that adds methods to Strelka::HTTPResponse for content-negotiation.
13
+ #
14
+ # response = request.response
15
+ # response.for( 'text/html' ) {...}
16
+ # response.for( :json ) {...}
17
+ # response.for_encoding( :en ) {...}
18
+ # response.for_language( :en ) {...}
19
+ #
20
+ # If the response was created from a request, it also knows whether or not it
21
+ # is acceptable according to its request's `Accept*` headers.
22
+ #
23
+ module Strelka::HTTPResponse::Negotiation
24
+ include Strelka::Constants
25
+
26
+ # TODO: Perhaps replace this with something like this:
27
+ # Mongrel2::Config::Mimetype.to_hash( :extension => :mimetype )
28
+ BUILTIN_MIMETYPES = {
29
+ :html => 'text/html',
30
+ :text => 'text/plain',
31
+
32
+ :yaml => 'application/x-yaml',
33
+ :json => 'application/json',
34
+
35
+ :jpeg => 'image/jpeg',
36
+ :png => 'image/png',
37
+ :gif => 'image/gif',
38
+
39
+ :rdf => 'application/rdf+xml',
40
+ :rss => 'application/rss+xml',
41
+ :atom => 'application/atom+xml',
42
+ }
43
+
44
+ # A collection of stringifier callbacks, keyed by mimetype. If an object other
45
+ # than a String is returned by a content callback, and an entry for the callback's
46
+ # mimetype exists in this Hash, it will be #call()ed to stringify the object.
47
+ STRINGIFIERS = {
48
+ 'application/x-yaml' => YAML.method( :dump ),
49
+ 'application/json' => Yajl.method( :dump ),
50
+ }
51
+
52
+
53
+ ### Add some instance variables for negotiation.
54
+ def initialize( * )
55
+ @mediatype_callbacks = {}
56
+ @language_callbacks = {}
57
+ @encoding_callbacks = {}
58
+
59
+ @vary_fields = Set.new
60
+
61
+ super
62
+ end
63
+
64
+
65
+ ######
66
+ public
67
+ ######
68
+
69
+ # The Hash of mediatype alternative callbacks for content negotiation,
70
+ # keyed by mimetype.
71
+ attr_reader :mediatype_callbacks
72
+
73
+ # The Hash of language alternative callbacks for content negotiation,
74
+ # keyed by language tag String.
75
+ attr_reader :language_callbacks
76
+
77
+ # The Hash of document coding alternative callbacks for content
78
+ # negotiation, keyed by coding name.
79
+ attr_reader :encoding_callbacks
80
+
81
+ # A Set of header fields to add to the 'Vary:' response header.
82
+ attr_reader :vary_fields
83
+
84
+
85
+ ### Overridden to reset content-negotiation callbacks, too.
86
+ def reset
87
+ super
88
+
89
+ @mediatype_callbacks.clear
90
+ @language_callbacks.clear
91
+ @encoding_callbacks.clear
92
+
93
+ # Not clearing the Vary: header for now, as it's useful in a 406 to
94
+ # determine what accept-* headers can be modified to get an acceptable
95
+ # response
96
+ # @vary_fields.clear
97
+ end
98
+
99
+
100
+ ### Overridden to add a Vary: header to outgoing headers if the response has
101
+ ### any #vary_fields.
102
+ def normalized_headers
103
+ headers = super
104
+
105
+ unless self.vary_fields.empty?
106
+ self.log.debug "Adding Vary header for %p" % [ self.vary_fields ]
107
+ headers.vary = self.vary_fields.to_a.join( ', ' )
108
+ end
109
+
110
+ return headers
111
+ end
112
+
113
+
114
+ ### Stringify the response -- overridden to use the negotiated body.
115
+ def to_s
116
+ return [
117
+ self.status_line,
118
+ self.header_data,
119
+ self.negotiated_body
120
+ ].join( "\r\n" )
121
+ end
122
+
123
+
124
+ ### Transform the entity body if it doesn't meet the criteria
125
+ def negotiated_body
126
+ return '' if self.bodiless?
127
+
128
+ self.negotiate
129
+ return self.body
130
+ end
131
+
132
+
133
+ ### Check for any negotiation that should happen and apply the necessary
134
+ ### transforms if they're available.
135
+ def negotiate
136
+ return if !self.request
137
+ self.transform_content_type
138
+ self.transform_language
139
+ self.transform_charset
140
+ self.transform_encoding
141
+ end
142
+
143
+
144
+ #
145
+ # :section: Acceptance Predicates
146
+ #
147
+
148
+ ### Return true if the receiver satisfies all of its originating request's
149
+ ### Accept* headers, or it's a bodiless response.
150
+ def acceptable?
151
+ # self.negotiate
152
+ return self.bodiless? ||
153
+ ( self.acceptable_content_type? &&
154
+ self.acceptable_charset? &&
155
+ self.acceptable_language? &&
156
+ self.acceptable_encoding? )
157
+ end
158
+ alias_method :is_acceptable?, :acceptable?
159
+
160
+
161
+ ### Returns true if the content-type of the response is set to a
162
+ ### mediatype that was designated as acceptable by the originating
163
+ ### request, or if there was no originating request.
164
+ def acceptable_content_type?
165
+ req = self.request or return true
166
+ answer = req.accepts?( self.content_type )
167
+ self.log.warn "Content-type %p NOT acceptable: %p" %
168
+ [ self.content_type, req.accepted_mediatypes ] unless answer
169
+
170
+ return answer
171
+ end
172
+ alias_method :has_acceptable_content_type?, :acceptable_content_type?
173
+
174
+
175
+ ### Returns true if the receiver's #charset is set to a value that was
176
+ ### designated as acceptable by the originating request, or if there
177
+ ### was no originating request.
178
+ def acceptable_charset?
179
+ req = self.request or return true
180
+ charset = self.find_header_charset
181
+
182
+ # Types other than text are binary, and so aren't subject to charset
183
+ # acceptability.
184
+ # For 'text/' subtypes:
185
+ # When no explicit charset parameter is provided by the sender, media
186
+ # subtypes of the "text" type are defined to have a default charset
187
+ # value of "ISO-8859-1" when received via HTTP. [RFC2616 3.7.1]
188
+ if charset == Encoding::ASCII_8BIT
189
+ return true unless self.content_type.start_with?( 'text/' )
190
+ charset = Encoding::ISO8859_1
191
+ end
192
+
193
+ answer = req.accepts_charset?( charset )
194
+ self.log.warn "Content-charset %p NOT acceptable: %p" %
195
+ [ self.find_header_charset, req.accepted_charsets ] unless answer
196
+
197
+ return answer
198
+ end
199
+ alias_method :has_acceptable_charset?, :acceptable_charset?
200
+
201
+
202
+ ### Returns true if at least one of the receiver's #languages is set
203
+ ### to a value that was designated as acceptable by the originating
204
+ ### request, if there was no originating request, or if no #languages
205
+ ### have been set for a non-empty entity body.
206
+ def acceptable_language?
207
+ req = self.request or return true
208
+
209
+ # Lack of an accept-language field means all languages are accepted
210
+ return true if req.accepted_languages.empty?
211
+
212
+ # If no language is given for an existing entity body, there's no way
213
+ # to know whether or not there's a better alternative
214
+ return true if self.languages.empty?
215
+
216
+ # If any of the languages present for the body are accepted, the
217
+ # request is acceptable. Or at least that's what I got out of
218
+ # reading RFC2616, Section 14.4.
219
+ answer = self.languages.any? {|lang| req.accepts_language?(lang) }
220
+ self.log.warn "Content-language %p NOT acceptable: %s" %
221
+ [ self.languages, req.accepted_languages ] unless answer
222
+
223
+ return answer
224
+ end
225
+ alias_method :has_acceptable_language?, :acceptable_language?
226
+
227
+
228
+ ### Returns true if all of the receiver's #encodings were designated
229
+ ### as acceptable by the originating request, if there was no originating
230
+ ### request, or if no #encodings have been set.
231
+ def acceptable_encoding?
232
+ req = self.request or return true
233
+
234
+ encs = self.encodings.dup
235
+ encs << 'identity' if encs.empty?
236
+
237
+ answer = encs.all? {|enc| req.accepts_encoding?(enc) }
238
+ self.log.warn "Content-encoding %p NOT acceptable: %s" %
239
+ [ encs, req.accepted_encodings ] unless answer
240
+
241
+ return answer
242
+ end
243
+ alias_method :has_acceptable_encoding?, :acceptable_encoding?
244
+
245
+
246
+ #
247
+ # :section: Content-type Callbacks
248
+ #
249
+
250
+ ### Register a callback that will be called during transparent content
251
+ ### negotiation for the entity body if one or more of the specified
252
+ ### +mediatypes+ is among the requested alternatives. The +mediatypes+
253
+ ### can be either mimetype Strings or Symbols that correspond to keys
254
+ ### in the BUILTIN_MIMETYPES hash. The +callback+ will be called with
255
+ ### the desired mimetype, and should return the new value for the entity
256
+ ### body if it successfully transformed the body, or a false value if
257
+ ### the next alternative should be tried instead.
258
+ ### If successful, the response's body will be set to the new value,
259
+ ### its content_type set to the new mimetype, and its status changed
260
+ ### to HTTP::OK.
261
+ def for( *mediatypes, &callback )
262
+ mediatypes.each do |mimetype|
263
+ mimetype = BUILTIN_MIMETYPES[ mimetype ] if mimetype.is_a?( Symbol )
264
+ self.mediatype_callbacks[ mimetype ] = callback
265
+ end
266
+
267
+ # Include the 'Accept:' header in the 'Vary:' header
268
+ self.vary_fields.add( 'accept' )
269
+ end
270
+
271
+
272
+ ### Returns Strelka::HTTPRequest::MediaType objects for mediatypes that have
273
+ ### a higher qvalue than the current response's entity body (if any).
274
+ def better_mediatypes
275
+ req = self.request or return []
276
+ return [] unless req.headers.accept
277
+
278
+ current_qvalue = 0.0
279
+ mediatypes = req.accepted_mediatypes.sort
280
+
281
+ # If the current mediatype exists in the Accept: header, reset the current qvalue
282
+ # to whatever its qvalue is
283
+ if self.content_type
284
+ mediatype = mediatypes.find {|mt| mt =~ self.content_type }
285
+ current_qvalue = mediatype.qvalue if mediatype
286
+ end
287
+
288
+ self.log.debug "Looking for better mediatypes than %p (%0.2f)" %
289
+ [ self.content_type, current_qvalue ]
290
+
291
+ return mediatypes.find_all do |mt|
292
+ mt.qvalue > current_qvalue
293
+ end
294
+ end
295
+
296
+
297
+ ### Iterate over the originating request's acceptable content types in
298
+ ### qvalue+listed order, looking for a content negotiation callback for
299
+ ### each mediatype. If any are found, they are tried in declared order
300
+ ### until one returns a true-ish value, which becomes the new entity
301
+ ### body. If the body object is not a String,
302
+ def transform_content_type
303
+ return if self.mediatype_callbacks.empty?
304
+
305
+ self.log.debug "Applying content-type transforms (if any)"
306
+ self.better_mediatypes.each do |mediatype|
307
+ callbacks = self.mediatype_callbacks.find_all do |mimetype, _|
308
+ mediatype =~ mimetype
309
+ end
310
+
311
+ if callbacks.empty?
312
+ self.log.debug " no transforms for %s" % [ mediatype ]
313
+ else
314
+ callbacks.each do |mimetype, callback|
315
+ return if self.try_content_type_callback( mimetype, callback )
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+
322
+ ### Attempt to apply the +callback+ for the specified +mediatype+ to the entity
323
+ ### body, making the necessary changes to the request and returning +true+ if
324
+ ### the callback returns a new entity body, or returning a false value if it doesn't.
325
+ def try_content_type_callback( mimetype, callback )
326
+ self.log.debug " trying content-type callback %p (%s)" % [ callback, mimetype ]
327
+
328
+ new_body = callback.call( mimetype ) or return false
329
+
330
+ self.log.debug " successfully transformed! Setting up response."
331
+ new_body = STRINGIFIERS[ mimetype ].call( new_body ) if
332
+ STRINGIFIERS.key?( mimetype ) && !new_body.is_a?( String )
333
+
334
+ self.body = new_body
335
+ self.content_type = mimetype
336
+ self.status ||= HTTP::OK
337
+
338
+ return true
339
+ end
340
+
341
+
342
+ #
343
+ # :section: Language negotiation callbacks
344
+ #
345
+
346
+ ### Register a callback that will be called during transparent content
347
+ ### negotiation for the entity body if one or more of the specified
348
+ ### +language_tags+ is among the requested alternatives. The +language_tags+
349
+ ### are Strings in the form described by RFC2616, section 3.10. The
350
+ ### +callback+ will be called with the desired language code, and should
351
+ ### return the new value for the entity body if it has value for the
352
+ ### body, or a false value if the next alternative should be tried
353
+ ### instead. If successful, the response's body will be set to the new
354
+ ### value, and its status changed to HTTP::OK.
355
+ def for_language( *language_tags, &callback )
356
+ language_tags.flatten.each do |lang|
357
+ self.language_callbacks[ lang.to_sym ] = callback
358
+ end
359
+
360
+ # Include the 'Accept-Language:' header in the 'Vary:' header
361
+ self.vary_fields.add( 'accept-language' )
362
+ end
363
+
364
+
365
+ ### Returns Strelka::HTTPRequest::Language objects for natural languages that have
366
+ ### a higher qvalue than the current response's entity body (if any).
367
+ def better_languages
368
+ req = self.request or return []
369
+
370
+ current_qvalue = 0.0
371
+ accepted_languages = req.accepted_languages.sort
372
+
373
+ # If any of the current languages exists in the Accept-Language: header, reset
374
+ # the current qvalue to the highest one among them
375
+ unless self.languages.empty?
376
+ current_qvalue = self.languages.reduce( current_qvalue ) do |qval, lang|
377
+ accepted_lang = accepted_languages.find {|alang| alang =~ lang } or
378
+ next qval
379
+ qval > accepted_lang.qvalue ? qval : accepted_lang.qvalue
380
+ end
381
+ end
382
+
383
+ self.log.debug "Looking for better languages than %p (%0.2f)" %
384
+ [ self.languages.join(', '), current_qvalue ]
385
+
386
+ return accepted_languages.find_all do |lang|
387
+ lang.qvalue > current_qvalue
388
+ end
389
+ end
390
+
391
+
392
+ ### If there are any languages that have a higher qvalue than the one/s in #languages,
393
+ ### look for a negotiation callback that provides that language. If any are found, they
394
+ ### are tried in declared order until one returns a true-ish value, which becomes the new
395
+ ### entity body.
396
+ def transform_language
397
+ return if self.language_callbacks.empty?
398
+
399
+ self.log.debug "Looking for language transformations"
400
+ self.better_languages.uniq.each do |lang|
401
+ callback = langcode = nil
402
+
403
+ if lang.primary_tag
404
+ langcode = lang.language_range
405
+ callback = self.language_callbacks[ lang.primary_tag.to_sym ]
406
+ else
407
+ langcode, callback = self.language_callbacks.first
408
+ end
409
+
410
+ next unless callback
411
+
412
+ self.log.debug " found a callback for %s: %p" % [ langcode, callback ]
413
+ if (( new_body = callback.call(langcode) ))
414
+ self.body = new_body
415
+ self.languages.replace([ langcode.to_s ])
416
+ self.log.debug " success."
417
+ break
418
+ end
419
+
420
+ end
421
+ end
422
+
423
+
424
+ #
425
+ # :section: Charset negotiation callbacks
426
+ #
427
+
428
+ ### Returns Strelka::HTTPRequest::Charset objects for accepted character sets that have
429
+ ### a higher qvalue than the one used by the current response.
430
+ def better_charsets
431
+ req = self.request or return []
432
+ return [] unless self.content_type &&
433
+ self.content_type.start_with?( 'text/', 'application/' )
434
+ return [] unless req.headers.accept_charset
435
+
436
+ current_qvalue = 0.0
437
+ charsets = req.accepted_charsets.sort
438
+ current_charset = self.find_header_charset
439
+
440
+ # If the current charset exists in the Accept-Charset: header, reset the current qvalue
441
+ # to whatever its qvalue is
442
+ if current_charset != Encoding::ASCII_8BIT
443
+ charset = charsets.find {|mt| mt =~ current_charset }
444
+ current_qvalue = charset.qvalue if charset
445
+ end
446
+
447
+ self.log.debug "Looking for better charsets than %p (%0.2f)" %
448
+ [ current_charset, current_qvalue ]
449
+
450
+ return charsets.sort.find_all do |cs|
451
+ cs.qvalue > current_qvalue
452
+ end
453
+ end
454
+
455
+
456
+ ### Iterate over the originating request's acceptable charsets in
457
+ ### qvalue+listed order, attempting to transcode the current entity body
458
+ ### if it
459
+ def transform_charset
460
+ self.log.debug "Looking for charset transformations."
461
+ self.better_charsets.each do |charset|
462
+ self.log.debug " trying to transcode to: %s" % [ charset ]
463
+
464
+ if self.body.respond_to?( :encode )
465
+ self.log.debug " body is a string; trying direct transcoding"
466
+ if self.transcode_body_string( charset )
467
+ self.log.debug " success; body is now %p" % [ self.body.encoding ]
468
+ self.vary_fields.add( 'accept-charset' )
469
+ break
470
+ end
471
+
472
+ # Can change the external_encoding if it's a File that has a #path
473
+ elsif self.body.respond_to?( :external_encoding )
474
+ raise NotImplementedError,
475
+ "Support for transcoding %p objects isn't done." % [ self.body.class ]
476
+ else
477
+ self.log.warn "Don't know how to transcode a %p" % [ self.body.class ]
478
+ end
479
+ end
480
+ end
481
+
482
+
483
+ ### Try to transcode the entity body String to one of the specified +charsets+. Returns
484
+ ### the succesful Encoding object if transcoding succeeded, or +nil+ if transcoding
485
+ ### failed.
486
+ def transcode_body_string( charset )
487
+ unless enc = charset.encoding_object
488
+ self.log.warn " unsupported charset: %s" % [ charset ]
489
+ return false
490
+ end
491
+
492
+ succeeded = false
493
+ begin
494
+ succeeded = self.body.encode!( enc )
495
+ rescue Encoding::UndefinedConversionError => err
496
+ self.log.error "%p while transcoding: %s" % [ err.class, err.message ]
497
+ end
498
+
499
+ return succeeded
500
+ end
501
+
502
+
503
+ #
504
+ # :section: Content-coding negotiation callbacks
505
+ #
506
+
507
+ ### Register a callback that will be called during transparent content
508
+ ### negotiation for the entity body if one or more of the specified
509
+ ### +codings+ is among the requested alternatives. The +codings+
510
+ ### are Strings in the form described by RFC2616, section 3.5. The
511
+ ### +callback+ will be called with the coding name, and should
512
+ ### return the new value for the entity body if it has transformed the
513
+ ### bod. If successful, the response's body will be set to the new
514
+ ### value, and the coding name added to the appropriate headers.
515
+ def for_encoding( *codings, &callback )
516
+ codings.each do |coding|
517
+ self.encoding_callbacks[ coding ] = callback
518
+ end
519
+
520
+ # Include the 'Accept-Encoding:' header in the 'Vary:' header
521
+ self.vary_fields.add( 'accept-encoding' )
522
+ end
523
+
524
+
525
+ ### Returns Strelka::HTTPRequest::Encoding objects for accepted encodings that have
526
+ ### a higher qvalue than the one used by the current response.
527
+ def better_encoding
528
+ req = self.request or return []
529
+ return [] unless req.headers.accept_encoding
530
+
531
+ current_qvalue = 0.0
532
+ encodings = req.accepted_encodings.sort
533
+ current_encodings = self.encodings.dup
534
+ current_encodings.unshift( 'identity' )
535
+
536
+ # Find the highest qvalue of the encodings that have been applied already
537
+ current_qvalue = current_encodings.inject( current_qvalue ) do |qval, current_enc|
538
+ qenc = encodings.find {|enc| enc =~ current_enc } or next qval
539
+ qenc.qvalue > qval ? qenc.qvalue : qval
540
+ end
541
+
542
+ self.log.debug "Looking for better encodings than %p (%0.2f)" %
543
+ [ current_encodings, current_qvalue ]
544
+
545
+ return encodings.find_all do |enc|
546
+ self.log.debug " %s (%0.2f) > %0.2f?" % [ enc, enc.qvalue, current_qvalue ]
547
+ enc.qvalue > current_qvalue
548
+ end
549
+ end
550
+
551
+
552
+ ### Iterate over the originating request's acceptable encodings and apply
553
+ ### each one in the order they were requested if they're available.
554
+ def transform_encoding
555
+ return if self.encoding_callbacks.empty?
556
+
557
+ self.log.debug "Looking for acceptable content codings"
558
+ self.better_encoding.each do |enc|
559
+ self.log.debug " looking for a callback for %p" % [ enc ]
560
+
561
+ if (( callback = self.encoding_callbacks[enc.content_coding.to_sym] ))
562
+ self.log.debug " trying callback %p for %p" %
563
+ [ callback, enc ]
564
+ if (( new_body = callback.call(enc.content_coding) ))
565
+ self.log.debug " callback succeeded"
566
+ self.body = new_body
567
+ self.encodings << enc.content_coding
568
+ break
569
+ end
570
+ elsif enc.content_coding == 'identity' && enc.qvalue.nonzero?
571
+ self.log.debug " identity coding, no callback"
572
+ break
573
+ end
574
+ end
575
+ end
576
+
577
+
578
+ end # module Strelka::HTTPResponse::Negotiation
579
+
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mongrel2/httpresponse'
4
+ require 'strelka' unless defined?( Strelka )
5
+
6
+ # An HTTP response class.
7
+ class Strelka::HTTPResponse < Mongrel2::HTTPResponse
8
+ include Strelka::Loggable,
9
+ Strelka::Constants
10
+
11
+
12
+ # Pattern for matching a 'charset' parameter in a media-type string, such as the
13
+ # Content-type header
14
+ CONTENT_TYPE_CHARSET_RE = /;\s*charset=(?<charset>\S+)\s*/i
15
+
16
+
17
+ ### Add some instance variables to new HTTPResponses.
18
+ def initialize( * ) # :notnew:
19
+ @charset = nil
20
+ @languages = []
21
+ @encodings = []
22
+
23
+ super
24
+ end
25
+
26
+
27
+ ######
28
+ public
29
+ ######
30
+
31
+ # Overridden charset of the response's entity body, as either an
32
+ # Encoding or a String. This will be appended to the Content-type
33
+ # header when the response is sent to the client, replacing any charset
34
+ # setting in the Content-type header already. Defaults to nil, which
35
+ # will cause the encoding of the entity body object to be used instead
36
+ # unless there's already one present in the Content-type. In any
37
+ # case, if the encoding is Encoding::ASCII_8BIT, no charset will be
38
+ # appended to the content-type header.
39
+ attr_accessor :charset
40
+
41
+ # An Array of any encodings that have been applied to the response's
42
+ # entity body, in the order they were applied. These will be set as
43
+ # the response's Content-Encoding header when it is sent to the client.
44
+ # Defaults to the empty Array.
45
+ attr_accessor :encodings
46
+
47
+ # The natural language(s) of the response's entity body. These will be
48
+ # set as the response's Content-Language header when it is sent to the
49
+ # client. Defaults to the empty Array.
50
+ attr_accessor :languages
51
+
52
+
53
+ ### Overridden to add charset, encodings, and languages to outgoing
54
+ ### headers if any of them are set.
55
+ def normalized_headers
56
+ headers = super
57
+
58
+ self.add_content_type_charset( headers )
59
+ headers.content_encoding ||= self.encodings.join(', ') unless self.encodings.empty?
60
+ headers.content_language ||= self.languages.join(', ') unless self.languages.empty?
61
+
62
+ return headers
63
+ end
64
+
65
+
66
+ ### Overridden to reset charset, language, and encoding data, too.
67
+ def reset
68
+ super
69
+
70
+ @charset = nil
71
+ @languages.clear
72
+ @encodings.clear
73
+ end
74
+
75
+
76
+
77
+ #########
78
+ protected
79
+ #########
80
+
81
+ ### Add a charset to the content-type header in +headers+ if possible.
82
+ def add_content_type_charset( headers )
83
+ charset = self.find_header_charset
84
+ self.log.debug "Setting the charset in the content-type header to: %p" % [ charset.name ]
85
+
86
+ headers.content_type.slice!( CONTENT_TYPE_CHARSET_RE ) and
87
+ self.log.debug " removed old charset parameter."
88
+ headers.content_type += "; charset=#{charset.name}" unless charset == Encoding::ASCII_8BIT
89
+ end
90
+
91
+
92
+ ### Try to find a character set for the request, using the #charset attribute first,
93
+ ### then the 'charset' parameter from the content-type header, then the Encoding object
94
+ ### associated with the entity body, then the default external encoding (if it's set). If
95
+ ### none of those are found, this method returns ISO-8859-1.
96
+ def find_header_charset
97
+ return ( self.charset ||
98
+ self.content_type_charset ||
99
+ self.entity_body_charset ||
100
+ Encoding.default_external ||
101
+ Encoding::ISO_8859_1 )
102
+ end
103
+
104
+
105
+ ### Return an Encoding object for the 'charset' parameter of the content-type
106
+ ### header, if there is one.
107
+ def content_type_charset
108
+ return nil unless self.content_type
109
+ name = self.content_type[ CONTENT_TYPE_CHARSET_RE, :charset ] or return nil
110
+
111
+ enc = Encoding.find( name )
112
+ self.log.debug "Extracted content-type charset: %p" % [ enc ]
113
+
114
+ return enc
115
+ end
116
+
117
+
118
+ ### Get the body's charset, if possible. Returns +nil+ if the charset
119
+ ### couldn't be determined.
120
+ def entity_body_charset
121
+ self.log.debug "Deriving charset from the entity body..."
122
+
123
+ # Have to use the instance variable instead of #body because plugins can
124
+ # override #body
125
+
126
+ if @body.respond_to?( :encoding )
127
+ self.log.debug " String-ish API. Encoding is: %p" % [ @body.encoding ]
128
+ return @body.encoding
129
+ elsif @body.respond_to?( :external_encoding )
130
+ self.log.debug " IO-ish API. Encoding is: %p" % [ @body.external_encoding ]
131
+ return @body.external_encoding
132
+ end
133
+
134
+ self.log.debug " Body didn't respond to either #encoding or #external_encoding."
135
+ return nil
136
+ end
137
+
138
+ end # class Strelka::HTTPResponse
139
+
140
+