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