josevalim-easy_http_cache 2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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