actionpack-action_caching 1.0.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/.gitignore +17 -0
- data/.travis.yml +17 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +115 -0
- data/Rakefile +11 -0
- data/actionpack-action_caching.gemspec +21 -0
- data/lib/action_controller/action_caching.rb +9 -0
- data/lib/action_controller/caching/actions.rb +193 -0
- data/lib/actionpack/action_caching.rb +1 -0
- data/test/abstract_unit.rb +40 -0
- data/test/caching_test.rb +510 -0
- data/test/fixtures/layouts/talk_from_action.erb +2 -0
- metadata +121 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
before_install:
|
3
|
+
- gem install bundler
|
4
|
+
rvm:
|
5
|
+
- 1.9.3
|
6
|
+
notifications:
|
7
|
+
email: false
|
8
|
+
irc:
|
9
|
+
on_success: change
|
10
|
+
on_failure: always
|
11
|
+
channels:
|
12
|
+
- "irc.freenode.org#rails-contrib"
|
13
|
+
campfire:
|
14
|
+
on_success: change
|
15
|
+
on_failure: always
|
16
|
+
rooms:
|
17
|
+
- secure: "WikBuknvGGTx/fNGc4qE+8WK+Glt+H+yZKhHXmavRV2zrN3hC0pTPwuGZhNs\nvkc6N9WKud7un2DtWu1v77BgFhIYjfJTRkmoZ8hoNsoHpe93W/a3s8LU30/l\nzDCKoTrqlHT5hJTmEKpNVqkhfFBPiXRFMgFWALUHiA8Q4Z9BUIc="
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 David Heinemeier Hansson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
actionpack-action_caching
|
2
|
+
=========================
|
3
|
+
|
4
|
+
Action caching for Action Pack (removed from core in Rails 4.0)
|
5
|
+
|
6
|
+
Installation
|
7
|
+
------------
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'actionpack-action_caching'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install actionpack-action_caching
|
20
|
+
|
21
|
+
Usage
|
22
|
+
-----
|
23
|
+
|
24
|
+
Action caching is similar to page caching by the fact that the entire
|
25
|
+
output of the response is cached, but unlike page caching, every
|
26
|
+
request still goes through Action Pack. The key benefit of this is
|
27
|
+
that filters run before the cache is served, which allows for
|
28
|
+
authentication and other restrictions on whether someone is allowed
|
29
|
+
to execute such action.
|
30
|
+
|
31
|
+
class ListsController < ApplicationController
|
32
|
+
before_filter :authenticate, except: :public
|
33
|
+
|
34
|
+
caches_page :public
|
35
|
+
caches_action :index, :show
|
36
|
+
end
|
37
|
+
|
38
|
+
In this example, the `public` action doesn't require authentication
|
39
|
+
so it's possible to use the faster page caching. On the other hand
|
40
|
+
`index` and `show` require authentication. They can still be cached,
|
41
|
+
but we need action caching for them.
|
42
|
+
|
43
|
+
Action caching uses fragment caching internally and an around
|
44
|
+
filter to do the job. The fragment cache is named according to
|
45
|
+
the host and path of the request. A page that is accessed at
|
46
|
+
`http://david.example.com/lists/show/1` will result in a fragment named
|
47
|
+
`david.example.com/lists/show/1`. This allows the cacher to
|
48
|
+
differentiate between `david.example.com/lists/` and
|
49
|
+
`jamis.example.com/lists/` -- which is a helpful way of assisting
|
50
|
+
the subdomain-as-account-key pattern.
|
51
|
+
|
52
|
+
Different representations of the same resource, e.g.
|
53
|
+
`http://david.example.com/lists` and
|
54
|
+
`http://david.example.com/lists.xml`
|
55
|
+
are treated like separate requests and so are cached separately.
|
56
|
+
Keep in mind when expiring an action cache that
|
57
|
+
`action: 'lists'` is not the same as
|
58
|
+
`action: 'list', format: :xml`.
|
59
|
+
|
60
|
+
You can modify the default action cache path by passing a
|
61
|
+
`:cache_path` option. This will be passed directly to
|
62
|
+
`ActionCachePath.new`. This is handy for actions with
|
63
|
+
multiple possible routes that should be cached differently. If a
|
64
|
+
block is given, it is called with the current controller instance.
|
65
|
+
|
66
|
+
And you can also use `:if` (or `:unless`) to pass a
|
67
|
+
proc that specifies when the action should be cached.
|
68
|
+
|
69
|
+
As of Rails 3.0, you can also pass `:expires_in` with a time
|
70
|
+
interval (in seconds) to schedule expiration of the cached item.
|
71
|
+
|
72
|
+
The following example depicts some of the points made above:
|
73
|
+
|
74
|
+
class ListsController < ApplicationController
|
75
|
+
before_filter :authenticate, except: :public
|
76
|
+
|
77
|
+
caches_page :public
|
78
|
+
|
79
|
+
caches_action :index, if: Proc.new do
|
80
|
+
!request.format.json? # cache if is not a JSON request
|
81
|
+
end
|
82
|
+
|
83
|
+
caches_action :show, cache_path: { project: 1 },
|
84
|
+
expires_in: 1.hour
|
85
|
+
|
86
|
+
caches_action :feed, cache_path: Proc.new do
|
87
|
+
if params[:user_id]
|
88
|
+
user_list_url(params[:user_id, params[:id])
|
89
|
+
else
|
90
|
+
list_url(params[:id])
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
If you pass `layout: false`, it will only cache your action
|
96
|
+
content. That's useful when your layout has dynamic information.
|
97
|
+
|
98
|
+
Warning: If the format of the request is determined by the Accept HTTP
|
99
|
+
header the Content-Type of the cached response could be wrong because
|
100
|
+
no information about the MIME type is stored in the cache key. So, if
|
101
|
+
you first ask for MIME type M in the Accept header, a cache entry is
|
102
|
+
created, and then perform a second request to the same resource asking
|
103
|
+
for a different MIME type, you'd get the content cached for M.
|
104
|
+
|
105
|
+
The `:format` parameter is taken into account though. The safest
|
106
|
+
way to cache by MIME type is to pass the format in the route.
|
107
|
+
|
108
|
+
Contributing
|
109
|
+
------------
|
110
|
+
|
111
|
+
1. Fork it.
|
112
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
113
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
114
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
115
|
+
5. Create a new Pull Request.
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = 'actionpack-action_caching'
|
5
|
+
gem.version = '1.0.0'
|
6
|
+
gem.author = 'David Heinemeier Hansson'
|
7
|
+
gem.email = 'david@loudthinking.com'
|
8
|
+
gem.description = 'Action caching for Action Pack (removed from core in Rails 4.0)'
|
9
|
+
gem.summary = 'Action caching for Action Pack (removed from core in Rails 4.0)'
|
10
|
+
gem.homepage = 'https://github.com/rails/actionpack-action_caching'
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($/)
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.require_paths = ['lib']
|
16
|
+
|
17
|
+
gem.add_dependency 'actionpack', '>= 4.0.0.beta', '< 5.0'
|
18
|
+
|
19
|
+
gem.add_development_dependency 'mocha'
|
20
|
+
gem.add_development_dependency 'activerecord', '>= 4.0.0.beta', '< 5.0'
|
21
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ActionController
|
4
|
+
module Caching
|
5
|
+
# Action caching is similar to page caching by the fact that the entire
|
6
|
+
# output of the response is cached, but unlike page caching, every
|
7
|
+
# request still goes through Action Pack. The key benefit of this is
|
8
|
+
# that filters run before the cache is served, which allows for
|
9
|
+
# authentication and other restrictions on whether someone is allowed
|
10
|
+
# to execute such action.
|
11
|
+
#
|
12
|
+
# class ListsController < ApplicationController
|
13
|
+
# before_filter :authenticate, except: :public
|
14
|
+
#
|
15
|
+
# caches_page :public
|
16
|
+
# caches_action :index, :show
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# In this example, the +public+ action doesn't require authentication
|
20
|
+
# so it's possible to use the faster page caching. On the other hand
|
21
|
+
# +index+ and +show+ require authentication. They can still be cached,
|
22
|
+
# but we need action caching for them.
|
23
|
+
#
|
24
|
+
# Action caching uses fragment caching internally and an around
|
25
|
+
# filter to do the job. The fragment cache is named according to
|
26
|
+
# the host and path of the request. A page that is accessed at
|
27
|
+
# <tt>http://david.example.com/lists/show/1</tt> will result in a fragment named
|
28
|
+
# <tt>david.example.com/lists/show/1</tt>. This allows the cacher to
|
29
|
+
# differentiate between <tt>david.example.com/lists/</tt> and
|
30
|
+
# <tt>jamis.example.com/lists/</tt> -- which is a helpful way of assisting
|
31
|
+
# the subdomain-as-account-key pattern.
|
32
|
+
#
|
33
|
+
# Different representations of the same resource, e.g.
|
34
|
+
# <tt>http://david.example.com/lists</tt> and
|
35
|
+
# <tt>http://david.example.com/lists.xml</tt>
|
36
|
+
# are treated like separate requests and so are cached separately.
|
37
|
+
# Keep in mind when expiring an action cache that
|
38
|
+
# <tt>action: 'lists'</tt> is not the same as
|
39
|
+
# <tt>action: 'list', format: :xml</tt>.
|
40
|
+
#
|
41
|
+
# You can modify the default action cache path by passing a
|
42
|
+
# <tt>:cache_path</tt> option. This will be passed directly to
|
43
|
+
# <tt>ActionCachePath.new</tt>. This is handy for actions with
|
44
|
+
# multiple possible routes that should be cached differently. If a
|
45
|
+
# block is given, it is called with the current controller instance.
|
46
|
+
#
|
47
|
+
# And you can also use <tt>:if</tt> (or <tt>:unless</tt>) to pass a
|
48
|
+
# proc that specifies when the action should be cached.
|
49
|
+
#
|
50
|
+
# As of Rails 3.0, you can also pass <tt>:expires_in</tt> with a time
|
51
|
+
# interval (in seconds) to schedule expiration of the cached item.
|
52
|
+
#
|
53
|
+
# The following example depicts some of the points made above:
|
54
|
+
#
|
55
|
+
# class ListsController < ApplicationController
|
56
|
+
# before_filter :authenticate, except: :public
|
57
|
+
#
|
58
|
+
# caches_page :public
|
59
|
+
#
|
60
|
+
# caches_action :index, if: Proc.new do
|
61
|
+
# !request.format.json? # cache if is not a JSON request
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# caches_action :show, cache_path: { project: 1 },
|
65
|
+
# expires_in: 1.hour
|
66
|
+
#
|
67
|
+
# caches_action :feed, cache_path: Proc.new do
|
68
|
+
# if params[:user_id]
|
69
|
+
# user_list_url(params[:user_id, params[:id])
|
70
|
+
# else
|
71
|
+
# list_url(params[:id])
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# If you pass <tt>layout: false</tt>, it will only cache your action
|
77
|
+
# content. That's useful when your layout has dynamic information.
|
78
|
+
#
|
79
|
+
# Warning: If the format of the request is determined by the Accept HTTP
|
80
|
+
# header the Content-Type of the cached response could be wrong because
|
81
|
+
# no information about the MIME type is stored in the cache key. So, if
|
82
|
+
# you first ask for MIME type M in the Accept header, a cache entry is
|
83
|
+
# created, and then perform a second request to the same resource asking
|
84
|
+
# for a different MIME type, you'd get the content cached for M.
|
85
|
+
#
|
86
|
+
# The <tt>:format</tt> parameter is taken into account though. The safest
|
87
|
+
# way to cache by MIME type is to pass the format in the route.
|
88
|
+
module Actions
|
89
|
+
extend ActiveSupport::Concern
|
90
|
+
|
91
|
+
module ClassMethods
|
92
|
+
# Declares that +actions+ should be cached.
|
93
|
+
# See ActionController::Caching::Actions for details.
|
94
|
+
def caches_action(*actions)
|
95
|
+
return unless cache_configured?
|
96
|
+
options = actions.extract_options!
|
97
|
+
options[:layout] = true unless options.key?(:layout)
|
98
|
+
filter_options = options.extract!(:if, :unless).merge(only: actions)
|
99
|
+
cache_options = options.extract!(:layout, :cache_path).merge(store_options: options)
|
100
|
+
|
101
|
+
around_filter ActionCacheFilter.new(cache_options), filter_options
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def _save_fragment(name, options)
|
106
|
+
content = ''
|
107
|
+
response_body.each do |parts|
|
108
|
+
content << parts
|
109
|
+
end
|
110
|
+
|
111
|
+
if caching_allowed?
|
112
|
+
write_fragment(name, content, options)
|
113
|
+
else
|
114
|
+
content
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def caching_allowed?
|
119
|
+
(request.get? || request.head?) && response.status == 200
|
120
|
+
end
|
121
|
+
|
122
|
+
protected
|
123
|
+
def expire_action(options = {})
|
124
|
+
return unless cache_configured?
|
125
|
+
|
126
|
+
if options.is_a?(Hash) && options[:action].is_a?(Array)
|
127
|
+
options[:action].each { |action| expire_action(options.merge(action: action)) }
|
128
|
+
else
|
129
|
+
expire_fragment(ActionCachePath.new(self, options, false).path)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class ActionCacheFilter # :nodoc:
|
134
|
+
def initialize(options, &block)
|
135
|
+
@cache_path, @store_options, @cache_layout =
|
136
|
+
options.values_at(:cache_path, :store_options, :layout)
|
137
|
+
end
|
138
|
+
|
139
|
+
def around(controller)
|
140
|
+
cache_layout = @cache_layout.respond_to?(:call) ? @cache_layout.call(controller) : @cache_layout
|
141
|
+
|
142
|
+
path_options = if @cache_path.respond_to?(:call)
|
143
|
+
controller.instance_exec(controller, &@cache_path)
|
144
|
+
else
|
145
|
+
@cache_path
|
146
|
+
end
|
147
|
+
|
148
|
+
cache_path = ActionCachePath.new(controller, path_options || {})
|
149
|
+
|
150
|
+
body = controller.read_fragment(cache_path.path, @store_options)
|
151
|
+
|
152
|
+
unless body
|
153
|
+
controller.action_has_layout = false unless cache_layout
|
154
|
+
yield
|
155
|
+
controller.action_has_layout = true
|
156
|
+
body = controller._save_fragment(cache_path.path, @store_options)
|
157
|
+
end
|
158
|
+
|
159
|
+
body = controller.render_to_string(text: body, layout: true) unless cache_layout
|
160
|
+
|
161
|
+
controller.response_body = body
|
162
|
+
controller.content_type = Mime[cache_path.extension || :html]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class ActionCachePath
|
167
|
+
attr_reader :path, :extension
|
168
|
+
|
169
|
+
# If +infer_extension+ is +true+, the cache path extension is looked up from the request's
|
170
|
+
# path and format. This is desirable when reading and writing the cache, but not when
|
171
|
+
# expiring the cache - +expire_action+ should expire the same files regardless of the
|
172
|
+
# request format.
|
173
|
+
def initialize(controller, options = {}, infer_extension = true)
|
174
|
+
if infer_extension
|
175
|
+
@extension = controller.params[:format]
|
176
|
+
options.reverse_merge!(format: @extension) if options.is_a?(Hash)
|
177
|
+
end
|
178
|
+
|
179
|
+
path = controller.url_for(options).split('://', 2).last
|
180
|
+
@path = normalize!(path)
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
def normalize!(path)
|
185
|
+
ext = URI.parser.escape(extension) if extension
|
186
|
+
path << 'index' if path[-1] == ?/
|
187
|
+
path << ".#{ext}" if extension and !path.split('?', 2).first.ends_with?(".#{ext}")
|
188
|
+
URI.parser.unescape(path)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'action_controller/action_caching'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'action_controller'
|
4
|
+
require 'active_record'
|
5
|
+
require 'action_controller/action_caching'
|
6
|
+
|
7
|
+
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
|
8
|
+
|
9
|
+
SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
|
10
|
+
|
11
|
+
module ActionController
|
12
|
+
class Base
|
13
|
+
include SharedTestRoutes.url_helpers
|
14
|
+
|
15
|
+
self.view_paths = FIXTURE_LOAD_PATH
|
16
|
+
end
|
17
|
+
|
18
|
+
class TestCase
|
19
|
+
def setup
|
20
|
+
@routes = SharedTestRoutes
|
21
|
+
|
22
|
+
@routes.draw do
|
23
|
+
get ':controller(/:action)'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module RackTestUtils
|
30
|
+
def body_to_string(body)
|
31
|
+
if body.respond_to?(:each)
|
32
|
+
str = ''
|
33
|
+
body.each {|s| str << s }
|
34
|
+
str
|
35
|
+
else
|
36
|
+
body
|
37
|
+
end
|
38
|
+
end
|
39
|
+
extend self
|
40
|
+
end
|
@@ -0,0 +1,510 @@
|
|
1
|
+
require 'abstract_unit'
|
2
|
+
|
3
|
+
CACHE_DIR = 'test_cache'
|
4
|
+
# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
|
5
|
+
FILE_STORE_PATH = File.join(File.dirname(__FILE__), '/../temp/', CACHE_DIR)
|
6
|
+
|
7
|
+
class CachingController < ActionController::Base
|
8
|
+
abstract!
|
9
|
+
|
10
|
+
self.cache_store = :file_store, FILE_STORE_PATH
|
11
|
+
end
|
12
|
+
|
13
|
+
class ActionCachingTestController < CachingController
|
14
|
+
rescue_from(Exception) { head 500 }
|
15
|
+
rescue_from(ActionController::UnknownFormat) { head :not_acceptable }
|
16
|
+
if defined? ActiveRecord
|
17
|
+
rescue_from(ActiveRecord::RecordNotFound) { head :not_found }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Eliminate uninitialized ivar warning
|
21
|
+
before_filter { @title = nil }
|
22
|
+
|
23
|
+
caches_action :index, :redirected, :forbidden, if: Proc.new { |c| c.request.format && !c.request.format.json? }, expires_in: 1.hour
|
24
|
+
caches_action :show, cache_path: 'http://test.host/custom/show'
|
25
|
+
caches_action :edit, cache_path: Proc.new { |c| c.params[:id] ? "http://test.host/#{c.params[:id]};edit" : 'http://test.host/edit' }
|
26
|
+
caches_action :with_layout
|
27
|
+
caches_action :with_format_and_http_param, cache_path: Proc.new { |c| { key: 'value' } }
|
28
|
+
caches_action :layout_false, layout: false
|
29
|
+
caches_action :with_layout_proc_param, layout: Proc.new { |c| c.params[:layout] }
|
30
|
+
caches_action :record_not_found, :four_oh_four, :simple_runtime_error
|
31
|
+
caches_action :streaming
|
32
|
+
caches_action :invalid
|
33
|
+
|
34
|
+
layout 'talk_from_action'
|
35
|
+
|
36
|
+
def index
|
37
|
+
@cache_this = MockTime.now.to_f.to_s
|
38
|
+
render text: @cache_this
|
39
|
+
end
|
40
|
+
|
41
|
+
def redirected
|
42
|
+
redirect_to action: 'index'
|
43
|
+
end
|
44
|
+
|
45
|
+
def forbidden
|
46
|
+
render text: 'Forbidden'
|
47
|
+
response.status = '403 Forbidden'
|
48
|
+
end
|
49
|
+
|
50
|
+
def with_layout
|
51
|
+
@cache_this = MockTime.now.to_f.to_s
|
52
|
+
@title = nil
|
53
|
+
render text: @cache_this, layout: true
|
54
|
+
end
|
55
|
+
|
56
|
+
def with_format_and_http_param
|
57
|
+
@cache_this = MockTime.now.to_f.to_s
|
58
|
+
render text: @cache_this
|
59
|
+
end
|
60
|
+
|
61
|
+
def record_not_found
|
62
|
+
raise ActiveRecord::RecordNotFound, 'oops!'
|
63
|
+
end
|
64
|
+
|
65
|
+
def four_oh_four
|
66
|
+
render text: "404'd!", status: 404
|
67
|
+
end
|
68
|
+
|
69
|
+
def simple_runtime_error
|
70
|
+
raise 'oops!'
|
71
|
+
end
|
72
|
+
|
73
|
+
alias_method :show, :index
|
74
|
+
alias_method :edit, :index
|
75
|
+
alias_method :destroy, :index
|
76
|
+
alias_method :layout_false, :with_layout
|
77
|
+
alias_method :with_layout_proc_param, :with_layout
|
78
|
+
|
79
|
+
def expire
|
80
|
+
expire_action controller: 'action_caching_test', action: 'index'
|
81
|
+
render nothing: true
|
82
|
+
end
|
83
|
+
|
84
|
+
def expire_xml
|
85
|
+
expire_action controller: 'action_caching_test', action: 'index', format: 'xml'
|
86
|
+
render nothing: true
|
87
|
+
end
|
88
|
+
|
89
|
+
def expire_with_url_string
|
90
|
+
expire_action url_for(controller: 'action_caching_test', action: 'index')
|
91
|
+
render nothing: true
|
92
|
+
end
|
93
|
+
|
94
|
+
def streaming
|
95
|
+
render text: 'streaming', stream: true
|
96
|
+
end
|
97
|
+
|
98
|
+
def invalid
|
99
|
+
@cache_this = MockTime.now.to_f.to_s
|
100
|
+
|
101
|
+
respond_to do |format|
|
102
|
+
format.json{ render json: @cache_this }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class MockTime < Time
|
108
|
+
# Let Time spicy to assure that Time.now != Time.now
|
109
|
+
def to_f
|
110
|
+
super+rand
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class ActionCachingMockController
|
115
|
+
attr_accessor :mock_url_for
|
116
|
+
attr_accessor :mock_path
|
117
|
+
|
118
|
+
def initialize
|
119
|
+
yield self if block_given?
|
120
|
+
end
|
121
|
+
|
122
|
+
def url_for(*args)
|
123
|
+
@mock_url_for
|
124
|
+
end
|
125
|
+
|
126
|
+
def params
|
127
|
+
request.parameters
|
128
|
+
end
|
129
|
+
|
130
|
+
def request
|
131
|
+
Object.new.instance_eval(<<-EVAL)
|
132
|
+
def path; '#{@mock_path}' end
|
133
|
+
def format; 'all' end
|
134
|
+
def parameters; { format: nil }; end
|
135
|
+
self
|
136
|
+
EVAL
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class ActionCacheTest < ActionController::TestCase
|
141
|
+
tests ActionCachingTestController
|
142
|
+
|
143
|
+
def setup
|
144
|
+
super
|
145
|
+
@request.host = 'hostname.com'
|
146
|
+
FileUtils.mkdir_p(FILE_STORE_PATH)
|
147
|
+
@path_class = ActionController::Caching::Actions::ActionCachePath
|
148
|
+
@mock_controller = ActionCachingMockController.new
|
149
|
+
end
|
150
|
+
|
151
|
+
def teardown
|
152
|
+
super
|
153
|
+
FileUtils.rm_rf(File.dirname(FILE_STORE_PATH))
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_simple_action_cache_with_http_head
|
157
|
+
head :index
|
158
|
+
assert_response :success
|
159
|
+
cached_time = content_to_cache
|
160
|
+
assert_equal cached_time, @response.body
|
161
|
+
assert fragment_exist?('hostname.com/action_caching_test')
|
162
|
+
|
163
|
+
head :index
|
164
|
+
assert_response :success
|
165
|
+
assert_equal cached_time, @response.body
|
166
|
+
end
|
167
|
+
|
168
|
+
def test_simple_action_cache
|
169
|
+
get :index
|
170
|
+
assert_response :success
|
171
|
+
cached_time = content_to_cache
|
172
|
+
assert_equal cached_time, @response.body
|
173
|
+
assert fragment_exist?('hostname.com/action_caching_test')
|
174
|
+
|
175
|
+
get :index
|
176
|
+
assert_response :success
|
177
|
+
assert_equal cached_time, @response.body
|
178
|
+
end
|
179
|
+
|
180
|
+
def test_simple_action_not_cached
|
181
|
+
get :destroy
|
182
|
+
assert_response :success
|
183
|
+
cached_time = content_to_cache
|
184
|
+
assert_equal cached_time, @response.body
|
185
|
+
assert !fragment_exist?('hostname.com/action_caching_test/destroy')
|
186
|
+
|
187
|
+
get :destroy
|
188
|
+
assert_response :success
|
189
|
+
assert_not_equal cached_time, @response.body
|
190
|
+
end
|
191
|
+
|
192
|
+
include RackTestUtils
|
193
|
+
|
194
|
+
def test_action_cache_with_layout
|
195
|
+
get :with_layout
|
196
|
+
assert_response :success
|
197
|
+
cached_time = content_to_cache
|
198
|
+
assert_not_equal cached_time, @response.body
|
199
|
+
assert fragment_exist?('hostname.com/action_caching_test/with_layout')
|
200
|
+
|
201
|
+
get :with_layout
|
202
|
+
assert_response :success
|
203
|
+
assert_not_equal cached_time, @response.body
|
204
|
+
body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout'))
|
205
|
+
assert_equal @response.body, body
|
206
|
+
end
|
207
|
+
|
208
|
+
def test_action_cache_with_layout_and_layout_cache_false
|
209
|
+
get :layout_false
|
210
|
+
assert_response :success
|
211
|
+
cached_time = content_to_cache
|
212
|
+
assert_not_equal cached_time, @response.body
|
213
|
+
assert fragment_exist?('hostname.com/action_caching_test/layout_false')
|
214
|
+
|
215
|
+
get :layout_false
|
216
|
+
assert_response :success
|
217
|
+
assert_not_equal cached_time, @response.body
|
218
|
+
body = body_to_string(read_fragment('hostname.com/action_caching_test/layout_false'))
|
219
|
+
assert_equal cached_time, body
|
220
|
+
end
|
221
|
+
|
222
|
+
def test_action_cache_with_layout_and_layout_cache_false_via_proc
|
223
|
+
get :with_layout_proc_param, layout: false
|
224
|
+
assert_response :success
|
225
|
+
cached_time = content_to_cache
|
226
|
+
assert_not_equal cached_time, @response.body
|
227
|
+
assert fragment_exist?('hostname.com/action_caching_test/with_layout_proc_param')
|
228
|
+
|
229
|
+
get :with_layout_proc_param, layout: false
|
230
|
+
assert_response :success
|
231
|
+
assert_not_equal cached_time, @response.body
|
232
|
+
body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout_proc_param'))
|
233
|
+
assert_equal cached_time, body
|
234
|
+
end
|
235
|
+
|
236
|
+
def test_action_cache_with_layout_and_layout_cache_true_via_proc
|
237
|
+
get :with_layout_proc_param, layout: true
|
238
|
+
assert_response :success
|
239
|
+
cached_time = content_to_cache
|
240
|
+
assert_not_equal cached_time, @response.body
|
241
|
+
assert fragment_exist?('hostname.com/action_caching_test/with_layout_proc_param')
|
242
|
+
|
243
|
+
get :with_layout_proc_param, layout: true
|
244
|
+
assert_response :success
|
245
|
+
assert_not_equal cached_time, @response.body
|
246
|
+
body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout_proc_param'))
|
247
|
+
assert_equal @response.body, body
|
248
|
+
end
|
249
|
+
|
250
|
+
def test_action_cache_conditional_options
|
251
|
+
@request.env['HTTP_ACCEPT'] = 'application/json'
|
252
|
+
get :index
|
253
|
+
assert_response :success
|
254
|
+
assert !fragment_exist?('hostname.com/action_caching_test')
|
255
|
+
end
|
256
|
+
|
257
|
+
def test_action_cache_with_format_and_http_param
|
258
|
+
get :with_format_and_http_param, format: 'json'
|
259
|
+
assert_response :success
|
260
|
+
assert !fragment_exist?('hostname.com/action_caching_test/with_format_and_http_param.json?key=value.json')
|
261
|
+
assert fragment_exist?('hostname.com/action_caching_test/with_format_and_http_param.json?key=value')
|
262
|
+
end
|
263
|
+
|
264
|
+
def test_action_cache_with_store_options
|
265
|
+
MockTime.expects(:now).returns(12345).once
|
266
|
+
@controller.expects(:read_fragment).with('hostname.com/action_caching_test', expires_in: 1.hour).once
|
267
|
+
@controller.expects(:write_fragment).with('hostname.com/action_caching_test', '12345.0', expires_in: 1.hour).once
|
268
|
+
get :index
|
269
|
+
assert_response :success
|
270
|
+
end
|
271
|
+
|
272
|
+
def test_action_cache_with_custom_cache_path
|
273
|
+
get :show
|
274
|
+
assert_response :success
|
275
|
+
cached_time = content_to_cache
|
276
|
+
assert_equal cached_time, @response.body
|
277
|
+
assert fragment_exist?('test.host/custom/show')
|
278
|
+
|
279
|
+
get :show
|
280
|
+
assert_response :success
|
281
|
+
assert_equal cached_time, @response.body
|
282
|
+
end
|
283
|
+
|
284
|
+
def test_action_cache_with_custom_cache_path_in_block
|
285
|
+
get :edit
|
286
|
+
assert_response :success
|
287
|
+
assert fragment_exist?('test.host/edit')
|
288
|
+
|
289
|
+
get :edit, id: 1
|
290
|
+
assert_response :success
|
291
|
+
assert fragment_exist?('test.host/1;edit')
|
292
|
+
end
|
293
|
+
|
294
|
+
def test_cache_expiration
|
295
|
+
get :index
|
296
|
+
assert_response :success
|
297
|
+
cached_time = content_to_cache
|
298
|
+
|
299
|
+
get :index
|
300
|
+
assert_response :success
|
301
|
+
assert_equal cached_time, @response.body
|
302
|
+
|
303
|
+
get :expire
|
304
|
+
assert_response :success
|
305
|
+
|
306
|
+
get :index
|
307
|
+
assert_response :success
|
308
|
+
new_cached_time = content_to_cache
|
309
|
+
assert_not_equal cached_time, @response.body
|
310
|
+
|
311
|
+
get :index
|
312
|
+
assert_response :success
|
313
|
+
assert_equal new_cached_time, @response.body
|
314
|
+
end
|
315
|
+
|
316
|
+
def test_cache_expiration_isnt_affected_by_request_format
|
317
|
+
get :index
|
318
|
+
cached_time = content_to_cache
|
319
|
+
|
320
|
+
@request.request_uri = "/action_caching_test/expire.xml"
|
321
|
+
get :expire, format: :xml
|
322
|
+
assert_response :success
|
323
|
+
|
324
|
+
get :index
|
325
|
+
assert_response :success
|
326
|
+
assert_not_equal cached_time, @response.body
|
327
|
+
end
|
328
|
+
|
329
|
+
def test_cache_expiration_with_url_string
|
330
|
+
get :index
|
331
|
+
cached_time = content_to_cache
|
332
|
+
|
333
|
+
@request.request_uri = "/action_caching_test/expire_with_url_string"
|
334
|
+
get :expire_with_url_string
|
335
|
+
assert_response :success
|
336
|
+
|
337
|
+
get :index
|
338
|
+
assert_response :success
|
339
|
+
assert_not_equal cached_time, @response.body
|
340
|
+
end
|
341
|
+
|
342
|
+
def test_cache_is_scoped_by_subdomain
|
343
|
+
@request.host = 'jamis.hostname.com'
|
344
|
+
get :index
|
345
|
+
assert_response :success
|
346
|
+
jamis_cache = content_to_cache
|
347
|
+
|
348
|
+
@request.host = 'david.hostname.com'
|
349
|
+
get :index
|
350
|
+
assert_response :success
|
351
|
+
david_cache = content_to_cache
|
352
|
+
assert_not_equal jamis_cache, @response.body
|
353
|
+
|
354
|
+
@request.host = 'jamis.hostname.com'
|
355
|
+
get :index
|
356
|
+
assert_response :success
|
357
|
+
assert_equal jamis_cache, @response.body
|
358
|
+
|
359
|
+
@request.host = 'david.hostname.com'
|
360
|
+
get :index
|
361
|
+
assert_response :success
|
362
|
+
assert_equal david_cache, @response.body
|
363
|
+
end
|
364
|
+
|
365
|
+
def test_redirect_is_not_cached
|
366
|
+
get :redirected
|
367
|
+
assert_response :redirect
|
368
|
+
get :redirected
|
369
|
+
assert_response :redirect
|
370
|
+
end
|
371
|
+
|
372
|
+
def test_forbidden_is_not_cached
|
373
|
+
get :forbidden
|
374
|
+
assert_response :forbidden
|
375
|
+
get :forbidden
|
376
|
+
assert_response :forbidden
|
377
|
+
end
|
378
|
+
|
379
|
+
def test_xml_version_of_resource_is_treated_as_different_cache
|
380
|
+
with_routing do |set|
|
381
|
+
set.draw do
|
382
|
+
get ':controller(/:action(.:format))'
|
383
|
+
end
|
384
|
+
|
385
|
+
get :index, format: 'xml'
|
386
|
+
assert_response :success
|
387
|
+
cached_time = content_to_cache
|
388
|
+
assert_equal cached_time, @response.body
|
389
|
+
assert fragment_exist?('hostname.com/action_caching_test/index.xml')
|
390
|
+
|
391
|
+
get :index, format: 'xml'
|
392
|
+
assert_response :success
|
393
|
+
assert_equal cached_time, @response.body
|
394
|
+
assert_equal 'application/xml', @response.content_type
|
395
|
+
|
396
|
+
get :expire_xml
|
397
|
+
assert_response :success
|
398
|
+
|
399
|
+
get :index, format: 'xml'
|
400
|
+
assert_response :success
|
401
|
+
assert_not_equal cached_time, @response.body
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def test_correct_content_type_is_returned_for_cache_hit
|
406
|
+
# run it twice to cache it the first time
|
407
|
+
get :index, id: 'content-type', format: 'xml'
|
408
|
+
get :index, id: 'content-type', format: 'xml'
|
409
|
+
assert_response :success
|
410
|
+
assert_equal 'application/xml', @response.content_type
|
411
|
+
end
|
412
|
+
|
413
|
+
def test_correct_content_type_is_returned_for_cache_hit_on_action_with_string_key
|
414
|
+
# run it twice to cache it the first time
|
415
|
+
get :show, format: 'xml'
|
416
|
+
get :show, format: 'xml'
|
417
|
+
assert_response :success
|
418
|
+
assert_equal 'application/xml', @response.content_type
|
419
|
+
end
|
420
|
+
|
421
|
+
def test_correct_content_type_is_returned_for_cache_hit_on_action_with_string_key_from_proc
|
422
|
+
# run it twice to cache it the first time
|
423
|
+
get :edit, id: 1, format: 'xml'
|
424
|
+
get :edit, id: 1, format: 'xml'
|
425
|
+
assert_response :success
|
426
|
+
assert_equal 'application/xml', @response.content_type
|
427
|
+
end
|
428
|
+
|
429
|
+
def test_empty_path_is_normalized
|
430
|
+
@mock_controller.mock_url_for = 'http://example.org/'
|
431
|
+
@mock_controller.mock_path = '/'
|
432
|
+
|
433
|
+
assert_equal 'example.org/index', @path_class.new(@mock_controller, {}).path
|
434
|
+
end
|
435
|
+
|
436
|
+
def test_file_extensions
|
437
|
+
get :index, id: 'kitten.jpg'
|
438
|
+
get :index, id: 'kitten.jpg'
|
439
|
+
|
440
|
+
assert_response :success
|
441
|
+
end
|
442
|
+
|
443
|
+
if defined? ActiveRecord
|
444
|
+
def test_record_not_found_returns_404_for_multiple_requests
|
445
|
+
get :record_not_found
|
446
|
+
assert_response 404
|
447
|
+
get :record_not_found
|
448
|
+
assert_response 404
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
def test_four_oh_four_returns_404_for_multiple_requests
|
453
|
+
get :four_oh_four
|
454
|
+
assert_response 404
|
455
|
+
get :four_oh_four
|
456
|
+
assert_response 404
|
457
|
+
end
|
458
|
+
|
459
|
+
def test_four_oh_four_renders_content
|
460
|
+
get :four_oh_four
|
461
|
+
assert_equal "404'd!", @response.body
|
462
|
+
end
|
463
|
+
|
464
|
+
def test_simple_runtime_error_returns_500_for_multiple_requests
|
465
|
+
get :simple_runtime_error
|
466
|
+
assert_response 500
|
467
|
+
get :simple_runtime_error
|
468
|
+
assert_response 500
|
469
|
+
end
|
470
|
+
|
471
|
+
def test_action_caching_plus_streaming
|
472
|
+
get :streaming
|
473
|
+
assert_response :success
|
474
|
+
assert_match(/streaming/, @response.body)
|
475
|
+
assert fragment_exist?('hostname.com/action_caching_test/streaming')
|
476
|
+
end
|
477
|
+
|
478
|
+
def test_invalid_format_returns_not_acceptable
|
479
|
+
get :invalid, format: 'json'
|
480
|
+
assert_response :success
|
481
|
+
cached_time = content_to_cache
|
482
|
+
assert_equal cached_time, @response.body
|
483
|
+
|
484
|
+
assert fragment_exist?("hostname.com/action_caching_test/invalid.json")
|
485
|
+
|
486
|
+
get :invalid, format: 'json'
|
487
|
+
assert_response :success
|
488
|
+
assert_equal cached_time, @response.body
|
489
|
+
|
490
|
+
get :invalid, format: 'xml'
|
491
|
+
assert_response :not_acceptable
|
492
|
+
|
493
|
+
get :invalid, format: '\xC3\x83'
|
494
|
+
assert_response :not_acceptable
|
495
|
+
end
|
496
|
+
|
497
|
+
private
|
498
|
+
|
499
|
+
def content_to_cache
|
500
|
+
assigns(:cache_this)
|
501
|
+
end
|
502
|
+
|
503
|
+
def fragment_exist?(path)
|
504
|
+
@controller.fragment_exist?(path)
|
505
|
+
end
|
506
|
+
|
507
|
+
def read_fragment(path)
|
508
|
+
@controller.read_fragment(path)
|
509
|
+
end
|
510
|
+
end
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: actionpack-action_caching
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Heinemeier Hansson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: actionpack
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 4.0.0.beta
|
22
|
+
- - <
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: '5.0'
|
25
|
+
type: :runtime
|
26
|
+
prerelease: false
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 4.0.0.beta
|
33
|
+
- - <
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '5.0'
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: mocha
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: activerecord
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 4.0.0.beta
|
60
|
+
- - <
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '5.0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 4.0.0.beta
|
71
|
+
- - <
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '5.0'
|
74
|
+
description: Action caching for Action Pack (removed from core in Rails 4.0)
|
75
|
+
email: david@loudthinking.com
|
76
|
+
executables: []
|
77
|
+
extensions: []
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- .gitignore
|
81
|
+
- .travis.yml
|
82
|
+
- Gemfile
|
83
|
+
- LICENSE.txt
|
84
|
+
- README.md
|
85
|
+
- Rakefile
|
86
|
+
- actionpack-action_caching.gemspec
|
87
|
+
- lib/action_controller/action_caching.rb
|
88
|
+
- lib/action_controller/caching/actions.rb
|
89
|
+
- lib/actionpack/action_caching.rb
|
90
|
+
- test/abstract_unit.rb
|
91
|
+
- test/caching_test.rb
|
92
|
+
- test/fixtures/layouts/talk_from_action.erb
|
93
|
+
homepage: https://github.com/rails/actionpack-action_caching
|
94
|
+
licenses: []
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 1.8.23
|
114
|
+
signing_key:
|
115
|
+
specification_version: 3
|
116
|
+
summary: Action caching for Action Pack (removed from core in Rails 4.0)
|
117
|
+
test_files:
|
118
|
+
- test/abstract_unit.rb
|
119
|
+
- test/caching_test.rb
|
120
|
+
- test/fixtures/layouts/talk_from_action.erb
|
121
|
+
has_rdoc:
|