easy_http_cache 2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2006 Coda Hale
2
+ Copyright (c) 2008 José Valim (jose.valim at gmail dot com)
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,155 @@
1
+ == Easy HTTP Cache
2
+
3
+ Allows Rails applications to do conditional cache easily and in a DRY way
4
+ (without messing up your actions):
5
+
6
+ class ListsController < ApplicationController
7
+ http_cache :index, :show, :last_modified => :list
8
+
9
+ protected
10
+ def list
11
+ @list ||= List.find(params[:id])
12
+ end
13
+ end
14
+
15
+ It uses :last_modified and :etag keys, that besides Time, String or resources
16
+ accepts Proc, Method and Symbol that are evaluated within the current controller.
17
+
18
+ Read more about each option (more examples at the end of this page):
19
+
20
+ :last_modified
21
+ Used to manipulate Last-Modified header. You can pass any object that responds
22
+ to :updated_at, :updated_on or :to_time. If you pass a Proc or Method or Symbol,
23
+ they will be evaluated within the current controller first.
24
+
25
+ Finally, if you pass an array, it will get the most recent time to be used.
26
+
27
+ :etag
28
+ Used to manipulate Etag header. The Etag is generated as memcached keys are
29
+ generated, i.e. calling to_param in the object and then MD5 is applied.
30
+
31
+ If you pass a Proc or Method or Symbols, they will be also evaluated within the
32
+ current controller first.
33
+
34
+ :if
35
+ Only perform http cache if it returns true.
36
+
37
+ :unless
38
+ Only perform http cache if it returns false.
39
+
40
+ :method
41
+ If in :last_modified you want to pass a object that doesn't respond to updated_at,
42
+ updated_on or to_time, you can specify the method that will be called in this object.
43
+
44
+ == Install
45
+
46
+ Install Easy HTTP Cache is available on gemcutter, so just execute:
47
+
48
+ sudo gem install easy_http_cache
49
+
50
+ And add it to your environment.
51
+
52
+ == Environment Variables
53
+
54
+ As in memcached, you can set ENV['RAILS_CACHE_ID'] or ENV['RAILS_APP_VERSION'] variables
55
+ to change the Etag that will be generated. This means you can control the cache by setting
56
+ a timestamp or a version number in ENV['RAILS_APP_VERSION'] everytime you deploy.
57
+
58
+ == Examples
59
+
60
+ The example below will cache your actions and it will never expire:
61
+
62
+ class ListsController < ApplicationController
63
+ http_cache :index, :show
64
+ end
65
+
66
+ If you do not want to cache when you are showing a flash message (and you
67
+ usually want that), you can simply do:
68
+
69
+ class ListsController < ApplicationController
70
+ http_cache :index, :show, :if => Proc.new { |c| c.send(:flash).empty? }
71
+ end
72
+
73
+ And if you do not want to cache JSON requests:
74
+
75
+ class ListsController < ApplicationController
76
+ http_cache :index, :show, :unless => Proc.new { |c| c.request.format.json? }
77
+ end
78
+
79
+ Or if you want to expire all http cache before 2008, just do:
80
+
81
+ class ListsController < ApplicationController
82
+ http_cache :index, :show, :last_modified => Time.utc(2008)
83
+ end
84
+
85
+ If you want to cache a list and automatically expire the cache when it changes,
86
+ just do (it will check updated_at and updated_on on the @list object):
87
+
88
+ class ListsController < ApplicationController
89
+ http_cache :index, :show, :last_modified => :list
90
+
91
+ protected
92
+ def list
93
+ @list ||= List.find(params[:id])
94
+ end
95
+ end
96
+
97
+ You can also set :etag header (it will generate an etag calling to_param
98
+ in the object and applying MD5):
99
+
100
+ class ListsController < ApplicationController
101
+ http_cache :index, :show, :etag => :list
102
+
103
+ protected
104
+ def list
105
+ @list ||= List.find(params[:id])
106
+ end
107
+ end
108
+
109
+ If you are using a resource that doesn't respond to updated_at or updated_on,
110
+ you can pass a method as parameter that will be called in your resources:
111
+
112
+ class ListsController < ApplicationController
113
+ http_cache :index, :show, :last_modified => :list, :method => :cached_at
114
+
115
+ protected
116
+ def list
117
+ @list ||= List.find(params[:id])
118
+ end
119
+ end
120
+
121
+ The sample below will call @list.cached_at to generate Last-Modified header.
122
+ Finally, you can also pass an array at :last_modified as below:
123
+
124
+ class ItemsController < ApplicationController
125
+ http_cache :index, :show,
126
+ :last_modified => [ :list, :item ]
127
+
128
+ protected
129
+ def list
130
+ @list ||= List.find(params[:list_id])
131
+ end
132
+
133
+ def item
134
+ @item ||= list.items.find(params[:id])
135
+ end
136
+ end
137
+
138
+ This will check which one is the most recent to compare with the
139
+ "Last-Modified" field sent by the client.
140
+
141
+ == What if?
142
+
143
+ At this point (or at some point), you will ask what happens if you use :etag
144
+ and :last_modified at the same time.
145
+
146
+ Well, the specification says that if both are sent by the client, both have
147
+ to be valid for the cache not be considered stale. This subject was already brought
148
+ to Rails Core group and this is also how Rails' current implementation behaves.
149
+
150
+ == Bugs and Feedback
151
+
152
+ If you find any issues, please use Github issues tracker.
153
+
154
+ Copyright (c) 2009 José Valim
155
+ http://blog.plataformatec.com.br/
@@ -0,0 +1,37 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Run tests for Easy HTTP Cache.'
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.pattern = 'test/**/*_test.rb'
8
+ t.verbose = true
9
+ end
10
+
11
+ desc 'Generate documentation for Easy HTTP Cache.'
12
+ Rake::RDocTask.new(:rdoc) do |rdoc|
13
+ rdoc.rdoc_dir = 'rdoc'
14
+ rdoc.title = 'Easy HTTP Cache'
15
+ rdoc.options << '--line-numbers' << '--inline-source'
16
+ rdoc.rdoc_files.include('README')
17
+ rdoc.rdoc_files.include('MIT-LICENSE')
18
+ rdoc.rdoc_files.include('lib/**/*.rb')
19
+ end
20
+
21
+ begin
22
+ require 'jeweler'
23
+ Jeweler::Tasks.new do |s|
24
+ s.name = "easy_http_cache"
25
+ s.version = "2.2"
26
+ s.summary = "Allows Rails applications to use HTTP cache specifications easily."
27
+ s.email = "contact@plataformatec.com.br"
28
+ s.homepage = "http://github.com/josevalim/easy_http_cache"
29
+ s.description = "Allows Rails applications to use HTTP cache specifications easily."
30
+ s.authors = ['José Valim']
31
+ s.files = FileList["[A-Z]*", "lib/**/*", "init.rb"]
32
+ end
33
+
34
+ Jeweler::GemcutterTasks.new
35
+ rescue LoadError
36
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install jeweler"
37
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'easy_http_cache'
@@ -0,0 +1,126 @@
1
+ module ActionController #:nodoc:
2
+ module Caching
3
+ module HttpCache
4
+ def self.included(base) #:nodoc:
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ # Declares that +actions+ should be cached.
10
+ #
11
+ def http_cache(*actions)
12
+ return unless perform_caching
13
+ options = actions.extract_options!
14
+
15
+ options.assert_valid_keys(
16
+ :last_modified, :method, :etag, :if, :unless
17
+ )
18
+
19
+ http_cache_filter = HttpCacheFilter.new(
20
+ :method => options.delete(:method),
21
+ :last_modified => [options.delete(:last_modified)].flatten.compact,
22
+ :etag => options.delete(:etag)
23
+ )
24
+ filter_options = {:only => actions}.merge(options)
25
+
26
+ before_filter(http_cache_filter, filter_options)
27
+ end
28
+ end
29
+
30
+ class HttpCacheFilter #:nodoc:
31
+ def initialize(options = {})
32
+ @options = options
33
+ end
34
+
35
+ def filter(controller)
36
+ return unless flash.empty? && controller.request.get?
37
+
38
+ last_modified = get_last_modified(controller)
39
+ controller.response.last_modified = last_modified if last_modified
40
+
41
+ processed_etags = get_processed_etags(controller)
42
+ controller.response.etag = processed_etags if processed_etags
43
+
44
+ if controller.request.fresh?(controller.response)
45
+ controller.send(:head, :not_modified)
46
+ return false
47
+ end
48
+ end
49
+
50
+ protected
51
+ # If :etag is an array, it processes all Methods, Procs and Symbols
52
+ # and return them as array. If it's an object, we only evaluate it.
53
+ #
54
+ def get_processed_etags(controller)
55
+ if @options[:etag].is_a?(Array)
56
+ @options[:etag].collect do |item|
57
+ evaluate_method(item, controller)
58
+ end
59
+ elsif @options[:etag]
60
+ evaluate_method(@options[:etag], controller)
61
+ else
62
+ nil
63
+ end
64
+ end
65
+
66
+ # We perform Last-Modified HTTP Cache when the option :last_modified is sent
67
+ # or no other cache mechanism is set (then we set a very old timestamp).
68
+ #
69
+ def get_last_modified(controller)
70
+ # Then, if @options[:last_modified] is not empty, we run through the array
71
+ # processing all objects (if needed) and return the latest one to be used.
72
+ #
73
+ if !@options[:last_modified].empty?
74
+ @options[:last_modified].collect do |item|
75
+ evaluate_time(item, controller)
76
+ end.compact.sort.last
77
+ elsif @options[:etag].blank?
78
+ Time.utc(0)
79
+ else
80
+ nil
81
+ end
82
+ end
83
+
84
+ def evaluate_method(method, controller)
85
+ case method
86
+ when Symbol
87
+ controller.__send__(method)
88
+ when Proc, Method
89
+ method.call(controller)
90
+ else
91
+ method
92
+ end
93
+ end
94
+
95
+ # Evaluate the objects sent and return time objects
96
+ #
97
+ # It process Symbols, String, Proc and Methods, get its results and then
98
+ # call :to_time, :updated_at, :updated_on on it.
99
+ #
100
+ # If the parameter :method is sent, it will try to call it on the object before
101
+ # calling :to_time, :updated_at, :updated_on.
102
+ #
103
+ def evaluate_time(method, controller)
104
+ return nil unless method
105
+ time = evaluate_method(method, controller)
106
+
107
+ time = time.__send__(@options[:method]) if @options[:method].is_a?(Symbol) && time.respond_to?(@options[:method])
108
+
109
+ if time.respond_to?(:to_time)
110
+ time.to_time.utc
111
+ elsif time.respond_to?(:updated_at)
112
+ time.updated_at.utc
113
+ elsif time.respond_to?(:updated_on)
114
+ time.updated_on.utc
115
+ else
116
+ nil
117
+ end
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+ end
124
+ end
125
+
126
+ ActionController::Base.send :include, ActionController::Caching::HttpCache
@@ -0,0 +1,212 @@
1
+ # Those lines are plugin test settings
2
+ require 'test/unit'
3
+ require 'rubygems'
4
+ require 'ostruct'
5
+
6
+ ENV["RAILS_ENV"] = "test"
7
+
8
+ require 'active_support'
9
+ require 'action_controller'
10
+ require 'action_controller/test_case'
11
+ require 'action_controller/test_process'
12
+
13
+ require File.dirname(__FILE__) + '/../lib/easy_http_cache.rb'
14
+
15
+ ActionController::Base.perform_caching = true
16
+ ActionController::Routing::Routes.draw do |map|
17
+ map.connect ':controller/:action/:id'
18
+ end
19
+
20
+ class HttpCacheTestController < ActionController::Base
21
+ http_cache :index
22
+ http_cache :show, :last_modified => 2.hours.ago, :if => Proc.new { |c| !c.request.format.json? }
23
+ http_cache :edit, :last_modified => Proc.new{ 30.minutes.ago }
24
+ http_cache :destroy, :last_modified => [2.hours.ago, Proc.new{|c| 30.minutes.ago }]
25
+ http_cache :invalid, :last_modified => [1.hours.ago, false]
26
+
27
+ http_cache :etag, :etag => 'ETAG_CACHE'
28
+ http_cache :etag_array, :etag => [ 'ETAG_CACHE', :resource ]
29
+ http_cache :resources, :last_modified => [:resource, :list, :object]
30
+ http_cache :resources_with_method, :last_modified => [:resource, :list, :object], :method => :cached_at
31
+
32
+ def index
33
+ render :text => '200 OK', :status => 200
34
+ end
35
+
36
+ alias_method :show, :index
37
+ alias_method :edit, :index
38
+ alias_method :destroy, :index
39
+ alias_method :invalid, :index
40
+ alias_method :etag, :index
41
+ alias_method :etag_array, :index
42
+ alias_method :resources, :index
43
+ alias_method :resources_with_method, :index
44
+
45
+ protected
46
+
47
+ def resource
48
+ resource = OpenStruct.new
49
+ resource.instance_eval do
50
+ def to_param
51
+ 12345
52
+ end
53
+ end
54
+
55
+ resource.updated_at = 2.hours.ago
56
+ resource
57
+ end
58
+
59
+ def list
60
+ list = OpenStruct.new
61
+ list.updated_on = 30.minutes.ago
62
+ list
63
+ end
64
+
65
+ def object
66
+ object = OpenStruct.new
67
+ object.cached_at = 15.minutes.ago
68
+ object
69
+ end
70
+ end
71
+
72
+ class HttpCacheTest < ActionController::TestCase
73
+ def setup
74
+ reset!
75
+ end
76
+
77
+ def test_last_modified_http_cache
78
+ last_modified_http_cache(:show, 1.hour.ago, 3.hours.ago)
79
+ end
80
+
81
+ def test_last_modified_http_cache_with_proc
82
+ last_modified_http_cache(:edit, 15.minutes.ago, 45.minutes.ago)
83
+ end
84
+
85
+ def test_last_modified_http_cache_with_array
86
+ last_modified_http_cache(:destroy, 15.minutes.ago, 45.minutes.ago)
87
+ end
88
+
89
+ def test_last_modified_http_cache_with_resources
90
+ last_modified_http_cache(:resources, 15.minutes.ago, 45.minutes.ago)
91
+ end
92
+
93
+ def test_last_modified_http_cache_with_resources_with_method
94
+ last_modified_http_cache(:resources_with_method, 10.minutes.ago, 20.minutes.ago)
95
+ end
96
+
97
+ def test_last_modified_http_cache_discards_invalid_input
98
+ last_modified_http_cache(:invalid, 30.minutes.ago, 90.minutes.ago)
99
+ end
100
+
101
+ def test_http_cache_without_input
102
+ get :index
103
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'Last-Modified', Time.utc(0).httpdate)
104
+ reset!
105
+
106
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = 1.hour.ago.httpdate
107
+ get :index
108
+ assert_headers('304 Not Modified', 'private, max-age=0, must-revalidate', 'Last-Modified', Time.utc(0).httpdate)
109
+ reset!
110
+
111
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = 3.hours.ago.httpdate
112
+ get :index
113
+ assert_headers('304 Not Modified', 'private, max-age=0, must-revalidate', 'Last-Modified', Time.utc(0).httpdate)
114
+ end
115
+
116
+ def test_http_cache_with_conditional_options
117
+ @request.env['HTTP_ACCEPT'] = 'application/json'
118
+ get :show
119
+ assert_nil @response.headers['Last-Modified']
120
+ reset!
121
+
122
+ @request.env['HTTP_ACCEPT'] = 'application/json'
123
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = 1.hour.ago.httpdate
124
+ get :show
125
+ assert_equal '200 OK', @response.status
126
+ end
127
+
128
+ def test_etag_http_cache
129
+ etag_http_cache(:etag, 'ETAG_CACHE')
130
+ end
131
+
132
+ def test_etag_http_cache_with_array
133
+ etag_http_cache(:etag_array, ['ETAG_CACHE', 12345])
134
+ end
135
+
136
+ def test_etag_http_cache_with_env_variable
137
+ ENV['RAILS_APP_VERSION'] = '1.2.3'
138
+ etag_http_cache(:etag, 'ETAG_CACHE')
139
+ end
140
+
141
+ private
142
+ def reset!
143
+ @request = ActionController::TestRequest.new
144
+ @response = ActionController::TestResponse.new
145
+ @controller = HttpCacheTestController.new
146
+ end
147
+
148
+ def etag_for(etag)
149
+ %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
150
+ end
151
+
152
+ def assert_headers(status, control, cache_header=nil, value=nil)
153
+ assert_equal status, @response.status
154
+ assert_equal control, @response.headers['Cache-Control']
155
+
156
+ if cache_header
157
+ if value
158
+ assert_equal value, @response.headers[cache_header]
159
+ else
160
+ assert @response.headers[cache_header]
161
+ end
162
+ end
163
+ end
164
+
165
+ # Goes through a http cache process:
166
+ #
167
+ # 1. Request an action
168
+ # 2. Get a '200 OK' status
169
+ # 3. Request the same action with a not expired HTTP_IF_MODIFIED_SINCE
170
+ # 4. Get a '304 Not Modified' status
171
+ # 5. Request the same action with an expired HTTP_IF_MODIFIED_SINCE
172
+ # 6. Get a '200 OK' status
173
+ #
174
+ def last_modified_http_cache(action, not_expired_time, expired_time)
175
+ get action
176
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'Last-Modified')
177
+ reset!
178
+
179
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = not_expired_time.httpdate
180
+ get action
181
+ assert_headers('304 Not Modified', 'private, max-age=0, must-revalidate', 'Last-Modified')
182
+ reset!
183
+
184
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = expired_time.httpdate
185
+ get action
186
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'Last-Modified')
187
+ end
188
+
189
+ # Goes through a http cache process:
190
+ #
191
+ # 1. Request an action
192
+ # 2. Get a '200 OK' status
193
+ # 3. Request the same action with a valid ETAG
194
+ # 4. Get a '304 Not Modified' status
195
+ # 5. Request the same action with an invalid IF_NONE_MATCH
196
+ # 6. Get a '200 OK' status
197
+ #
198
+ def etag_http_cache(action, variable)
199
+ get action
200
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'ETag', etag_for(variable))
201
+ reset!
202
+
203
+ @request.env['HTTP_IF_NONE_MATCH'] = etag_for(variable)
204
+ get action
205
+ assert_headers('304 Not Modified', 'private, max-age=0, must-revalidate', 'ETag', etag_for(variable))
206
+ reset!
207
+
208
+ @request.env['HTTP_IF_NONE_MATCH'] = 'INVALID'
209
+ get action
210
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'ETag', etag_for(variable))
211
+ end
212
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easy_http_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: "2.2"
5
+ platform: ruby
6
+ authors:
7
+ - "Jos\xC3\xA9 Valim"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-23 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Allows Rails applications to use HTTP cache specifications easily.
17
+ email: contact@plataformatec.com.br
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - MIT-LICENSE
26
+ - README.rdoc
27
+ - Rakefile
28
+ - init.rb
29
+ - lib/easy_http_cache.rb
30
+ has_rdoc: true
31
+ homepage: http://github.com/josevalim/easy_http_cache
32
+ licenses: []
33
+
34
+ post_install_message:
35
+ rdoc_options:
36
+ - --charset=UTF-8
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements: []
52
+
53
+ rubyforge_project:
54
+ rubygems_version: 1.3.5
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: Allows Rails applications to use HTTP cache specifications easily.
58
+ test_files:
59
+ - test/easy_http_cache_test.rb