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

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

[Source]

     # File lib/sinatra/base.rb, line 475
475:     def etag_matches?(list, new_resource = request.post?)
476:       return !new_resource if list == '*'
477:       list.to_s.split(/\s*,\s*/).include? response['ETag']
478:     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 333
333:     def expires(amount, *values)
334:       values << {} unless values.last.kind_of?(Hash)
335: 
336:       if amount.is_a? Integer
337:         time    = Time.now + amount.to_i
338:         max_age = amount
339:       else
340:         time    = time_for amount
341:         max_age = time - Time.now
342:       end
343: 
344:       values.last.merge!(:max_age => max_age)
345:       cache_control(*values)
346: 
347:       response['Expires'] = time.httpdate
348:     end

whether or not the status is set to 1xx

[Source]

     # File lib/sinatra/base.rb, line 417
417:     def informational?
418:       status.between? 100, 199
419:     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 357
357:     def last_modified(time)
358:       return unless time
359:       time = time_for time
360:       response['Last-Modified'] = time.httpdate
361:       return if env['HTTP_IF_NONE_MATCH']
362: 
363:       if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
364:         # compare based on seconds since epoch
365:         since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
366:         halt 304 if since >= time.to_i
367:       end
368: 
369:       if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
370:         # compare based on seconds since epoch
371:         since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
372:         halt 412 if since < time.to_i
373:       end
374:     rescue ArgumentError
375:     end

whether or not the status is set to 404

[Source]

     # File lib/sinatra/base.rb, line 442
442:     def not_found?
443:       status == 404
444:     end

whether or not the status is set to 3xx

[Source]

     # File lib/sinatra/base.rb, line 427
427:     def redirect?
428:       status.between? 300, 399
429:     end

whether or not the status is set to 5xx

[Source]

     # File lib/sinatra/base.rb, line 437
437:     def server_error?
438:       status.between? 500, 599
439:     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, &block)
291:       scheduler = env['async.callback'] ? EventMachine : Stream
292:       body Stream.new(scheduler, keep_open, &block)
293:     end

whether or not the status is set to 2xx

[Source]

     # File lib/sinatra/base.rb, line 422
422:     def success?
423:       status.between? 200, 299
424:     end

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

[Source]

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

[Validate]