josevalim-easy_http_cache 2.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -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.
data/README ADDED
@@ -0,0 +1,174 @@
1
+ Copyright (c) 2008 José Valim (jose.valim at gmail dot com)
2
+ Site: http://www.pagestacker.com/
3
+ Blog: http://josevalim.blogspot.com/
4
+ License: MIT
5
+ Version: 2.0
6
+
7
+ You can also read this README in pretty html at the GitHub project Wiki page
8
+
9
+ http://github.com/josevalim/easy_http_cache/wikis/home
10
+
11
+ Warning
12
+ -------
13
+
14
+ Since version 2.0, this plugin/gem has drastically changed to fit Rails 2.2
15
+ http cache goodness. :expires_in, :expires_at and :control options were
16
+ removed, so if you want to use previous versions, see "Previous versions"
17
+ below.
18
+
19
+ Description
20
+ -----------
21
+
22
+ Allows Rails applications to do conditional cache easily and in a DRY way
23
+ (without messing up your actions):
24
+
25
+ class ListsController < ApplicationController
26
+ http_cache :index, :show
27
+ end
28
+
29
+ It uses :last_modified and :etag keys, that besides Time, String or resources
30
+ accepts Proc, Method and Symbols that are evaluated within the current controller.
31
+ Read more about each option below:
32
+
33
+ :last_modified
34
+ Used to manipulate Last-Modified header. You can pass any object that responds
35
+ to :to_time. If you pass a Proc or Method or Symbols, they will be evaluated
36
+ within the current controller and :to_time will be called.
37
+
38
+ You can also pass resources and :updated_at or :updated_on will be called on
39
+ it. If you want to call a different method on your resource, you can pass it as
40
+ a symbol using the :method option.
41
+
42
+ All times will be converted to UTC. Finally, if you pass an array, it will get
43
+ the most recent time to be used.
44
+
45
+ :etag
46
+ Used to manipulate Etag header. If you pass a Proc or Method or Symbols, they
47
+ will be evaluated within the current controller.
48
+
49
+ You can also pass an array and each element will be also evaluated with needed.
50
+
51
+ :if
52
+ Only perform http cache if it returns true.
53
+
54
+ :unless
55
+ Only perform http cache if it returns false.
56
+
57
+
58
+ Install
59
+ -------
60
+
61
+ Install Easy HTTP Cache is very easy. It is stored in GitHub, so if you
62
+ have never installed a gem via GitHub run the following:
63
+
64
+ gem sources -a http://gems.github.com
65
+
66
+ Then install the gem:
67
+
68
+ sudo gem install josevalim-easy_http_cache
69
+
70
+ In RAILS_ROOT/config/environment.rb:
71
+
72
+ config.gem "josevalim-easy_http_cache", :lib => "easy_http_cache", :source => "http://gems.github.com"
73
+
74
+ If you want it as plugin, just do:
75
+
76
+ cd myapp
77
+ git clone git://github.com/josevalim/easy_http_cache.git
78
+ rm -rf vendor/plugins/easy_http_cache/.git
79
+
80
+
81
+ Previous versions
82
+ -----------------
83
+
84
+ If you are running on Rails 2.1.x, you should use v1.2.3:
85
+
86
+ cd myapp
87
+ git clone git://github.com/josevalim/easy_http_cache.git
88
+ cd vendor/plugins/easy_http_cache
89
+ git checkout v1.2.3
90
+ rm -rf ./.git
91
+
92
+ If you are using a previous version, please updagrade your app. =)
93
+
94
+
95
+ Variables
96
+ ---------
97
+
98
+ You can set ENV['RAILS_CACHE_ID'] or ENV['RAILS_APP_VERSION'] to change
99
+ the ETag that will be generated, expiring all previous caches. Those variables
100
+ are also used by other cache stores (memcached, file, ...).
101
+
102
+
103
+ Examples
104
+ --------
105
+
106
+ Just as above:
107
+
108
+ class ListsController < ApplicationController
109
+ http_cache :index, :show
110
+ end
111
+
112
+ If you do not want to cache when you are showing a flash message (and you
113
+ usually want that), you can simply do:
114
+
115
+ class ListsController < ApplicationController
116
+ http_cache :index, :show, :if => Proc.new { |c| c.__send__(:flash).empty? }
117
+ end
118
+
119
+ And if you do not want JSON requests:
120
+
121
+ class ListsController < ApplicationController
122
+ http_cache :index, :show, :unless => Proc.new { |c| c.request.format.json? }
123
+ end
124
+
125
+ Or if you want to expire all http cache before 2008, just do:
126
+
127
+ class ListsController < ApplicationController
128
+ http_cache :index, :show, :last_modified => Time.utc(2008)
129
+ end
130
+
131
+ If You want to cache a list and automatically expire the cache when
132
+ it changes, just do:
133
+
134
+ class ListsController < ApplicationController
135
+ http_cache :index, :show, :last_modified => :list
136
+
137
+ protected
138
+ def list
139
+ @list ||= List.find(params[:id])
140
+ end
141
+ end
142
+
143
+ You can also set :etag header:
144
+
145
+ class ListsController < ApplicationController
146
+ http_cache :index, :show, :etag => :list
147
+
148
+ protected
149
+ def list
150
+ @list ||= List.find(params[:id])
151
+ end
152
+ end
153
+
154
+ If you are using a resource that doesn't respond to updated_at or updated_on,
155
+ you can pass a method as parameter and it will be called in your resources:
156
+
157
+ class ListsController < ApplicationController
158
+ http_cache :index, :show, :last_modified => :list, :method => :cached_at
159
+
160
+ protected
161
+ def list
162
+ @list ||= List.find(params[:id])
163
+ end
164
+ end
165
+
166
+ Finally, you can also pass an array at :last_modified as below:
167
+
168
+ class ListsController < ApplicationController
169
+ http_cache :index, :show,
170
+ :last_modified => [ :list, Time.utc(2007,12,27) ]
171
+ end
172
+
173
+ This will check which one is the most recent to compare with the
174
+ "Last-Modified" field sent by the client.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Generate documentation for Footnotes plugin.'
6
+ Rake::RDocTask.new(:rdoc) do |rdoc|
7
+ rdoc.rdoc_dir = 'rdoc'
8
+ rdoc.title = 'Easy HTTP Cache'
9
+ rdoc.options << '--line-numbers' << '--inline-source'
10
+ rdoc.rdoc_files.include('README')
11
+ rdoc.rdoc_files.include('lib/**/*.rb')
12
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'easy_http_cache'
@@ -0,0 +1,140 @@
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
+ # We don't go ahead if we are rendering a component
37
+ #
38
+ return if component_request?(controller)
39
+
40
+ last_modified = get_last_modified(controller)
41
+ controller.response.last_modified = last_modified if last_modified
42
+
43
+ processed_etags = get_processed_etags(controller)
44
+ controller.response.etag = processed_etags if processed_etags
45
+
46
+ if controller.request.fresh?(controller.response)
47
+ controller.__send__(:head, :not_modified)
48
+ return false
49
+ end
50
+ end
51
+
52
+ protected
53
+ # If :etag is an array, it processes all Methods, Procs and Symbols
54
+ # and return them as array. If it's an object, we only evaluate it.
55
+ #
56
+ # Finally, if :etag is not sent but RAILS_CACHE_ID or RAILS_APP_VERSION
57
+ # are set, we return an empty string allowing etag to be performed
58
+ # because those variables, when modified, are a valid way to expire
59
+ # all previous caches.
60
+ #
61
+ def get_processed_etags(controller)
62
+ if @options[:etag].is_a?(Array)
63
+ @options[:etag].collect do |item|
64
+ evaluate_method(item, controller)
65
+ end
66
+ elsif @options[:etag]
67
+ evaluate_method(@options[:etag], controller)
68
+ elsif ENV['RAILS_CACHE_ID'] || ENV['RAILS_APP_VERSION']
69
+ ''
70
+ else
71
+ nil
72
+ end
73
+ end
74
+
75
+ # We perform Last-Modified HTTP Cache when the option :last_modified is sent
76
+ # or no other cache mechanism is set (then we set a very old timestamp).
77
+ #
78
+ def get_last_modified(controller)
79
+ # Then, if @options[:last_modified] is not empty, we run through the array
80
+ # processing all objects (if needed) and return the latest one to be used.
81
+ #
82
+ if !@options[:last_modified].empty?
83
+ @options[:last_modified].collect do |item|
84
+ evaluate_time(item, controller)
85
+ end.compact.sort.last
86
+ elsif @options[:etag].blank?
87
+ Time.utc(0)
88
+ else
89
+ nil
90
+ end
91
+ end
92
+
93
+ def evaluate_method(method, controller)
94
+ case method
95
+ when Symbol
96
+ controller.__send__(method)
97
+ when Proc, Method
98
+ method.call(controller)
99
+ else
100
+ method
101
+ end
102
+ end
103
+
104
+ # Evaluate the objects sent and return time objects
105
+ #
106
+ # It process Symbols, String, Proc and Methods, get its results and then
107
+ # call :to_time, :updated_at, :updated_on on it.
108
+ #
109
+ # If the parameter :method is sent, it will try to call it on the object before
110
+ # calling :to_time, :updated_at, :updated_on.
111
+ #
112
+ def evaluate_time(method, controller)
113
+ return nil unless method
114
+ time = evaluate_method(method, controller)
115
+
116
+ time = time.__send__(@options[:method]) if @options[:method].is_a?(Symbol) && time.respond_to?(@options[:method])
117
+
118
+ if time.respond_to?(:to_time)
119
+ time.to_time.utc
120
+ elsif time.respond_to?(:updated_at)
121
+ time.updated_at.utc
122
+ elsif time.respond_to?(:updated_on)
123
+ time.updated_on.utc
124
+ else
125
+ nil
126
+ end
127
+ end
128
+
129
+ # We should not do http cache when we are using components
130
+ #
131
+ def component_request?(controller)
132
+ controller.instance_variable_get('@parent_controller')
133
+ end
134
+ end
135
+
136
+ end
137
+ end
138
+ end
139
+
140
+ ActionController::Base.__send__ :include, ActionController::Caching::HttpCache
@@ -0,0 +1,239 @@
1
+ # Those lines are plugin test settings
2
+ ENV["RAILS_ENV"] = "test"
3
+ require 'ostruct'
4
+ require File.dirname(__FILE__) + '/../../../../config/environment'
5
+ require File.dirname(__FILE__) + '/../lib/easy_http_cache.rb'
6
+ require 'test_help'
7
+
8
+ ActionController::Base.perform_caching = true
9
+ ActionController::Routing::Routes.draw do |map|
10
+ map.connect ':controller/:action/:id'
11
+ end
12
+
13
+ class HttpCacheTestController < ActionController::Base
14
+ http_cache :index
15
+ http_cache :show, :last_modified => 2.hours.ago, :if => Proc.new { |c| !c.request.format.json? }
16
+ http_cache :edit, :last_modified => Proc.new{ 30.minutes.ago }
17
+ http_cache :destroy, :last_modified => [2.hours.ago, Proc.new{|c| 30.minutes.ago }]
18
+ http_cache :invalid, :last_modified => [1.hours.ago, false]
19
+
20
+ http_cache :etag, :etag => 'ETAG_CACHE'
21
+ http_cache :etag_array, :etag => [ 'ETAG_CACHE', :resource ]
22
+ http_cache :resources, :last_modified => [:resource, :list, :object]
23
+ http_cache :resources_with_method, :last_modified => [:resource, :list, :object], :method => :cached_at
24
+
25
+ def index
26
+ render :text => '200 OK', :status => 200
27
+ end
28
+
29
+ alias_method :show, :index
30
+ alias_method :edit, :index
31
+ alias_method :destroy, :index
32
+ alias_method :invalid, :index
33
+ alias_method :etag, :index
34
+ alias_method :etag_array, :index
35
+ alias_method :resources, :index
36
+ alias_method :resources_with_method, :index
37
+
38
+ protected
39
+
40
+ def resource
41
+ resource = OpenStruct.new
42
+ resource.instance_eval do
43
+ def to_param
44
+ 12345
45
+ end
46
+ end
47
+
48
+ resource.updated_at = 2.hours.ago
49
+ resource
50
+ end
51
+
52
+ def list
53
+ list = OpenStruct.new
54
+ list.updated_on = 30.minutes.ago
55
+ list
56
+ end
57
+
58
+ def object
59
+ object = OpenStruct.new
60
+ object.cached_at = 15.minutes.ago
61
+ object
62
+ end
63
+ end
64
+
65
+ class HttpCacheTest < Test::Unit::TestCase
66
+ def setup
67
+ reset!
68
+ end
69
+
70
+ def test_last_modified_http_cache
71
+ last_modified_http_cache(:show, 1.hour.ago, 3.hours.ago)
72
+ end
73
+
74
+ def test_last_modified_http_cache_with_proc
75
+ last_modified_http_cache(:edit, 15.minutes.ago, 45.minutes.ago)
76
+ end
77
+
78
+ def test_last_modified_http_cache_with_array
79
+ last_modified_http_cache(:destroy, 15.minutes.ago, 45.minutes.ago)
80
+ end
81
+
82
+ def test_last_modified_http_cache_with_resources
83
+ last_modified_http_cache(:resources, 15.minutes.ago, 45.minutes.ago)
84
+ end
85
+
86
+ def test_last_modified_http_cache_with_resources_with_method
87
+ last_modified_http_cache(:resources_with_method, 10.minutes.ago, 20.minutes.ago)
88
+ end
89
+
90
+ def test_last_modified_http_cache_discards_invalid_input
91
+ last_modified_http_cache(:invalid, 30.minutes.ago, 90.minutes.ago)
92
+ end
93
+
94
+ def test_http_cache_without_input
95
+ get :index
96
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'Last-Modified', Time.utc(0).httpdate)
97
+ reset!
98
+
99
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = 1.hour.ago.httpdate
100
+ get :index
101
+ assert_headers('304 Not Modified', 'private, max-age=0, must-revalidate', 'Last-Modified', Time.utc(0).httpdate)
102
+ reset!
103
+
104
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = 3.hours.ago.httpdate
105
+ get :index
106
+ assert_headers('304 Not Modified', 'private, max-age=0, must-revalidate', 'Last-Modified', Time.utc(0).httpdate)
107
+ end
108
+
109
+ def test_http_cache_with_conditional_options
110
+ @request.env['HTTP_ACCEPT'] = 'application/json'
111
+ get :show
112
+ assert_nil @response.headers['Last-Modified']
113
+ reset!
114
+
115
+ @request.env['HTTP_ACCEPT'] = 'application/json'
116
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = 1.hour.ago.httpdate
117
+ get :show
118
+ assert_equal '200 OK', @response.headers['Status']
119
+ end
120
+
121
+ def test_http_cache_without_input_with_env_variable
122
+ ENV['RAILS_APP_VERSION'] = '1.2.3'
123
+
124
+ get :index
125
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'Last-Modified', Time.utc(0).httpdate)
126
+ reset!
127
+
128
+ etag_http_cache(:index, '')
129
+ end
130
+
131
+ def test_etag_http_cache
132
+ etag_http_cache(:etag, 'ETAG_CACHE')
133
+ end
134
+
135
+ def test_etag_http_cache_with_array
136
+ etag_http_cache(:etag_array, ['ETAG_CACHE', 12345])
137
+ end
138
+
139
+ def test_etag_http_cache_with_env_variable
140
+ ENV['RAILS_APP_VERSION'] = '1.2.3'
141
+ etag_http_cache(:etag, 'ETAG_CACHE')
142
+ end
143
+
144
+ def test_should_not_cache_when_rendering_components
145
+ set_parent_controller!
146
+ get :show
147
+ assert_headers('200 OK', 'no-cache')
148
+
149
+ set_parent_controller!
150
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = 1.hour.ago.httpdate
151
+ get :show
152
+ assert_headers('200 OK', 'no-cache')
153
+
154
+ set_parent_controller!
155
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = 3.hours.ago.httpdate
156
+ get :show
157
+ assert_headers('200 OK', 'no-cache')
158
+ end
159
+
160
+ private
161
+ def reset!
162
+ @request = ActionController::TestRequest.new
163
+ @response = ActionController::TestResponse.new
164
+ @controller = HttpCacheTestController.new
165
+ end
166
+
167
+ def set_parent_controller!
168
+ get :index
169
+ old_controller = @controller.dup
170
+ reset!
171
+
172
+ @controller.instance_variable_set('@parent_controller', old_controller)
173
+ end
174
+
175
+ def etag_for(etag)
176
+ %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
177
+ end
178
+
179
+ def assert_headers(status, control, cache_header=nil, value=nil)
180
+ assert_equal status, @response.headers['Status']
181
+ assert_equal control, @response.headers['Cache-Control']
182
+
183
+ if cache_header
184
+ if value
185
+ assert_equal value, @response.headers[cache_header]
186
+ else
187
+ assert @response.headers[cache_header]
188
+ end
189
+ end
190
+ end
191
+
192
+ # Goes through a http cache process:
193
+ #
194
+ # 1. Request an action
195
+ # 2. Get a '200 OK' status
196
+ # 3. Request the same action with a not expired HTTP_IF_MODIFIED_SINCE
197
+ # 4. Get a '304 Not Modified' status
198
+ # 5. Request the same action with an expired HTTP_IF_MODIFIED_SINCE
199
+ # 6. Get a '200 OK' status
200
+ #
201
+ def last_modified_http_cache(action, not_expired_time, expired_time)
202
+ get action
203
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'Last-Modified')
204
+ reset!
205
+
206
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = not_expired_time.httpdate
207
+ get action
208
+ assert_headers('304 Not Modified', 'private, max-age=0, must-revalidate', 'Last-Modified')
209
+ reset!
210
+
211
+ @request.env['HTTP_IF_MODIFIED_SINCE'] = expired_time.httpdate
212
+ get action
213
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'Last-Modified')
214
+ end
215
+
216
+ # Goes through a http cache process:
217
+ #
218
+ # 1. Request an action
219
+ # 2. Get a '200 OK' status
220
+ # 3. Request the same action with a valid ETAG
221
+ # 4. Get a '304 Not Modified' status
222
+ # 5. Request the same action with an invalid IF_NONE_MATCH
223
+ # 6. Get a '200 OK' status
224
+ #
225
+ def etag_http_cache(action, variable)
226
+ get action
227
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'ETag', etag_for(variable))
228
+ reset!
229
+
230
+ @request.env['HTTP_IF_NONE_MATCH'] = etag_for(variable)
231
+ get action
232
+ assert_headers('304 Not Modified', 'private, max-age=0, must-revalidate', 'ETag', etag_for(variable))
233
+ reset!
234
+
235
+ @request.env['HTTP_IF_NONE_MATCH'] = 'INVALID'
236
+ get action
237
+ assert_headers('200 OK', 'private, max-age=0, must-revalidate', 'ETag', etag_for(variable))
238
+ end
239
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: josevalim-easy_http_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: "2.0"
5
+ platform: ruby
6
+ authors:
7
+ - "Jos\xC3\xA9 Valim"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-29 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Allows Rails applications to use HTTP cache specifications easily.
17
+ email: jose.valim@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - MIT-LICENSE
26
+ - README
27
+ - Rakefile
28
+ - init.rb
29
+ - lib/easy_http_cache.rb
30
+ - test/easy_http_cache_test.rb
31
+ has_rdoc: true
32
+ homepage: http://github.com/josevalim/easy_http_cache
33
+ post_install_message:
34
+ rdoc_options:
35
+ - --main
36
+ - README
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.2.0
55
+ signing_key:
56
+ specification_version: 2
57
+ summary: Allows Rails applications to use HTTP cache specifications easily.
58
+ test_files:
59
+ - test/easy_http_cache_test.rb