From c2c2098bf23435b2a88d332e34d9909a4c03a5df Mon Sep 17 00:00:00 2001 From: Geoff Buesing Date: Sun, 17 Jan 2010 10:28:49 -0600 Subject: [PATCH] Rack::CacheControl - Set the Cache-Control response header --- README.rdoc | 1 + lib/rack/contrib.rb | 1 + lib/rack/contrib/cache_control.rb | 66 +++++++++++++++++++++++++++++++++++++ test/spec_rack_cache_control.rb | 63 +++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 0 deletions(-) create mode 100644 lib/rack/contrib/cache_control.rb create mode 100644 test/spec_rack_cache_control.rb diff --git a/README.rdoc b/README.rdoc index ca015ae..72fd85c 100644 --- a/README.rdoc +++ b/README.rdoc @@ -46,6 +46,7 @@ interface: * Rack::HostMeta - Configures /host-meta using a block * Rack::Cookies - Adds simple cookie jar hash to env * Rack::Access - Limit access based on IP address +* Rack::CacheControl - Set the Cache-Control response header === Use diff --git a/lib/rack/contrib.rb b/lib/rack/contrib.rb index 8905437..6c278ab 100644 --- a/lib/rack/contrib.rb +++ b/lib/rack/contrib.rb @@ -10,6 +10,7 @@ module Rack autoload :AcceptFormat, "rack/contrib/accept_format" autoload :Access, "rack/contrib/access" autoload :BounceFavicon, "rack/contrib/bounce_favicon" + autoload :CacheControl, "rack/contrib/cache_control" autoload :Cookies, "rack/contrib/cookies" autoload :CSSHTTPRequest, "rack/contrib/csshttprequest" autoload :Deflect, "rack/contrib/deflect" diff --git a/lib/rack/contrib/cache_control.rb b/lib/rack/contrib/cache_control.rb new file mode 100644 index 0000000..d563571 --- /dev/null +++ b/lib/rack/contrib/cache_control.rb @@ -0,0 +1,66 @@ +module Rack + class CacheControl + # Lets you set the Cache-Control response header from middleware. Does not overwrite + # existing Cache-Control response header, if it already has been set. + # + # Examples: + # + # use Rack::CacheControl, :public, :max_age => 5 + # # => Cache-Control: public, max-age=5 + # + # use Rack::CacheControl, :private, :must_revalidate, :community => "UCI" + # # => Cache-Control: private, must-revalidate, community="UCI" + # + # Values specified as a Proc will be called at runtime for each request: + # + # use Rack::CacheControl, :public, :max_age => Proc.new { rand(6) + 3 } + # # => Cache-Control: public, max-age=5 + def initialize(app, *directives) + @app = app + @hash = extract_hash!(directives) + @directives = directives + extract_non_callable_values_from_hash! + stringify_hash_keys! + stringify_directives! + end + + def call(env) + response = @app.call(env) + headers = Utils::HeaderHash.new(response[1]) + unless headers.has_key?('Cache-Control') + headers['Cache-Control'] = directives + end + response[1] = headers + response + end + + private + def extract_hash!(array) + array.last.kind_of?(Hash) ? array.pop : {} + end + + def extract_non_callable_values_from_hash! + @hash.reject! { |k,v| v == false } + @hash.reject! { |k,v| @directives << k if v == true } + @hash.reject! { |k,v| @directives << "#{k}=#{v.inspect}" if !v.respond_to?(:call) } + end + + def stringify_hash_keys! + @hash.each do |key, value| + @hash[stringify_directive(key)] = @hash.delete(key) + end + end + + def stringify_directives! + @directives = @directives.map {|d| stringify_directive(d)}.join(', ') + end + + def stringify_directive(directive) + directive.to_s.tr('_','-') + end + + def directives + @hash.inject(@directives) {|str, (k, v)| "#{str}, #{k}=#{v.call.inspect}"} + end + end +end diff --git a/test/spec_rack_cache_control.rb b/test/spec_rack_cache_control.rb new file mode 100644 index 0000000..4555ada --- /dev/null +++ b/test/spec_rack_cache_control.rb @@ -0,0 +1,63 @@ +require 'test/spec' +require 'rack/mock' +require 'rack/contrib/cache_control' + +context "Rack::CacheControl" do + + setup do + @app = Proc.new { [200, {}, []]} + end + + specify "integer value for key=value directive" do + middleware = Rack::CacheControl.new(@app, :public, :max_age => 5) + status, headers, body = middleware.call({}) + headers['Cache-Control'].should == 'public, max-age=5' + end + + specify "string value for key=value directive" do + middleware = Rack::CacheControl.new(@app, :public, :community => "UCI") + status, headers, body = middleware.call({}) + headers['Cache-Control'].should == %(public, community="UCI") + end + + specify "proc value for key=value directive" do + value = 7 #compile-time value + middleware = Rack::CacheControl.new(@app, :public, :max_age => Proc.new {value}) + value = 5 #runtime value + status, headers, body = middleware.call({}) + headers['Cache-Control'].should == 'public, max-age=5' + end + + specify "multiple directives" do + middleware = Rack::CacheControl.new(@app, :private, :no_cache, :must_revalidate) + status, headers, body = middleware.call({}) + headers['Cache-Control'].should == %(private, no-cache, must-revalidate) + end + + specify "true value" do + middleware = Rack::CacheControl.new(@app, :private, :must_revalidate => true) + status, headers, body = middleware.call({}) + headers['Cache-Control'].should == 'private, must-revalidate' + end + + specify "false value" do + middleware = Rack::CacheControl.new(@app, :private, :must_revalidate => false) + status, headers, body = middleware.call({}) + headers['Cache-Control'].should == 'private' + end + + specify "respects existing Cache-Control header" do + app = Proc.new { [200, {'Cache-Control' => 'private'}, []]} + middleware = Rack::CacheControl.new(app, :public, :max_age => 5) + status, headers, body = middleware.call({}) + headers['Cache-Control'].should == 'private' + end + + specify "respects existing Cache-Control header with case-insensitivity" do + app = Proc.new { [200, {'cache-control' => 'private'}, []]} + middleware = Rack::CacheControl.new(app, :public, :max_age => 5) + status, headers, body = middleware.call({}) + headers['Cache-Control'].should == 'private' + end + +end -- 1.6.1