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