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 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
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
# 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
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
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
# 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 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
# File lib/sinatra/base.rb 482 def <<(data) 483 @scheduler.schedule { @front.call(data.to_s) } 484 self 485 end
Sugar for redirect (example: redirect back)
# File lib/sinatra/base.rb 635 def back 636 request.referer 637 end
whether or not the status is set to 400
# File lib/sinatra/base.rb 670 def bad_request? 671 status == 400 672 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 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
# File lib/sinatra/base.rb 487 def callback(&block) 488 return yield if closed? 489 490 @callbacks << block 491 end
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
# 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
# File lib/sinatra/base.rb 495 def closed? 496 @closed 497 end
# 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
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
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
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
whether or not the status is set to 1xx
# File lib/sinatra/base.rb 640 def informational? 641 status.between? 100, 199 642 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 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
whether or not the status is set to 404
# File lib/sinatra/base.rb 665 def not_found? 666 status == 404 667 end
whether or not the status is set to 3xx
# File lib/sinatra/base.rb 650 def redirect? 651 status.between? 300, 399 652 end
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
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
whether or not the status is set to 2xx
# File lib/sinatra/base.rb 645 def success? 646 status.between? 200, 299 647 end
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
# 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