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