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

Public Class Methods

defer(*) { |end| ... } click to toggle source
# File lib/sinatra/base.rb, line 247
  def self.defer(*)    yield end

  def initialize(scheduler = self.class, keep_open = false, &back)
    @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
    @callbacks, @closed = [], false
  end

  def close
    return if @closed
    @closed = true
    @scheduler.schedule { @callbacks.each { |c| c.call }}
  end

  def each(&front)
    @front = front
    @scheduler.defer do
      begin
        @back.call(self)
      rescue Exception => e
        @scheduler.schedule { raise e }
      end
      close unless @keep_open
    end
  end

  def <<(data)
    @scheduler.schedule { @front.call(data.to_s) }
    self
  end

  def callback(&block)
    @callbacks << block
  end

  alias errback callback
end
new(scheduler = self.class, keep_open = false, &back) click to toggle source
# File lib/sinatra/base.rb, line 249
def initialize(scheduler = self.class, keep_open = false, &back)
  @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
  @callbacks, @closed = [], false
end
schedule(*) { |end| ... } click to toggle source
# File lib/sinatra/base.rb, line 246
    def self.schedule(*) yield end
    def self.defer(*)    yield end

    def initialize(scheduler = self.class, keep_open = false, &back)
      @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
      @callbacks, @closed = [], false
    end

    def close
      return if @closed
      @closed = true
      @scheduler.schedule { @callbacks.each { |c| c.call }}
    end

    def each(&front)
      @front = front
      @scheduler.defer do
        begin
          @back.call(self)
        rescue Exception => e
          @scheduler.schedule { raise e }
        end
        close unless @keep_open
      end
    end

    def <<(data)
      @scheduler.schedule { @front.call(data.to_s) }
      self
    end

    def callback(&block)
      @callbacks << block
    end

    alias errback callback
  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.
  def stream(keep_open = false)
    scheduler = env['async.callback'] ? EventMachine : Stream
    current   = @params.dup
    block     = proc do |out|
      begin
        original, @params = @params, current
        yield(out)
      ensure
        @params = original if original
      end
    end

    body Stream.new(scheduler, keep_open, &block)
  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:
  # http://tools.ietf.org/html/rfc2616#section-14.9.1
  def cache_control(*values)
    if values.last.kind_of?(Hash)
      hash = values.pop
      hash.reject! { |k,v| v == false }
      hash.reject! { |k,v| values << k if v == true }
    else
      hash = {}
    end

    values.map! { |value| value.to_s.tr('_','-') }
    hash.each do |key, value|
      key = key.to_s.tr('_', '-')
      value = value.to_i if key == "max-age"
      values << [key, value].join('=')
    end

    response['Cache-Control'] = values.join(', ') if values.any?
  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
  #
  def expires(amount, *values)
    values << {} unless values.last.kind_of?(Hash)

    if amount.is_a? Integer
      time    = Time.now + amount.to_i
      max_age = amount
    else
      time    = time_for amount
      max_age = time - Time.now
    end

    values.last.merge!(:max_age => max_age)
    cache_control(*values)

    response['Expires'] = time.httpdate
  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.
  def last_modified(time)
    return unless time
    time = time_for time
    response['Last-Modified'] = time.httpdate
    return if env['HTTP_IF_NONE_MATCH']

    if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
      # compare based on seconds since epoch
      since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
      halt 304 if since >= time.to_i
    end

    if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
      # compare based on seconds since epoch
      since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
      halt 412 if since < time.to_i
    end
  rescue ArgumentError
  end

  # 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.
  def etag(value, options = {})
    # Before touching this code, please double check RFC 2616 14.24 and 14.26.
    options      = {:kind => options} unless Hash === options
    kind         = options[:kind] || :strong
    new_resource = options.fetch(:new_resource) { request.post? }

    unless [:strong, :weak].include?(kind)
      raise ArgumentError, ":strong or :weak expected"
    end

    value = '"%s"' % value
    value = 'W/' + value if kind == :weak
    response['ETag'] = value

    if success? or status == 304
      if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
        halt(request.safe? ? 304 : 412)
      end

      if env['HTTP_IF_MATCH']
        halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
      end
    end
  end

  # Sugar for redirect (example:  redirect back)
  def back
    request.referer
  end

  # whether or not the status is set to 1xx
  def informational?
    status.between? 100, 199
  end

  # whether or not the status is set to 2xx
  def success?
    status.between? 200, 299
  end

  # whether or not the status is set to 3xx
  def redirect?
    status.between? 300, 399
  end

  # whether or not the status is set to 4xx
  def client_error?
    status.between? 400, 499
  end

  # whether or not the status is set to 5xx
  def server_error?
    status.between? 500, 599
  end

  # whether or not the status is set to 404
  def not_found?
    status == 404
  end

  # Generates a Time object from the given value.
  # Used by #expires and #last_modified.
  def time_for(value)
    if value.respond_to? :to_time
      value.to_time
    elsif value.is_a? Time
      value
    elsif value.respond_to? :new_offset
      # DateTime#to_time does the same on 1.9
      d = value.new_offset 0
      t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
      t.getlocal
    elsif value.respond_to? :mday
      # Date#to_time does the same on 1.9
      Time.local(value.year, value.mon, value.mday)
    elsif value.is_a? Numeric
      Time.at value
    else
      Time.parse value.to_s
    end
  rescue ArgumentError => boom
    raise boom
  rescue Exception
    raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
  end

  private

  # Helper method checking if a ETag value list includes the current ETag.
  def etag_matches?(list, new_resource = request.post?)
    return !new_resource if list == '*'
    list.to_s.split(%r\s*,\s*/).include? response['ETag']
  end
end

Public Instance Methods

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

Sugar for redirect (example: redirect back)

# File lib/sinatra/base.rb, line 422
def back
  request.referer
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, line 315
def cache_control(*values)
  if values.last.kind_of?(Hash)
    hash = values.pop
    hash.reject! { |k,v| v == false }
    hash.reject! { |k,v| values << k if v == true }
  else
    hash = {}
  end

  values.map! { |value| value.to_s.tr('_','-') }
  hash.each do |key, value|
    key = key.to_s.tr('_', '-')
    value = value.to_i if key == "max-age"
    values << [key, value].join('=')
  end

  response['Cache-Control'] = values.join(', ') if values.any?
end
callback(&block) click to toggle source
# File lib/sinatra/base.rb, line 277
def callback(&block)
  @callbacks << block
end
client_error?() click to toggle source

whether or not the status is set to 4xx

# File lib/sinatra/base.rb, line 442
def client_error?
  status.between? 400, 499
end
close() click to toggle source
# File lib/sinatra/base.rb, line 254
def close
  return if @closed
  @closed = true
  @scheduler.schedule { @callbacks.each { |c| c.call }}
end
each(&front) click to toggle source
# File lib/sinatra/base.rb, line 260
def each(&front)
  @front = front
  @scheduler.defer do
    begin
      @back.call(self)
    rescue Exception => e
      @scheduler.schedule { raise e }
    end
    close unless @keep_open
  end
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, line 396
def etag(value, options = {})
  # Before touching this code, please double check RFC 2616 14.24 and 14.26.
  options      = {:kind => options} unless Hash === options
  kind         = options[:kind] || :strong
  new_resource = options.fetch(:new_resource) { request.post? }

  unless [:strong, :weak].include?(kind)
    raise ArgumentError, ":strong or :weak expected"
  end

  value = '"%s"' % value
  value = 'W/' + value if kind == :weak
  response['ETag'] = value

  if success? or status == 304
    if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
      halt(request.safe? ? 304 : 412)
    end

    if env['HTTP_IF_MATCH']
      halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
    end
  end
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, line 485
def etag_matches?(list, new_resource = request.post?)
  return !new_resource if list == '*'
  list.to_s.split(%r\s*,\s*/).include? response['ETag']
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, line 343
def expires(amount, *values)
  values << {} unless values.last.kind_of?(Hash)

  if amount.is_a? Integer
    time    = Time.now + amount.to_i
    max_age = amount
  else
    time    = time_for amount
    max_age = time - Time.now
  end

  values.last.merge!(:max_age => max_age)
  cache_control(*values)

  response['Expires'] = time.httpdate
end
informational?() click to toggle source

whether or not the status is set to 1xx

# File lib/sinatra/base.rb, line 427
def informational?
  status.between? 100, 199
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, line 367
def last_modified(time)
  return unless time
  time = time_for time
  response['Last-Modified'] = time.httpdate
  return if env['HTTP_IF_NONE_MATCH']

  if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
    # compare based on seconds since epoch
    since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
    halt 304 if since >= time.to_i
  end

  if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
    # compare based on seconds since epoch
    since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
    halt 412 if since < time.to_i
  end
rescue ArgumentError
end
not_found?() click to toggle source

whether or not the status is set to 404

# File lib/sinatra/base.rb, line 452
def not_found?
  status == 404
end
redirect?() click to toggle source

whether or not the status is set to 3xx

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

whether or not the status is set to 5xx

# File lib/sinatra/base.rb, line 447
def server_error?
  status.between? 500, 599
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 #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, line 290
def stream(keep_open = false)
  scheduler = env['async.callback'] ? EventMachine : Stream
  current   = @params.dup
  block     = proc do |out|
    begin
      original, @params = @params, current
      yield(out)
    ensure
      @params = original if original
    end
  end

  body Stream.new(scheduler, keep_open, &block)
end
success?() click to toggle source

whether or not the status is set to 2xx

# File lib/sinatra/base.rb, line 432
def success?
  status.between? 200, 299
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, line 458
def time_for(value)
  if value.respond_to? :to_time
    value.to_time
  elsif value.is_a? Time
    value
  elsif value.respond_to? :new_offset
    # DateTime#to_time does the same on 1.9
    d = value.new_offset 0
    t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
    t.getlocal
  elsif value.respond_to? :mday
    # Date#to_time does the same on 1.9
    Time.local(value.year, value.mon, value.mday)
  elsif value.is_a? Numeric
    Time.at value
  else
    Time.parse value.to_s
  end
rescue ArgumentError => boom
  raise boom
rescue Exception
  raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
end