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
# 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
# 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
# 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
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
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
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 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
# File lib/sinatra/base.rb 410 def <<(data) 411 @scheduler.schedule { @front.call(data.to_s) } 412 self 413 end
Sugar for redirect (example: redirect back)
# File lib/sinatra/base.rb 557 def back 558 request.referer 559 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
# 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
# File lib/sinatra/base.rb 415 def callback(&block) 416 return yield if @closed 417 @callbacks << block 418 end
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
# 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
# File lib/sinatra/base.rb 422 def closed? 423 @closed 424 end
# 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
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
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
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
whether or not the status is set to 1xx
# File lib/sinatra/base.rb 562 def informational? 563 status.between? 100, 199 564 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.
# 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
whether or not the status is set to 404
# File lib/sinatra/base.rb 587 def not_found? 588 status == 404 589 end
whether or not the status is set to 3xx
# File lib/sinatra/base.rb 572 def redirect? 573 status.between? 300, 399 574 end
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
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
whether or not the status is set to 2xx
# File lib/sinatra/base.rb 567 def success? 568 status.between? 200, 299 569 end
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
# 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