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
453   def self.defer(*)    yield end
454 
455   def initialize(scheduler = self.class, keep_open = false, &back)
456     @back = back.to_proc
457     @scheduler = scheduler
458     @keep_open = keep_open
459     @callbacks = []
460     @closed = false
461   end
462 
463   def close
464     return if closed?
465 
466     @closed = true
467     @scheduler.schedule { @callbacks.each { |c| c.call } }
468   end
469 
470   def each(&front)
471     @front = front
472     @scheduler.defer do
473       begin
474         @back.call(self)
475       rescue Exception => e
476         @scheduler.schedule { raise e }
477       end
478       close unless @keep_open
479     end
480   end
481 
482   def <<(data)
483     @scheduler.schedule { @front.call(data.to_s) }
484     self
485   end
486 
487   def callback(&block)
488     return yield if closed?
489 
490     @callbacks << block
491   end
492 
493   alias errback callback
494 
495   def closed?
496     @closed
497   end
498 end
helpers(*extensions, &block) click to toggle source

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

     # File lib/sinatra/base.rb
2028 def self.helpers(*extensions, &block)
2029   Delegator.target.helpers(*extensions, &block)
2030 end
new(scheduler = self.class, keep_open = false, &back) click to toggle source
    # File lib/sinatra/base.rb
455 def initialize(scheduler = self.class, keep_open = false, &back)
456   @back = back.to_proc
457   @scheduler = scheduler
458   @keep_open = keep_open
459   @callbacks = []
460   @closed = false
461 end
new(base = Base, &block) click to toggle source

Create a new Sinatra application; the block is evaluated in the class scope.

     # File lib/sinatra/base.rb
2016 def self.new(base = Base, &block)
2017   base = Class.new(base)
2018   base.class_eval(&block) if block_given?
2019   base
2020 end
register(*extensions, &block) click to toggle source

Extend the top-level DSL with the modules provided.

     # File lib/sinatra/base.rb
2023 def self.register(*extensions, &block)
2024   Delegator.target.register(*extensions, &block)
2025 end
schedule(*) { |end| ... } click to toggle source
    # File lib/sinatra/base.rb
452     def self.schedule(*) yield end
453     def self.defer(*)    yield end
454 
455     def initialize(scheduler = self.class, keep_open = false, &back)
456       @back = back.to_proc
457       @scheduler = scheduler
458       @keep_open = keep_open
459       @callbacks = []
460       @closed = false
461     end
462 
463     def close
464       return if closed?
465 
466       @closed = true
467       @scheduler.schedule { @callbacks.each { |c| c.call } }
468     end
469 
470     def each(&front)
471       @front = front
472       @scheduler.defer do
473         begin
474           @back.call(self)
475         rescue Exception => e
476           @scheduler.schedule { raise e }
477         end
478         close unless @keep_open
479       end
480     end
481 
482     def <<(data)
483       @scheduler.schedule { @front.call(data.to_s) }
484       self
485     end
486 
487     def callback(&block)
488       return yield if closed?
489 
490       @callbacks << block
491     end
492 
493     alias errback callback
494 
495     def closed?
496       @closed
497     end
498   end
499 
500   # Allows to start sending data to the client even though later parts of
501   # the response body have not yet been generated.
502   #
503   # The close parameter specifies whether Stream#close should be called
504   # after the block has been executed. This is only relevant for evented
505   # servers like Rainbows.
506   def stream(keep_open = false)
507     scheduler = env['async.callback'] ? EventMachine : Stream
508     current   = @params.dup
509     body Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } }
510   end
511 
512   # Specify response freshness policy for HTTP caches (Cache-Control header).
513   # Any number of non-value directives (:public, :private, :no_cache,
514   # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
515   # a Hash of value directives (:max_age, :s_maxage).
516   #
517   #   cache_control :public, :must_revalidate, :max_age => 60
518   #   => Cache-Control: public, must-revalidate, max-age=60
519   #
520   # See RFC 2616 / 14.9 for more on standard cache control directives:
521   # http://tools.ietf.org/html/rfc2616#section-14.9.1
522   def cache_control(*values)
523     if values.last.is_a?(Hash)
524       hash = values.pop
525       hash.reject! { |_k, v| v == false }
526       hash.reject! { |k, v| values << k if v == true }
527     else
528       hash = {}
529     end
530 
531     values.map! { |value| value.to_s.tr('_', '-') }
532     hash.each do |key, value|
533       key = key.to_s.tr('_', '-')
534       value = value.to_i if %w[max-age s-maxage].include? key
535       values << "#{key}=#{value}"
536     end
537 
538     response['Cache-Control'] = values.join(', ') if values.any?
539   end
540 
541   # Set the Expires header and Cache-Control/max-age directive. Amount
542   # can be an integer number of seconds in the future or a Time object
543   # indicating when the response should be considered "stale". The remaining
544   # "values" arguments are passed to the #cache_control helper:
545   #
546   #   expires 500, :public, :must_revalidate
547   #   => Cache-Control: public, must-revalidate, max-age=500
548   #   => Expires: Mon, 08 Jun 2009 08:50:17 GMT
549   #
550   def expires(amount, *values)
551     values << {} unless values.last.is_a?(Hash)
552 
553     if amount.is_a? Integer
554       time    = Time.now + amount.to_i
555       max_age = amount
556     else
557       time    = time_for amount
558       max_age = time - Time.now
559     end
560 
561     values.last.merge!(max_age: max_age) { |_key, v1, v2| v1 || v2 }
562     cache_control(*values)
563 
564     response['Expires'] = time.httpdate
565   end
566 
567   # Set the last modified time of the resource (HTTP 'Last-Modified' header)
568   # and halt if conditional GET matches. The +time+ argument is a Time,
569   # DateTime, or other object that responds to +to_time+.
570   #
571   # When the current request includes an 'If-Modified-Since' header that is
572   # equal or later than the time specified, execution is immediately halted
573   # with a '304 Not Modified' response.
574   def last_modified(time)
575     return unless time
576 
577     time = time_for time
578     response['Last-Modified'] = time.httpdate
579     return if env['HTTP_IF_NONE_MATCH']
580 
581     if (status == 200) && env['HTTP_IF_MODIFIED_SINCE']
582       # compare based on seconds since epoch
583       since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
584       halt 304 if since >= time.to_i
585     end
586 
587     if (success? || (status == 412)) && env['HTTP_IF_UNMODIFIED_SINCE']
588       # compare based on seconds since epoch
589       since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
590       halt 412 if since < time.to_i
591     end
592   rescue ArgumentError
593   end
594 
595   ETAG_KINDS = %i[strong weak].freeze
596   # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
597   # GET matches. The +value+ argument is an identifier that uniquely
598   # identifies the current version of the resource. The +kind+ argument
599   # indicates whether the etag should be used as a :strong (default) or :weak
600   # cache validator.
601   #
602   # When the current request includes an 'If-None-Match' header with a
603   # matching etag, execution is immediately halted. If the request method is
604   # GET or HEAD, a '304 Not Modified' response is sent.
605   def etag(value, options = {})
606     # Before touching this code, please double check RFC 2616 14.24 and 14.26.
607     options      = { kind: options } unless Hash === options
608     kind         = options[:kind] || :strong
609     new_resource = options.fetch(:new_resource) { request.post? }
610 
611     unless ETAG_KINDS.include?(kind)
612       raise ArgumentError, ':strong or :weak expected'
613     end
614 
615     value = format('"%s"', value)
616     value = "W/#{value}" if kind == :weak
617     response['ETag'] = value
618 
619     return unless success? || status == 304
620 
621     if etag_matches?(env['HTTP_IF_NONE_MATCH'], new_resource)
622       halt(request.safe? ? 304 : 412)
623     end
624 
625     if env['HTTP_IF_MATCH']
626       return if etag_matches?(env['HTTP_IF_MATCH'], new_resource)
627 
628       halt 412
629     end
630 
631     nil
632   end
633 
634   # Sugar for redirect (example:  redirect back)
635   def back
636     request.referer
637   end
638 
639   # whether or not the status is set to 1xx
640   def informational?
641     status.between? 100, 199
642   end
643 
644   # whether or not the status is set to 2xx
645   def success?
646     status.between? 200, 299
647   end
648 
649   # whether or not the status is set to 3xx
650   def redirect?
651     status.between? 300, 399
652   end
653 
654   # whether or not the status is set to 4xx
655   def client_error?
656     status.between? 400, 499
657   end
658 
659   # whether or not the status is set to 5xx
660   def server_error?
661     status.between? 500, 599
662   end
663 
664   # whether or not the status is set to 404
665   def not_found?
666     status == 404
667   end
668 
669   # whether or not the status is set to 400
670   def bad_request?
671     status == 400
672   end
673 
674   # Generates a Time object from the given value.
675   # Used by #expires and #last_modified.
676   def time_for(value)
677     if value.is_a? Numeric
678       Time.at value
679     elsif value.respond_to? :to_s
680       Time.parse value.to_s
681     else
682       value.to_time
683     end
684   rescue ArgumentError => e
685     raise e
686   rescue Exception
687     raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
688   end
689 
690   private
691 
692   # Helper method checking if a ETag value list includes the current ETag.
693   def etag_matches?(list, new_resource = request.post?)
694     return !new_resource if list == '*'
695 
696     list.to_s.split(/\s*,\s*/).include? response['ETag']
697   end
698 
699   def with_params(temp_params)
700     original = @params
701     @params = temp_params
702     yield
703   ensure
704     @params = original if original
705   end
706 end
use(*args, &block) click to toggle source

Use the middleware for classic applications.

     # File lib/sinatra/base.rb
2033 def self.use(*args, &block)
2034   Delegator.target.use(*args, &block)
2035 end

Public Instance Methods

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

Sugar for redirect (example: redirect back)

    # File lib/sinatra/base.rb
635 def back
636   request.referer
637 end
bad_request?() click to toggle source

whether or not the status is set to 400

    # File lib/sinatra/base.rb
670 def bad_request?
671   status == 400
672 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, :s_maxage).

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
522 def cache_control(*values)
523   if values.last.is_a?(Hash)
524     hash = values.pop
525     hash.reject! { |_k, v| v == false }
526     hash.reject! { |k, v| values << k if v == true }
527   else
528     hash = {}
529   end
530 
531   values.map! { |value| value.to_s.tr('_', '-') }
532   hash.each do |key, value|
533     key = key.to_s.tr('_', '-')
534     value = value.to_i if %w[max-age s-maxage].include? key
535     values << "#{key}=#{value}"
536   end
537 
538   response['Cache-Control'] = values.join(', ') if values.any?
539 end
callback() { || ... } click to toggle source
    # File lib/sinatra/base.rb
487 def callback(&block)
488   return yield if closed?
489 
490   @callbacks << block
491 end
client_error?() click to toggle source

whether or not the status is set to 4xx

    # File lib/sinatra/base.rb
655 def client_error?
656   status.between? 400, 499
657 end
close() click to toggle source
    # File lib/sinatra/base.rb
463 def close
464   return if closed?
465 
466   @closed = true
467   @scheduler.schedule { @callbacks.each { |c| c.call } }
468 end
closed?() click to toggle source
    # File lib/sinatra/base.rb
495 def closed?
496   @closed
497 end
each(&front) click to toggle source
    # File lib/sinatra/base.rb
470 def each(&front)
471   @front = front
472   @scheduler.defer do
473     begin
474       @back.call(self)
475     rescue Exception => e
476       @scheduler.schedule { raise e }
477     end
478     close unless @keep_open
479   end
480 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
605 def etag(value, options = {})
606   # Before touching this code, please double check RFC 2616 14.24 and 14.26.
607   options      = { kind: options } unless Hash === options
608   kind         = options[:kind] || :strong
609   new_resource = options.fetch(:new_resource) { request.post? }
610 
611   unless ETAG_KINDS.include?(kind)
612     raise ArgumentError, ':strong or :weak expected'
613   end
614 
615   value = format('"%s"', value)
616   value = "W/#{value}" if kind == :weak
617   response['ETag'] = value
618 
619   return unless success? || status == 304
620 
621   if etag_matches?(env['HTTP_IF_NONE_MATCH'], new_resource)
622     halt(request.safe? ? 304 : 412)
623   end
624 
625   if env['HTTP_IF_MATCH']
626     return if etag_matches?(env['HTTP_IF_MATCH'], new_resource)
627 
628     halt 412
629   end
630 
631   nil
632 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
693 def etag_matches?(list, new_resource = request.post?)
694   return !new_resource if list == '*'
695 
696   list.to_s.split(/\s*,\s*/).include? response['ETag']
697 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=500
=> Expires: Mon, 08 Jun 2009 08:50:17 GMT
    # File lib/sinatra/base.rb
550 def expires(amount, *values)
551   values << {} unless values.last.is_a?(Hash)
552 
553   if amount.is_a? Integer
554     time    = Time.now + amount.to_i
555     max_age = amount
556   else
557     time    = time_for amount
558     max_age = time - Time.now
559   end
560 
561   values.last.merge!(max_age: max_age) { |_key, v1, v2| v1 || v2 }
562   cache_control(*values)
563 
564   response['Expires'] = time.httpdate
565 end
informational?() click to toggle source

whether or not the status is set to 1xx

    # File lib/sinatra/base.rb
640 def informational?
641   status.between? 100, 199
642 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
574 def last_modified(time)
575   return unless time
576 
577   time = time_for time
578   response['Last-Modified'] = time.httpdate
579   return if env['HTTP_IF_NONE_MATCH']
580 
581   if (status == 200) && env['HTTP_IF_MODIFIED_SINCE']
582     # compare based on seconds since epoch
583     since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
584     halt 304 if since >= time.to_i
585   end
586 
587   if (success? || (status == 412)) && env['HTTP_IF_UNMODIFIED_SINCE']
588     # compare based on seconds since epoch
589     since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
590     halt 412 if since < time.to_i
591   end
592 rescue ArgumentError
593 end
not_found?() click to toggle source

whether or not the status is set to 404

    # File lib/sinatra/base.rb
665 def not_found?
666   status == 404
667 end
redirect?() click to toggle source

whether or not the status is set to 3xx

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

whether or not the status is set to 5xx

    # File lib/sinatra/base.rb
660 def server_error?
661   status.between? 500, 599
662 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 Rainbows.

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

whether or not the status is set to 2xx

    # File lib/sinatra/base.rb
645 def success?
646   status.between? 200, 299
647 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
676 def time_for(value)
677   if value.is_a? Numeric
678     Time.at value
679   elsif value.respond_to? :to_s
680     Time.parse value.to_s
681   else
682     value.to_time
683   end
684 rescue ArgumentError => e
685   raise e
686 rescue Exception
687   raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
688 end
with_params(temp_params) { || ... } click to toggle source
    # File lib/sinatra/base.rb
699 def with_params(temp_params)
700   original = @params
701   @params = temp_params
702   yield
703 ensure
704   @params = original if original
705 end