Class Sinatra::Helpers::Stream
In: lib/sinatra/base.rb
Parent: Object

Class of the response body in case you use stream.

Three things really matter: The front and back block (back being the blog generating content, front the one sending it to the client) and the scheduler, integrating with whatever concurrency feature the Rack handler is using.

Scheduler has to respond to defer and schedule.

Methods

Classes and Modules

Module Sinatra::Helpers::Stream::Templates
Class Sinatra::Helpers::Stream::Application
Class Sinatra::Helpers::Stream::Base

Public Class methods

[Source]

     # File lib/sinatra/base.rb, line 247
247:       def self.defer(*)    yield end
248: 
249:       def initialize(scheduler = self.class, keep_open = false, &back)
250:         @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
251:         @callbacks, @closed = [], false
252:       end
253: 
254:       def close
255:         return if @closed
256:         @closed = true
257:         @scheduler.schedule { @callbacks.each { |c| c.call }}
258:       end
259: 
260:       def each(&front)
261:         @front = front
262:         @scheduler.defer do
263:           begin
264:             @back.call(self)
265:           rescue Exception => e
266:             @scheduler.schedule { raise e }
267:           end
268:           close unless @keep_open
269:         end
270:       end
271: 
272:       def <<(data)
273:         @scheduler.schedule { @front.call(data.to_s) }
274:         self
275:       end
276: 
277:       def callback(&block)
278:         @callbacks << block
279:       end
280: 
281:       alias errback callback
282:     end

[Source]

     # File lib/sinatra/base.rb, line 249
249:       def initialize(scheduler = self.class, keep_open = false, &back)
250:         @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
251:         @callbacks, @closed = [], false
252:       end

[Source]

     # File lib/sinatra/base.rb, line 246
246:       def self.schedule(*) yield end
247:       def self.defer(*)    yield end
248: 
249:       def initialize(scheduler = self.class, keep_open = false, &back)
250:         @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
251:         @callbacks, @closed = [], false
252:       end
253: 
254:       def close
255:         return if @closed
256:         @closed = true
257:         @scheduler.schedule { @callbacks.each { |c| c.call }}
258:       end
259: 
260:       def each(&front)
261:         @front = front
262:         @scheduler.defer do
263:           begin
264:             @back.call(self)
265:           rescue Exception => e
266:             @scheduler.schedule { raise e }
267:           end
268:           close unless @keep_open
269:         end
270:       end
271: 
272:       def <<(data)
273:         @scheduler.schedule { @front.call(data.to_s) }
274:         self
275:       end
276: 
277:       def callback(&block)
278:         @callbacks << block
279:       end
280: 
281:       alias errback callback
282:     end
283: 
284:     # Allows to start sending data to the client even though later parts of
285:     # the response body have not yet been generated.
286:     #
287:     # The close parameter specifies whether Stream#close should be called
288:     # after the block has been executed. This is only relevant for evented
289:     # servers like Thin or Rainbows.
290:     def stream(keep_open = false)
291:       scheduler = env['async.callback'] ? EventMachine : Stream
292:       current   = @params.dup
293:       block     = proc do |out|
294:         begin
295:           original, @params = @params, current
296:           yield(out)
297:         ensure
298:           @params = original if original
299:         end
300:       end
301: 
302:       body Stream.new(scheduler, keep_open, &block)
303:     end
304: 
305:     # Specify response freshness policy for HTTP caches (Cache-Control header).
306:     # Any number of non-value directives (:public, :private, :no_cache,
307:     # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
308:     # a Hash of value directives (:max_age, :min_stale, :s_max_age).
309:     #
310:     #   cache_control :public, :must_revalidate, :max_age => 60
311:     #   => Cache-Control: public, must-revalidate, max-age=60
312:     #
313:     # See RFC 2616 / 14.9 for more on standard cache control directives:
314:     # http://tools.ietf.org/html/rfc2616#section-14.9.1
315:     def cache_control(*values)
316:       if values.last.kind_of?(Hash)
317:         hash = values.pop
318:         hash.reject! { |k,v| v == false }
319:         hash.reject! { |k,v| values << k if v == true }
320:       else
321:         hash = {}
322:       end
323: 
324:       values.map! { |value| value.to_s.tr('_','-') }
325:       hash.each do |key, value|
326:         key = key.to_s.tr('_', '-')
327:         value = value.to_i if key == "max-age"
328:         values << [key, value].join('=')
329:       end
330: 
331:       response['Cache-Control'] = values.join(', ') if values.any?
332:     end
333: 
334:     # Set the Expires header and Cache-Control/max-age directive. Amount
335:     # can be an integer number of seconds in the future or a Time object
336:     # indicating when the response should be considered "stale". The remaining
337:     # "values" arguments are passed to the #cache_control helper:
338:     #
339:     #   expires 500, :public, :must_revalidate
340:     #   => Cache-Control: public, must-revalidate, max-age=60
341:     #   => Expires: Mon, 08 Jun 2009 08:50:17 GMT
342:     #
343:     def expires(amount, *values)
344:       values << {} unless values.last.kind_of?(Hash)
345: 
346:       if amount.is_a? Integer
347:         time    = Time.now + amount.to_i
348:         max_age = amount
349:       else
350:         time    = time_for amount
351:         max_age = time - Time.now
352:       end
353: 
354:       values.last.merge!(:max_age => max_age)
355:       cache_control(*values)
356: 
357:       response['Expires'] = time.httpdate
358:     end
359: 
360:     # Set the last modified time of the resource (HTTP 'Last-Modified' header)
361:     # and halt if conditional GET matches. The +time+ argument is a Time,
362:     # DateTime, or other object that responds to +to_time+.
363:     #
364:     # When the current request includes an 'If-Modified-Since' header that is
365:     # equal or later than the time specified, execution is immediately halted
366:     # with a '304 Not Modified' response.
367:     def last_modified(time)
368:       return unless time
369:       time = time_for time
370:       response['Last-Modified'] = time.httpdate
371:       return if env['HTTP_IF_NONE_MATCH']
372: 
373:       if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
374:         # compare based on seconds since epoch
375:         since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
376:         halt 304 if since >= time.to_i
377:       end
378: 
379:       if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
380:         # compare based on seconds since epoch
381:         since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
382:         halt 412 if since < time.to_i
383:       end
384:     rescue ArgumentError
385:     end
386: 
387:     # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
388:     # GET matches. The +value+ argument is an identifier that uniquely
389:     # identifies the current version of the resource. The +kind+ argument
390:     # indicates whether the etag should be used as a :strong (default) or :weak
391:     # cache validator.
392:     #
393:     # When the current request includes an 'If-None-Match' header with a
394:     # matching etag, execution is immediately halted. If the request method is
395:     # GET or HEAD, a '304 Not Modified' response is sent.
396:     def etag(value, options = {})
397:       # Before touching this code, please double check RFC 2616 14.24 and 14.26.
398:       options      = {:kind => options} unless Hash === options
399:       kind         = options[:kind] || :strong
400:       new_resource = options.fetch(:new_resource) { request.post? }
401: 
402:       unless [:strong, :weak].include?(kind)
403:         raise ArgumentError, ":strong or :weak expected"
404:       end
405: 
406:       value = '"%s"' % value
407:       value = 'W/' + value if kind == :weak
408:       response['ETag'] = value
409: 
410:       if success? or status == 304
411:         if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
412:           halt(request.safe? ? 304 : 412)
413:         end
414: 
415:         if env['HTTP_IF_MATCH']
416:           halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
417:         end
418:       end
419:     end
420: 
421:     # Sugar for redirect (example:  redirect back)
422:     def back
423:       request.referer
424:     end
425: 
426:     # whether or not the status is set to 1xx
427:     def informational?
428:       status.between? 100, 199
429:     end
430: 
431:     # whether or not the status is set to 2xx
432:     def success?
433:       status.between? 200, 299
434:     end
435: 
436:     # whether or not the status is set to 3xx
437:     def redirect?
438:       status.between? 300, 399
439:     end
440: 
441:     # whether or not the status is set to 4xx
442:     def client_error?
443:       status.between? 400, 499
444:     end
445: 
446:     # whether or not the status is set to 5xx
447:     def server_error?
448:       status.between? 500, 599
449:     end
450: 
451:     # whether or not the status is set to 404
452:     def not_found?
453:       status == 404
454:     end
455: 
456:     # Generates a Time object from the given value.
457:     # Used by #expires and #last_modified.
458:     def time_for(value)
459:       if value.respond_to? :to_time
460:         value.to_time
461:       elsif value.is_a? Time
462:         value
463:       elsif value.respond_to? :new_offset
464:         # DateTime#to_time does the same on 1.9
465:         d = value.new_offset 0
466:         t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
467:         t.getlocal
468:       elsif value.respond_to? :mday
469:         # Date#to_time does the same on 1.9
470:         Time.local(value.year, value.mon, value.mday)
471:       elsif value.is_a? Numeric
472:         Time.at value
473:       else
474:         Time.parse value.to_s
475:       end
476:     rescue ArgumentError => boom
477:       raise boom
478:     rescue Exception
479:       raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
480:     end
481: 
482:     private
483: 
484:     # Helper method checking if a ETag value list includes the current ETag.
485:     def etag_matches?(list, new_resource = request.post?)
486:       return !new_resource if list == '*'
487:       list.to_s.split(/\s*,\s*/).include? response['ETag']
488:     end
489:   end

Public Instance methods

[Source]

     # File lib/sinatra/base.rb, line 272
272:       def <<(data)
273:         @scheduler.schedule { @front.call(data.to_s) }
274:         self
275:       end

Sugar for redirect (example: redirect back)

[Source]

     # File lib/sinatra/base.rb, line 422
422:     def back
423:       request.referer
424:     end

Specify response freshness policy for HTTP caches (Cache-Control header). Any number of non-value directives (:public, :private, :no_cache, :no_store, :must_revalidate, :proxy_revalidate) may be passed along with a Hash of value directives (:max_age, :min_stale, :s_max_age).

  cache_control :public, :must_revalidate, :max_age => 60
  => Cache-Control: public, must-revalidate, max-age=60

See RFC 2616 / 14.9 for more on standard cache control directives: tools.ietf.org/html/rfc2616#section-14.9.1

[Source]

     # File lib/sinatra/base.rb, line 315
315:     def cache_control(*values)
316:       if values.last.kind_of?(Hash)
317:         hash = values.pop
318:         hash.reject! { |k,v| v == false }
319:         hash.reject! { |k,v| values << k if v == true }
320:       else
321:         hash = {}
322:       end
323: 
324:       values.map! { |value| value.to_s.tr('_','-') }
325:       hash.each do |key, value|
326:         key = key.to_s.tr('_', '-')
327:         value = value.to_i if key == "max-age"
328:         values << [key, value].join('=')
329:       end
330: 
331:       response['Cache-Control'] = values.join(', ') if values.any?
332:     end

[Source]

     # File lib/sinatra/base.rb, line 277
277:       def callback(&block)
278:         @callbacks << block
279:       end

whether or not the status is set to 4xx

[Source]

     # File lib/sinatra/base.rb, line 442
442:     def client_error?
443:       status.between? 400, 499
444:     end

[Source]

     # File lib/sinatra/base.rb, line 254
254:       def close
255:         return if @closed
256:         @closed = true
257:         @scheduler.schedule { @callbacks.each { |c| c.call }}
258:       end

[Source]

     # File lib/sinatra/base.rb, line 260
260:       def each(&front)
261:         @front = front
262:         @scheduler.defer do
263:           begin
264:             @back.call(self)
265:           rescue Exception => e
266:             @scheduler.schedule { raise e }
267:           end
268:           close unless @keep_open
269:         end
270:       end
errback(&block)

Alias for callback

Set the response entity tag (HTTP ‘ETag’ header) and halt if conditional GET matches. The value argument is an identifier that uniquely identifies the current version of the resource. The kind argument indicates whether the etag should be used as a :strong (default) or :weak cache validator.

When the current request includes an ‘If-None-Match’ header with a matching etag, execution is immediately halted. If the request method is GET or HEAD, a ‘304 Not Modified’ response is sent.

[Source]

     # File lib/sinatra/base.rb, line 396
396:     def etag(value, options = {})
397:       # Before touching this code, please double check RFC 2616 14.24 and 14.26.
398:       options      = {:kind => options} unless Hash === options
399:       kind         = options[:kind] || :strong
400:       new_resource = options.fetch(:new_resource) { request.post? }
401: 
402:       unless [:strong, :weak].include?(kind)
403:         raise ArgumentError, ":strong or :weak expected"
404:       end
405: 
406:       value = '"%s"' % value
407:       value = 'W/' + value if kind == :weak
408:       response['ETag'] = value
409: 
410:       if success? or status == 304
411:         if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
412:           halt(request.safe? ? 304 : 412)
413:         end
414: 
415:         if env['HTTP_IF_MATCH']
416:           halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
417:         end
418:       end
419:     end

Helper method checking if a ETag value list includes the current ETag.

[Source]

     # File lib/sinatra/base.rb, line 485
485:     def etag_matches?(list, new_resource = request.post?)
486:       return !new_resource if list == '*'
487:       list.to_s.split(/\s*,\s*/).include? response['ETag']
488:     end

Set the Expires header and Cache-Control/max-age directive. Amount can be an integer number of seconds in the future or a Time object indicating when the response should be considered "stale". The remaining "values" arguments are passed to the cache_control helper:

  expires 500, :public, :must_revalidate
  => Cache-Control: public, must-revalidate, max-age=60
  => Expires: Mon, 08 Jun 2009 08:50:17 GMT

[Source]

     # File lib/sinatra/base.rb, line 343
343:     def expires(amount, *values)
344:       values << {} unless values.last.kind_of?(Hash)
345: 
346:       if amount.is_a? Integer
347:         time    = Time.now + amount.to_i
348:         max_age = amount
349:       else
350:         time    = time_for amount
351:         max_age = time - Time.now
352:       end
353: 
354:       values.last.merge!(:max_age => max_age)
355:       cache_control(*values)
356: 
357:       response['Expires'] = time.httpdate
358:     end

whether or not the status is set to 1xx

[Source]

     # File lib/sinatra/base.rb, line 427
427:     def informational?
428:       status.between? 100, 199
429:     end

Set the last modified time of the resource (HTTP ‘Last-Modified’ header) and halt if conditional GET matches. The time argument is a Time, DateTime, or other object that responds to to_time.

When the current request includes an ‘If-Modified-Since’ header that is equal or later than the time specified, execution is immediately halted with a ‘304 Not Modified’ response.

[Source]

     # File lib/sinatra/base.rb, line 367
367:     def last_modified(time)
368:       return unless time
369:       time = time_for time
370:       response['Last-Modified'] = time.httpdate
371:       return if env['HTTP_IF_NONE_MATCH']
372: 
373:       if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
374:         # compare based on seconds since epoch
375:         since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
376:         halt 304 if since >= time.to_i
377:       end
378: 
379:       if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
380:         # compare based on seconds since epoch
381:         since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
382:         halt 412 if since < time.to_i
383:       end
384:     rescue ArgumentError
385:     end

whether or not the status is set to 404

[Source]

     # File lib/sinatra/base.rb, line 452
452:     def not_found?
453:       status == 404
454:     end

whether or not the status is set to 3xx

[Source]

     # File lib/sinatra/base.rb, line 437
437:     def redirect?
438:       status.between? 300, 399
439:     end

whether or not the status is set to 5xx

[Source]

     # File lib/sinatra/base.rb, line 447
447:     def server_error?
448:       status.between? 500, 599
449:     end

Allows to start sending data to the client even though later parts of the response body have not yet been generated.

The close parameter specifies whether Stream#close should be called after the block has been executed. This is only relevant for evented servers like Thin or Rainbows.

[Source]

     # File lib/sinatra/base.rb, line 290
290:     def stream(keep_open = false)
291:       scheduler = env['async.callback'] ? EventMachine : Stream
292:       current   = @params.dup
293:       block     = proc do |out|
294:         begin
295:           original, @params = @params, current
296:           yield(out)
297:         ensure
298:           @params = original if original
299:         end
300:       end
301: 
302:       body Stream.new(scheduler, keep_open, &block)
303:     end

whether or not the status is set to 2xx

[Source]

     # File lib/sinatra/base.rb, line 432
432:     def success?
433:       status.between? 200, 299
434:     end

Generates a Time object from the given value. Used by expires and last_modified.

[Source]

     # File lib/sinatra/base.rb, line 458
458:     def time_for(value)
459:       if value.respond_to? :to_time
460:         value.to_time
461:       elsif value.is_a? Time
462:         value
463:       elsif value.respond_to? :new_offset
464:         # DateTime#to_time does the same on 1.9
465:         d = value.new_offset 0
466:         t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
467:         t.getlocal
468:       elsif value.respond_to? :mday
469:         # Date#to_time does the same on 1.9
470:         Time.local(value.year, value.mon, value.mday)
471:       elsif value.is_a? Numeric
472:         Time.at value
473:       else
474:         Time.parse value.to_s
475:       end
476:     rescue ArgumentError => boom
477:       raise boom
478:     rescue Exception
479:       raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
480:     end

[Validate]