easy_http_cache 2.2

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.
@@ -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