strelka 0.0.1pre4 → 0.0.1.pre129

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.
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
+