class Sinatra::Helpers::Stream

Class of the response body in case you use stream.

Three things really matter: The front and back block (back being the block 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.

Constants

ETAG_KINDS

Public Class Methods

defer(*) { |end| ... } click to toggle source
    # File lib/sinatra/base.rb
385   def self.defer(*)    yield end
386 
387   def initialize(scheduler = self.class, keep_open = false, &back)
388     @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
389     @callbacks, @closed = [], false
390   end
391 
392   def close
393     return if @closed
394     @closed = true
395     @scheduler.schedule { @callbacks.each { |c| c.call }}
396   end
397 
398   def each(&front)
399     @front = front
400     @scheduler.defer do
401       begin
402         @back.call(self)
403       rescue Exception => e
404         @scheduler.schedule { raise e }
405       end
406       close unless @keep_open
407     end
408   end
409 
410   def <<(data)
411     @scheduler.schedule { @front.call(data.to_s) }
412     self
413   end
414 
415   def callback(&block)
416     return yield if @closed
417     @callbacks << block
418   end
419 
420   alias errback callback
421 
422   def closed?
423     @closed
424   end
425 end
new(scheduler = self.class, keep_open = false, &back) click to toggle source
    # File lib/sinatra/base.rb
387 def initialize(scheduler = self.class, keep_open = false, &back)
388   @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
389   @callbacks, @closed = [], false
390 end
schedule(*) { |end| ... } click to toggle source
    # File lib/sinatra/base.rb
384     def self.schedule(*) yield end
385     def self.defer(*)    yield end
386 
387     def initialize(scheduler = self.class, keep_open = false, &back)
388       @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
389       @callbacks, @closed = [], false
390     end
391 
392     def close
393       return if @closed
394       @closed = true
395       @scheduler.schedule { @callbacks.each { |c| c.call }}
396     end
397 
398     def each(&front)
399       @front = front
400       @scheduler.defer do
401         begin
402           @back.call(self)
403         rescue Exception => e
404           @scheduler.schedule { raise e }
405         end
406         close unless @keep_open
407       end
408     end
409 
410     def <<(data)
411       @scheduler.schedule { @front.call(data.to_s) }
412       self
413     end
414 
415     def callback(&block)
416       return yield if @closed
417       @callbacks << block
418     end
419 
420     alias errback callback
421 
422     def closed?
423       @closed
424     end
425   end
426 
427   # Allows to start sending data to the client even though later parts of
428   # the response body have not yet been generated.
429   #
430   # The close parameter specifies whether Stream#close should be called
431   # after the block has been executed. This is only relevant for evented
432   # servers like Thin or Rainbows.
433   def stream(keep_open = false)
434     scheduler = env['async.callback'] ? EventMachine : Stream
435     current   = @params.dup
436     body Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } }
437   end
438 
439   # Specify response freshness policy for HTTP caches (Cache-Control header).
440   # Any number of non-value directives (:public, :private, :no_cache,
441   # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
442   # a Hash of value directives (:max_age, :min_stale, :s_max_age).
443   #
444   #   cache_control :public, :must_revalidate, :max_age => 60
445   #   => Cache-Control: public, must-revalidate, max-age=60
446   #
447   # See RFC 2616 / 14.9 for more on standard cache control directives:
448   # http://tools.ietf.org/html/rfc2616#section-14.9.1
449   def cache_control(*values)
450     if values.last.kind_of?(Hash)
451       hash = values.pop
452       hash.reject! { |k,v| v == false }
453       hash.reject! { |k,v| values << k if v == true }
454     else
455       hash = {}
456     end
457 
458     values.map! { |value| value.to_s.tr('_','-') }
459     hash.each do |key, value|
460       key = key.to_s.tr('_', '-')
461       value = value.to_i if key == "max-age"
462       values << "#{key}=#{value}"
463     end
464 
465     response['Cache-Control'] = values.join(', ') if values.any?
466   end
467 
468   # Set the Expires header and Cache-Control/max-age directive. Amount
469   # can be an integer number of seconds in the future or a Time object
470   # indicating when the response should be considered "stale". The remaining
471   # "values" arguments are passed to the #cache_control helper:
472   #
473   #   expires 500, :public, :must_revalidate
474   #   => Cache-Control: public, must-revalidate, max-age=60
475   #   => Expires: Mon, 08 Jun 2009 08:50:17 GMT
476   #
477   def expires(amount, *values)
478     values << {} unless values.last.kind_of?(Hash)
479 
480     if amount.is_a? Integer
481       time    = Time.now + amount.to_i
482       max_age = amount
483     else
484       time    = time_for amount
485       max_age = time - Time.now
486     end
487 
488     values.last.merge!(:max_age => max_age)
489     cache_control(*values)
490 
491     response['Expires'] = time.httpdate
492   end
493 
494   # Set the last modified time of the resource (HTTP 'Last-Modified' header)
495   # and halt if conditional GET matches. The +time+ argument is a Time,
496   # DateTime, or other object that responds to +to_time+.
497   #
498   # When the current request includes an 'If-Modified-Since' header that is
499   # equal or later than the time specified, execution is immediately halted
500   # with a '304 Not Modified' response.
501   def last_modified(time)
502     return unless time
503     time = time_for time
504     response['Last-Modified'] = time.httpdate
505     return if env['HTTP_IF_NONE_MATCH']
506 
507     if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
508       # compare based on seconds since epoch
509       since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
510       halt 304 if since >= time.to_i
511     end
512 
513     if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
514       # compare based on seconds since epoch
515       since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
516       halt 412 if since < time.to_i
517     end
518   rescue ArgumentError
519   end
520 
521   ETAG_KINDS = [:strong, :weak]
522   # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
523   # GET matches. The +value+ argument is an identifier that uniquely
524   # identifies the current version of the resource. The +kind+ argument
525   # indicates whether the etag should be used as a :strong (default) or :weak
526   # cache validator.
527   #
528   # When the current request includes an 'If-None-Match' header with a
529   # matching etag, execution is immediately halted. If the request method is
530   # GET or HEAD, a '304 Not Modified' response is sent.
531   def etag(value, options = {})
532     # Before touching this code, please double check RFC 2616 14.24 and 14.26.
533     options      = {:kind => options} unless Hash === options
534     kind         = options[:kind] || :strong
535     new_resource = options.fetch(:new_resource) { request.post? }
536 
537     unless ETAG_KINDS.include?(kind)
538       raise ArgumentError, ":strong or :weak expected"
539     end
540 
541     value = '"%s"' % value
542     value = "W/#{value}" if kind == :weak
543     response['ETag'] = value
544 
545     if success? or status == 304
546       if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
547         halt(request.safe? ? 304 : 412)
548       end
549 
550       if env['HTTP_IF_MATCH']
551         halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
552       end
553     end
554   end
555 
556   # Sugar for redirect (example:  redirect back)
557   def back
558     request.referer
559   end
560 
561   # whether or not the status is set to 1xx
562   def informational?
563     status.between? 100, 199
564   end
565 
566   # whether or not the status is set to 2xx
567   def success?
568     status.between? 200, 299
569   end
570 
571   # whether or not the status is set to 3xx
572   def redirect?
573     status.between? 300, 399
574   end
575 
576   # whether or not the status is set to 4xx
577   def client_error?
578     status.between? 400, 499
579   end
580 
581   # whether or not the status is set to 5xx
582   def server_error?
583     status.between? 500, 599
584   end
585 
586   # whether or not the status is set to 404
587   def not_found?
588     status == 404
589   end
590 
591   # Generates a Time object from the given value.
592   # Used by #expires and #last_modified.
593   def time_for(value)
594     if value.respond_to? :to_time
595       value.to_time
596     elsif value.is_a? Time
597       value
598     elsif value.respond_to? :new_offset
599       # DateTime#to_time does the same on 1.9
600       d = value.new_offset 0
601       t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
602       t.getlocal
603     elsif value.respond_to? :mday
604       # Date#to_time does the same on 1.9
605       Time.local(value.year, value.mon, value.mday)
606     elsif value.is_a? Numeric
607       Time.at value
608     else
609       Time.parse value.to_s
610     end
611   rescue ArgumentError => boom
612     raise boom
613   rescue Exception
614     raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
615   end
616 
617   private
618 
619   # Helper method checking if a ETag value list includes the current ETag.
620   def etag_matches?(list, new_resource = request.post?)
621     return !new_resource if list == '*'
622     list.to_s.split(/\s*,\s*/).include? response['ETag']
623   end
624 
625   def with_params(temp_params)
626     original, @params = @params, temp_params
627     yield
628   ensure
629     @params = original if original
630   end
631 end

Private Class Methods

helpers(*extensions, &block) click to toggle source

Include the helper modules provided in Sinatra’s request context.

     # File lib/sinatra/base.rb
2036 def self.helpers(*extensions, &block)
2037   Delegator.target.helpers(*extensions, &block)
2038 end
new(base = Base, &block) click to toggle source

Create a new Sinatra application. The block is evaluated in the new app’s class scope.

     # File lib/sinatra/base.rb
2024 def self.new(base = Base, &block)
2025   base = Class.new(base)
2026   base.class_eval(&block) if block_given?
2027   base
2028 end
register(*extensions, &block) click to toggle source

Extend the top-level DSL with the modules provided.

     # File lib/sinatra/base.rb
2031 def self.register(*extensions, &block)
2032   Delegator.target.register(*extensions, &block)
2033 end
use(*args, &block) click to toggle source

Use the middleware for classic applications.

     # File lib/sinatra/base.rb
2041 def self.use(*args, &block)
2042   Delegator.target.use(*args, &block)
2043 end

Public Instance Methods

<<(data) click to toggle source
    # File lib/sinatra/base.rb
410 def <<(data)
411   @scheduler.schedule { @front.call(data.to_s) }
412   self
413 end
back() click to toggle source

Sugar for redirect (example: redirect back)

    # File lib/sinatra/base.rb
557 def back
558   request.referer
559 end
cache_control(*values) click to toggle source

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

    # File lib/sinatra/base.rb
449 def cache_control(*values)
450   if values.last.kind_of?(Hash)
451     hash = values.pop
452     hash.reject! { |k,v| v == false }
453     hash.reject! { |k,v| values << k if v == true }
454   else
455     hash = {}
456   end
457 
458   values.map! { |value| value.to_s.tr('_','-') }
459   hash.each do |key, value|
460     key = key.to_s.tr('_', '-')
461     value = value.to_i if key == "max-age"
462     values << "#{key}=#{value}"
463   end
464 
465   response['Cache-Control'] = values.join(', ') if values.any?
466 end
callback() { || ... } click to toggle source
    # File lib/sinatra/base.rb
415 def callback(&block)
416   return yield if @closed
417   @callbacks << block
418 end
client_error?() click to toggle source

whether or not the status is set to 4xx

    # File lib/sinatra/base.rb
577 def client_error?
578   status.between? 400, 499
579 end
close() click to toggle source
    # File lib/sinatra/base.rb
392 def close
393   return if @closed
394   @closed = true
395   @scheduler.schedule { @callbacks.each { |c| c.call }}
396 end
closed?() click to toggle source
    # File lib/sinatra/base.rb
422 def closed?
423   @closed
424 end
each(&front) click to toggle source
    # File lib/sinatra/base.rb
398 def each(&front)
399   @front = front
400   @scheduler.defer do
401     begin
402       @back.call(self)
403     rescue Exception => e
404       @scheduler.schedule { raise e }
405     end
406     close unless @keep_open
407   end
408 end
etag(value, options = {}) click to toggle source

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.

    # File lib/sinatra/base.rb
531 def etag(value, options = {})
532   # Before touching this code, please double check RFC 2616 14.24 and 14.26.
533   options      = {:kind => options} unless Hash === options
534   kind         = options[:kind] || :strong
535   new_resource = options.fetch(:new_resource) { request.post? }
536 
537   unless ETAG_KINDS.include?(kind)
538     raise ArgumentError, ":strong or :weak expected"
539   end
540 
541   value = '"%s"' % value
542   value = "W/#{value}" if kind == :weak
543   response['ETag'] = value
544 
545   if success? or status == 304
546     if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
547       halt(request.safe? ? 304 : 412)
548     end
549 
550     if env['HTTP_IF_MATCH']
551       halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
552     end
553   end
554 end
etag_matches?(list, new_resource = request.post?) click to toggle source

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

    # File lib/sinatra/base.rb
620 def etag_matches?(list, new_resource = request.post?)
621   return !new_resource if list == '*'
622   list.to_s.split(/\s*,\s*/).include? response['ETag']
623 end
expires(amount, *values) click to toggle source

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
    # File lib/sinatra/base.rb
477 def expires(amount, *values)
478   values << {} unless values.last.kind_of?(Hash)
479 
480   if amount.is_a? Integer
481     time    = Time.now + amount.to_i
482     max_age = amount
483   else
484     time    = time_for amount
485     max_age = time - Time.now
486   end
487 
488   values.last.merge!(:max_age => max_age)
489   cache_control(*values)
490 
491   response['Expires'] = time.httpdate
492 end
informational?() click to toggle source

whether or not the status is set to 1xx

    # File lib/sinatra/base.rb
562 def informational?
563   status.between? 100, 199
564 end
last_modified(time) click to toggle source

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.

    # File lib/sinatra/base.rb
501 def last_modified(time)
502   return unless time
503   time = time_for time
504   response['Last-Modified'] = time.httpdate
505   return if env['HTTP_IF_NONE_MATCH']
506 
507   if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
508     # compare based on seconds since epoch
509     since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
510     halt 304 if since >= time.to_i
511   end
512 
513   if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
514     # compare based on seconds since epoch
515     since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
516     halt 412 if since < time.to_i
517   end
518 rescue ArgumentError
519 end
not_found?() click to toggle source

whether or not the status is set to 404

    # File lib/sinatra/base.rb
587 def not_found?
588   status == 404
589 end
redirect?() click to toggle source

whether or not the status is set to 3xx

    # File lib/sinatra/base.rb
572 def redirect?
573   status.between? 300, 399
574 end
server_error?() click to toggle source

whether or not the status is set to 5xx

    # File lib/sinatra/base.rb
582 def server_error?
583   status.between? 500, 599
584 end
stream(keep_open = false) { |out| ... } click to toggle source

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.

    # File lib/sinatra/base.rb
433 def stream(keep_open = false)
434   scheduler = env['async.callback'] ? EventMachine : Stream
435   current   = @params.dup
436   body Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } }
437 end
success?() click to toggle source

whether or not the status is set to 2xx

    # File lib/sinatra/base.rb
567 def success?
568   status.between? 200, 299
569 end
time_for(value) click to toggle source

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

    # File lib/sinatra/base.rb
593 def time_for(value)
594   if value.respond_to? :to_time
595     value.to_time
596   elsif value.is_a? Time
597     value
598   elsif value.respond_to? :new_offset
599     # DateTime#to_time does the same on 1.9
600     d = value.new_offset 0
601     t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
602     t.getlocal
603   elsif value.respond_to? :mday
604     # Date#to_time does the same on 1.9
605     Time.local(value.year, value.mon, value.mday)
606   elsif value.is_a? Numeric
607     Time.at value
608   else
609     Time.parse value.to_s
610   end
611 rescue ArgumentError => boom
612   raise boom
613 rescue Exception
614   raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
615 end
with_params(temp_params) { || ... } click to toggle source
    # File lib/sinatra/base.rb
625 def with_params(temp_params)
626   original, @params = @params, temp_params
627   yield
628 ensure
629   @params = original if original
630 end