├── Gemfile
├── lib
├── actionpack
│ ├── action_caching.rb
│ └── action_caching
│ │ └── railtie.rb
└── action_controller
│ ├── action_caching.rb
│ └── caching
│ └── actions.rb
├── .gitignore
├── test
├── fixtures
│ └── layouts
│ │ └── talk_from_action.html.erb
├── abstract_unit.rb
└── caching_test.rb
├── .codeclimate.yml
├── gemfiles
├── Gemfile-5-0-stable
├── Gemfile-5-1-stable
├── Gemfile-5-2-stable
├── Gemfile-6-0-stable
├── Gemfile-4-0-stable
├── Gemfile-4-1-stable
├── Gemfile-edge
└── Gemfile-4-2-stable
├── Rakefile
├── actionpack-action_caching.gemspec
├── LICENSE.txt
├── CHANGELOG.md
├── .rubocop.yml
├── .travis.yml
└── README.md
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | gem "rails"
6 |
--------------------------------------------------------------------------------
/lib/actionpack/action_caching.rb:
--------------------------------------------------------------------------------
1 | require "actionpack/action_caching/railtie"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .ruby-version
2 | Gemfile.lock
3 | gemfiles/*.lock
4 | pkg/*
5 | test/tmp
6 |
--------------------------------------------------------------------------------
/test/fixtures/layouts/talk_from_action.html.erb:
--------------------------------------------------------------------------------
1 |
<%= params[:title] %>
2 | <%= yield -%>
3 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | engines:
2 | rubocop:
3 | enabled: true
4 |
5 | ratings:
6 | paths:
7 | - "**.rb"
8 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-5-0-stable:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "rails", github: "rails/rails", branch: "5-0-stable"
6 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-5-1-stable:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "rails", github: "rails/rails", branch: "5-1-stable"
6 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-5-2-stable:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "rails", github: "rails/rails", branch: "5-2-stable"
6 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-6-0-stable:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "rails", github: "rails/rails", branch: "6-0-stable"
6 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-4-0-stable:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "rails", github: "rails/rails", branch: "4-0-stable"
6 | gem "mime-types", "< 3"
7 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-4-1-stable:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "rails", github: "rails/rails", branch: "4-1-stable"
6 | gem "mime-types", "< 3"
7 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-edge:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "rails", github: "rails/rails", branch: "master"
6 | gem "arel", github: "rails/arel", branch: "master"
7 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | require "bundler/gem_tasks"
3 | require "rake/testtask"
4 |
5 | Rake::TestTask.new do |t|
6 | t.libs = ["test"]
7 | t.pattern = "test/**/*_test.rb"
8 | t.ruby_opts = ["-w"]
9 | end
10 |
11 | task default: :test
12 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-4-2-stable:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "rails", github: "rails/rails", branch: "4-2-stable"
6 | gem "mime-types", "< 3"
7 |
8 | if RUBY_VERSION < "2.1"
9 | gem "nokogiri", "< 1.7"
10 | end
11 |
--------------------------------------------------------------------------------
/lib/action_controller/action_caching.rb:
--------------------------------------------------------------------------------
1 | require "action_controller/caching/actions"
2 |
3 | module ActionController
4 | module Caching
5 | eager_autoload do
6 | autoload :Actions
7 | end
8 |
9 | include Actions
10 | end
11 | end
12 |
13 | ActionController::Base.send(:include, ActionController::Caching::Actions)
14 |
--------------------------------------------------------------------------------
/test/abstract_unit.rb:
--------------------------------------------------------------------------------
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.expand_path("../fixtures", __FILE__)
8 |
9 | if ActiveSupport.respond_to?(:test_order)
10 | ActiveSupport.test_order = :random
11 | end
12 |
--------------------------------------------------------------------------------
/lib/actionpack/action_caching/railtie.rb:
--------------------------------------------------------------------------------
1 | require "rails/railtie"
2 |
3 | module ActionPack
4 | module ActionCaching
5 | class Railtie < Rails::Railtie
6 | initializer "action_pack.action_caching" do
7 | ActiveSupport.on_load(:action_controller) do
8 | require "action_controller/action_caching"
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/actionpack-action_caching.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |gem|
2 | gem.name = "actionpack-action_caching"
3 | gem.version = "1.2.0"
4 | gem.author = "David Heinemeier Hansson"
5 | gem.email = "david@loudthinking.com"
6 | gem.description = "Action caching for Action Pack (removed from core in Rails 4.0)"
7 | gem.summary = "Action caching for Action Pack (removed from core in Rails 4.0)"
8 | gem.homepage = "https://github.com/rails/actionpack-action_caching"
9 |
10 | gem.required_ruby_version = ">= 1.9.3"
11 | gem.files = `git ls-files`.split($/)
12 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14 | gem.require_paths = ["lib"]
15 | gem.license = "MIT"
16 |
17 | gem.add_dependency "actionpack", ">= 4.0.0"
18 |
19 | gem.add_development_dependency "mocha"
20 | gem.add_development_dependency "activerecord", ">= 4.0.0"
21 | end
22 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
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.
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.2.0 (January 23, 2017)
2 |
3 | * Support proc options with zero arguments
4 |
5 | Fixes #40.
6 |
7 | *Andrew White*
8 |
9 | * The options `:layout` and `:cache_path` now behave the same when
10 | passed a `Symbol`, `Proc` or object that responds to call.
11 |
12 | *Andrew White*
13 |
14 | * Respect `Accept` header when caching actions
15 |
16 | Fixes #18.
17 |
18 | *Andrew White*
19 |
20 | * Support Rails 4.0, 4.1, 4.2, 5.0 and edge
21 |
22 | *Eileen Uchitelle*, *Andrew White*
23 |
24 | * Call `to_s` on the extension as it may be a symbol
25 |
26 | Fixes #10.
27 |
28 | *Andrew White*
29 |
30 |
31 | ## 1.1.1 (January 2, 2014)
32 |
33 | * Fix load order problem with other gems
34 |
35 | *Andrew White*
36 |
37 |
38 | ## 1.1.0 (November 1, 2013)
39 |
40 | * Allow to use non-proc object in `cache_path` option. You can pass an object that
41 | responds to a `call` method.
42 |
43 | Example:
44 |
45 | class CachePath
46 | def call(controller)
47 | controller.id
48 | end
49 | end
50 |
51 | class TestController < ApplicationController
52 | caches_action :index, :cache_path => CachePath.new
53 | def index; end
54 | end
55 |
56 | *Piotr Niełacny*
57 |
58 |
59 | ## 1.0.0 (February 28, 2013)
60 |
61 | * Extract Action Pack - Action Caching from Rails core.
62 |
63 | *Francesco Rodriguez*
64 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.2
3 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
4 | # to ignore them, so only the ones explicitly set in this file are enabled.
5 | DisabledByDefault: true
6 |
7 | # Prefer &&/|| over and/or.
8 | Style/AndOr:
9 | Enabled: true
10 |
11 | # Do not use braces for hash literals when they are the last argument of a
12 | # method call.
13 | Style/BracesAroundHashParameters:
14 | Enabled: true
15 |
16 | # Align `when` with `case`.
17 | Style/CaseIndentation:
18 | Enabled: true
19 |
20 | # Align comments with method definitions.
21 | Style/CommentIndentation:
22 | Enabled: true
23 |
24 | # No extra empty lines.
25 | Style/EmptyLines:
26 | Enabled: true
27 |
28 | # In a regular class definition, no empty lines around the body.
29 | Style/EmptyLinesAroundClassBody:
30 | Enabled: true
31 |
32 | # In a regular module definition, no empty lines around the body.
33 | Style/EmptyLinesAroundModuleBody:
34 | Enabled: true
35 |
36 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
37 | Style/HashSyntax:
38 | Enabled: true
39 |
40 | # Method definitions after `private` or `protected` isolated calls need one
41 | # extra level of indentation.
42 | Style/IndentationConsistency:
43 | Enabled: true
44 | EnforcedStyle: rails
45 |
46 | # Two spaces, no tabs (for indentation).
47 | Style/IndentationWidth:
48 | Enabled: true
49 |
50 | Style/SpaceAfterColon:
51 | Enabled: true
52 |
53 | Style/SpaceAfterComma:
54 | Enabled: true
55 |
56 | Style/SpaceAroundEqualsInParameterDefault:
57 | Enabled: true
58 |
59 | Style/SpaceAroundKeyword:
60 | Enabled: true
61 |
62 | Style/SpaceAroundOperators:
63 | Enabled: true
64 |
65 | Style/SpaceBeforeFirstArg:
66 | Enabled: true
67 |
68 | # Defining a method with parameters needs parentheses.
69 | Style/MethodDefParentheses:
70 | Enabled: true
71 |
72 | # Use `foo {}` not `foo{}`.
73 | Style/SpaceBeforeBlockBraces:
74 | Enabled: true
75 |
76 | # Use `foo { bar }` not `foo {bar}`.
77 | Style/SpaceInsideBlockBraces:
78 | Enabled: true
79 |
80 | # Use `{ a: 1 }` not `{a:1}`.
81 | Style/SpaceInsideHashLiteralBraces:
82 | Enabled: true
83 |
84 | Style/SpaceInsideParens:
85 | Enabled: true
86 |
87 | # Check quotes usage according to lint rule below.
88 | Style/StringLiterals:
89 | Enabled: true
90 | EnforcedStyle: double_quotes
91 |
92 | # Detect hard tabs, no hard tabs.
93 | Style/Tab:
94 | Enabled: true
95 |
96 | # Blank lines should not have any spaces.
97 | Style/TrailingBlankLines:
98 | Enabled: true
99 |
100 | # No trailing whitespace.
101 | Style/TrailingWhitespace:
102 | Enabled: true
103 |
104 | # Use quotes for string literals when they are enough.
105 | Style/UnneededPercentQ:
106 | Enabled: true
107 |
108 | # Align `end` with the matching keyword or starting expression except for
109 | # assignments, where it should be aligned with the LHS.
110 | Lint/EndAlignment:
111 | Enabled: true
112 | AlignWith: variable
113 |
114 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
115 | Lint/RequireParentheses:
116 | Enabled: true
117 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | sudo: false
3 |
4 | cache:
5 | bundler: true
6 |
7 | before_install:
8 | - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
9 | - gem install bundler -v '< 2'
10 |
11 | rvm:
12 | - 1.9.3
13 | - 2.0.0
14 | - 2.1.10
15 | - 2.2.10
16 | - 2.3.8
17 | - 2.4.6
18 | - 2.5.5
19 | - 2.6.3
20 |
21 | gemfile:
22 | - Gemfile
23 | - gemfiles/Gemfile-4-0-stable
24 | - gemfiles/Gemfile-4-1-stable
25 | - gemfiles/Gemfile-4-2-stable
26 | - gemfiles/Gemfile-5-0-stable
27 | - gemfiles/Gemfile-5-1-stable
28 | - gemfiles/Gemfile-5-2-stable
29 | - gemfiles/Gemfile-6-0-stable
30 | - gemfiles/Gemfile-edge
31 |
32 | matrix:
33 | allow_failures:
34 | - gemfile: gemfiles/Gemfile-edge
35 | exclude:
36 | - rvm: 1.9.3
37 | gemfile: Gemfile
38 | - rvm: 2.0.0
39 | gemfile: Gemfile
40 | - rvm: 2.1.10
41 | gemfile: Gemfile
42 | - rvm: 1.9.3
43 | gemfile: gemfiles/Gemfile-5-0-stable
44 | - rvm: 1.9.3
45 | gemfile: gemfiles/Gemfile-5-1-stable
46 | - rvm: 1.9.3
47 | gemfile: gemfiles/Gemfile-5-2-stable
48 | - rvm: 1.9.3
49 | gemfile: gemfiles/Gemfile-6-0-stable
50 | - rvm: 2.0.0
51 | gemfile: gemfiles/Gemfile-5-0-stable
52 | - rvm: 2.0.0
53 | gemfile: gemfiles/Gemfile-5-1-stable
54 | - rvm: 2.0.0
55 | gemfile: gemfiles/Gemfile-5-2-stable
56 | - rvm: 2.0.0
57 | gemfile: gemfiles/Gemfile-6-0-stable
58 | - rvm: 2.1.10
59 | gemfile: gemfiles/Gemfile-5-0-stable
60 | - rvm: 2.1.10
61 | gemfile: gemfiles/Gemfile-5-1-stable
62 | - rvm: 2.1.10
63 | gemfile: gemfiles/Gemfile-5-2-stable
64 | - rvm: 2.1.10
65 | gemfile: gemfiles/Gemfile-6-0-stable
66 | - rvm: 2.2.10
67 | gemfile: gemfiles/Gemfile-6-0-stable
68 | - rvm: 2.3.8
69 | gemfile: gemfiles/Gemfile-6-0-stable
70 | - rvm: 2.4.6
71 | gemfile: gemfiles/Gemfile-6-0-stable
72 | - rvm: 1.9.3
73 | gemfile: gemfiles/Gemfile-edge
74 | - rvm: 2.0.0
75 | gemfile: gemfiles/Gemfile-edge
76 | - rvm: 2.1.10
77 | gemfile: gemfiles/Gemfile-edge
78 | - rvm: 2.2.10
79 | gemfile: gemfiles/Gemfile-edge
80 | - rvm: 2.3.8
81 | gemfile: gemfiles/Gemfile-edge
82 | - rvm: 2.4.6
83 | gemfile: gemfiles/Gemfile-edge
84 | - rvm: 2.4.6
85 | gemfile: gemfiles/Gemfile-4-0-stable
86 | - rvm: 2.4.6
87 | gemfile: gemfiles/Gemfile-4-1-stable
88 | - rvm: 2.5.5
89 | gemfile: gemfiles/Gemfile-4-0-stable
90 | - rvm: 2.5.5
91 | gemfile: gemfiles/Gemfile-4-1-stable
92 | - rvm: 2.6.3
93 | gemfile: gemfiles/Gemfile-4-0-stable
94 | - rvm: 2.6.3
95 | gemfile: gemfiles/Gemfile-4-1-stable
96 | - rvm: 2.6.3
97 | gemfile: gemfiles/Gemfile-4-2-stable
98 | - rvm: 2.6.3
99 | gemfile: gemfiles/Gemfile-5-0-stable
100 | - rvm: 2.6.3
101 | gemfile: gemfiles/Gemfile-5-1-stable
102 |
103 | notifications:
104 | email: false
105 | irc:
106 | on_success: change
107 | on_failure: always
108 | channels:
109 | - "irc.freenode.org#rails-contrib"
110 | campfire:
111 | on_success: change
112 | on_failure: always
113 | rooms:
114 | - secure: "WikBuknvGGTx/fNGc4qE+8WK+Glt+H+yZKhHXmavRV2zrN3hC0pTPwuGZhNs\nvkc6N9WKud7un2DtWu1v77BgFhIYjfJTRkmoZ8hoNsoHpe93W/a3s8LU30/l\nzDCKoTrqlHT5hJTmEKpNVqkhfFBPiXRFMgFWALUHiA8Q4Z9BUIc="
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
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 | ```ruby
12 | gem 'actionpack-action_caching'
13 | ```
14 |
15 | And then execute:
16 |
17 | $ bundle
18 |
19 | Or install it yourself as:
20 |
21 | $ gem install actionpack-action_caching
22 |
23 | Usage
24 | -----
25 |
26 | Action caching is similar to page caching by the fact that the entire
27 | output of the response is cached, but unlike page caching, every
28 | request still goes through Action Pack. The key benefit of this is
29 | that filters run before the cache is served, which allows for
30 | authentication and other restrictions on whether someone is allowed
31 | to execute such action.
32 |
33 | ```ruby
34 | class ListsController < ApplicationController
35 | before_action :authenticate, except: :public
36 |
37 | caches_page :public
38 | caches_action :index, :show
39 | end
40 | ```
41 |
42 | In this example, the `public` action doesn't require authentication
43 | so it's possible to use the faster page caching. On the other hand
44 | `index` and `show` require authentication. They can still be cached,
45 | but we need action caching for them.
46 |
47 | Action caching uses fragment caching internally and an around
48 | filter to do the job. The fragment cache is named according to
49 | the host and path of the request. A page that is accessed at
50 | `http://david.example.com/lists/show/1` will result in a fragment named
51 | `david.example.com/lists/show/1`. This allows the cacher to
52 | differentiate between `david.example.com/lists/` and
53 | `jamis.example.com/lists/` -- which is a helpful way of assisting
54 | the subdomain-as-account-key pattern.
55 |
56 | Different representations of the same resource, e.g.
57 | `http://david.example.com/lists` and
58 | `http://david.example.com/lists.xml`
59 | are treated like separate requests and so are cached separately.
60 | Keep in mind when expiring an action cache that
61 | `action: "lists"` is not the same as
62 | `action: "list", format: :xml`.
63 |
64 | You can modify the default action cache path by passing a
65 | `:cache_path` option. This will be passed directly to
66 | `ActionCachePath.new`. This is handy for actions with
67 | multiple possible routes that should be cached differently. If a
68 | proc (or an object that responds to `to_proc`) is given, it is
69 | called with the current controller instance.
70 |
71 | And you can also use `:if` (or `:unless`) to control when the action
72 | should be cached, similar to how you use them with `before_action`.
73 |
74 | As of Rails 3.0, you can also pass `:expires_in` with a time
75 | interval (in seconds) to schedule expiration of the cached item.
76 |
77 | The following example depicts some of the points made above:
78 |
79 | ```ruby
80 | class ListsController < ApplicationController
81 | before_action :authenticate, except: :public
82 |
83 | # simple fragment cache
84 | caches_action :current
85 |
86 | # expire cache after an hour
87 | caches_action :archived, expires_in: 1.hour
88 |
89 | # cache unless it's a JSON request
90 | caches_action :index, unless: -> { request.format.json? }
91 |
92 | # custom cache path
93 | caches_action :show, cache_path: { project: 1 }
94 |
95 | # custom cache path with a proc
96 | caches_action :history, cache_path: -> { request.domain }
97 |
98 | # custom cache path with a symbol
99 | caches_action :feed, cache_path: :user_cache_path
100 |
101 | protected
102 | def user_cache_path
103 | if params[:user_id]
104 | user_list_url(params[:user_id], params[:id])
105 | else
106 | list_url(params[:id])
107 | end
108 | end
109 | end
110 | ```
111 |
112 | If you pass `layout: false`, it will only cache your action
113 | content. That's useful when your layout has dynamic information.
114 |
115 | Note: Both the `:format` param and the `Accept` header are taken
116 | into account when caching the fragment with the `:format` having
117 | precedence. For backwards compatibility when the `Accept` header
118 | indicates a HTML request the fragment is stored without the
119 | extension but if an explicit `"html"` is passed in `:format` then
120 | that _is_ used for storing the fragment.
121 |
122 | Contributing
123 | ------------
124 |
125 | 1. Fork it.
126 | 2. Create your feature branch (`git checkout -b my-new-feature`).
127 | 3. Commit your changes (`git commit -am 'Add some feature'`).
128 | 4. Push to the branch (`git push origin my-new-feature`).
129 | 5. Create a new Pull Request.
130 |
131 | Code Status
132 | -----------
133 |
134 | * [](https://travis-ci.org/rails/actionpack-action_caching)
135 |
--------------------------------------------------------------------------------
/lib/action_controller/caching/actions.rb:
--------------------------------------------------------------------------------
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_action :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 | # http://david.example.com/lists/show/1 will result in a fragment named
28 | # david.example.com/lists/show/1. This allows the cacher to
29 | # differentiate between david.example.com/lists/ and
30 | # jamis.example.com/lists/ -- 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 | # http://david.example.com/lists and
35 | # http://david.example.com/lists.xml
36 | # are treated like separate requests and so are cached separately.
37 | # Keep in mind when expiring an action cache that
38 | # action: "lists" is not the same as
39 | # action: "lists", format: :xml.
40 | #
41 | # You can modify the default action cache path by passing a
42 | # :cache_path option. This will be passed directly to
43 | # ActionCachePath.new. 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 | # If an object that responds to call is given, it'll be called
47 | # with the current controller instance.
48 | #
49 | # And you can also use :if (or :unless) to pass a
50 | # proc that specifies when the action should be cached.
51 | #
52 | # As of Rails 3.0, you can also pass :expires_in with a time
53 | # interval (in seconds) to schedule expiration of the cached item.
54 | #
55 | # The following example depicts some of the points made above:
56 | #
57 | # class CachePathCreator
58 | # def initialize(name)
59 | # @name = name
60 | # end
61 | #
62 | # def call(controller)
63 | # "cache-path-#{@name}"
64 | # end
65 | # end
66 | #
67 | #
68 | # class ListsController < ApplicationController
69 | # before_action :authenticate, except: :public
70 | #
71 | # caches_page :public
72 | #
73 | # caches_action :index, if: Proc.new do
74 | # !request.format.json? # cache if is not a JSON request
75 | # end
76 | #
77 | # caches_action :show, cache_path: { project: 1 },
78 | # expires_in: 1.hour
79 | #
80 | # caches_action :feed, cache_path: Proc.new do
81 | # if params[:user_id]
82 | # user_list_url(params[:user_id, params[:id])
83 | # else
84 | # list_url(params[:id])
85 | # end
86 | # end
87 | #
88 | # caches_action :posts, cache_path: CachePathCreator.new("posts")
89 | # end
90 | #
91 | # If you pass layout: false, it will only cache your action
92 | # content. That's useful when your layout has dynamic information.
93 | #
94 | # Warning: If the format of the request is determined by the Accept HTTP
95 | # header the Content-Type of the cached response could be wrong because
96 | # no information about the MIME type is stored in the cache key. So, if
97 | # you first ask for MIME type M in the Accept header, a cache entry is
98 | # created, and then perform a second request to the same resource asking
99 | # for a different MIME type, you'd get the content cached for M.
100 | #
101 | # The :format parameter is taken into account though. The safest
102 | # way to cache by MIME type is to pass the format in the route.
103 | module Actions
104 | extend ActiveSupport::Concern
105 |
106 | module ClassMethods
107 | # Declares that +actions+ should be cached.
108 | # See ActionController::Caching::Actions for details.
109 | def caches_action(*actions)
110 | return unless cache_configured?
111 | options = actions.extract_options!
112 | options[:layout] = true unless options.key?(:layout)
113 | filter_options = options.extract!(:if, :unless).merge(only: actions)
114 | cache_options = options.extract!(:layout, :cache_path).merge(store_options: options)
115 |
116 | around_action ActionCacheFilter.new(cache_options), filter_options
117 | end
118 | end
119 |
120 | def _save_fragment(name, options)
121 | content = ""
122 | response_body.each do |parts|
123 | content << parts
124 | end
125 |
126 | if caching_allowed?
127 | write_fragment(name, content, options)
128 | else
129 | content
130 | end
131 | end
132 |
133 | def caching_allowed?
134 | (request.get? || request.head?) && response.status == 200
135 | end
136 |
137 | protected
138 | def expire_action(options = {})
139 | return unless cache_configured?
140 |
141 | if options.is_a?(Hash) && options[:action].is_a?(Array)
142 | options[:action].each { |action| expire_action(options.merge(action: action)) }
143 | else
144 | expire_fragment(ActionCachePath.new(self, options, false).path)
145 | end
146 | end
147 |
148 | class ActionCacheFilter # :nodoc:
149 | def initialize(options, &block)
150 | @cache_path, @store_options, @cache_layout =
151 | options.values_at(:cache_path, :store_options, :layout)
152 | end
153 |
154 | def around(controller)
155 | cache_layout = expand_option(controller, @cache_layout)
156 | path_options = expand_option(controller, @cache_path)
157 | cache_path = ActionCachePath.new(controller, path_options || {})
158 |
159 | body = controller.read_fragment(cache_path.path, @store_options)
160 |
161 | unless body
162 | controller.action_has_layout = false unless cache_layout
163 | yield
164 | controller.action_has_layout = true
165 | body = controller._save_fragment(cache_path.path, @store_options)
166 | end
167 |
168 | body = render_to_string(controller, body) unless cache_layout
169 |
170 | controller.response_body = body
171 | controller.content_type = Mime[cache_path.extension || :html]
172 | end
173 |
174 | if ActionPack::VERSION::STRING < "4.1"
175 | def render_to_string(controller, body)
176 | controller.render_to_string(text: body, layout: true)
177 | end
178 | else
179 | def render_to_string(controller, body)
180 | controller.render_to_string(html: body.html_safe, layout: true)
181 | end
182 | end
183 |
184 | private
185 | def expand_option(controller, option)
186 | option = option.to_proc if option.respond_to?(:to_proc)
187 |
188 | if option.is_a?(Proc)
189 | case option.arity
190 | when -2, -1, 1
191 | controller.instance_exec(controller, &option)
192 | when 0
193 | controller.instance_exec(&option)
194 | else
195 | raise ArgumentError, "Invalid proc arity of #{option.arity} - proc options should have an arity of 0 or 1"
196 | end
197 | elsif option.respond_to?(:call)
198 | option.call(controller)
199 | else
200 | option
201 | end
202 | end
203 | end
204 |
205 | class ActionCachePath
206 | attr_reader :path, :extension
207 |
208 | # If +infer_extension+ is +true+, the cache path extension is looked up from the request's
209 | # path and format. This is desirable when reading and writing the cache, but not when
210 | # expiring the cache - +expire_action+ should expire the same files regardless of the
211 | # request format.
212 | def initialize(controller, options = {}, infer_extension = true)
213 | if infer_extension
214 | if controller.params.key?(:format)
215 | @extension = controller.params[:format]
216 | elsif !controller.request.format.html?
217 | @extension = controller.request.format.to_sym
218 | else
219 | @extension = nil
220 | end
221 |
222 | options.reverse_merge!(format: @extension) if options.is_a?(Hash)
223 | end
224 |
225 | path = controller.url_for(options).split("://", 2).last
226 | @path = normalize!(path)
227 | end
228 |
229 | private
230 | def normalize!(path)
231 | ext = URI.parser.escape(extension.to_s) if extension
232 | path << "index" if path[-1] == ?/
233 | path << ".#{ext}" if extension && !path.split("?", 2).first.ends_with?(".#{ext}")
234 | URI.parser.unescape(path)
235 | end
236 | end
237 | end
238 | end
239 | end
240 |
--------------------------------------------------------------------------------
/test/caching_test.rb:
--------------------------------------------------------------------------------
1 | require "abstract_unit"
2 | require "mocha/setup"
3 |
4 | CACHE_DIR = "test_cache"
5 | # Don't change "../tmp" cavalierly or you might hose something you don't want hosed
6 | TEST_TMP_DIR = File.expand_path("../tmp", __FILE__)
7 | FILE_STORE_PATH = File.join(TEST_TMP_DIR, CACHE_DIR)
8 |
9 | class CachingController < ActionController::Base
10 | abstract!
11 |
12 | self.cache_store = :file_store, FILE_STORE_PATH
13 | end
14 |
15 | class CachePath
16 | def call(controller)
17 | ["controller", controller.params[:id]].compact.join("-")
18 | end
19 | end
20 |
21 | class ActionCachingTestController < CachingController
22 | rescue_from(Exception) { head 500 }
23 | rescue_from(ActionController::UnknownFormat) { head :not_acceptable }
24 | if defined? ActiveRecord
25 | rescue_from(ActiveRecord::RecordNotFound) { head :not_found }
26 | end
27 |
28 | self.view_paths = FIXTURE_LOAD_PATH
29 |
30 | before_action only: :with_symbol_format do
31 | request.params[:format] = :json
32 | end
33 |
34 | caches_action :index, :redirected, :forbidden, if: ->(c) { c.request.format && !c.request.format.json? }, expires_in: 1.hour
35 | caches_action :show, cache_path: "http://test.host/custom/show"
36 | caches_action :edit, cache_path: ->(c) { c.params[:id] ? "http://test.host/#{c.params[:id]};edit" : "http://test.host/edit" }
37 | caches_action :custom_cache_path, cache_path: CachePath.new
38 | caches_action :symbol_cache_path, cache_path: :cache_path_protected_method
39 | caches_action :with_layout
40 | caches_action :with_format_and_http_param, cache_path: ->(c) { { key: "value" } }
41 | caches_action :with_symbol_format, cache_path: "http://test.host/action_caching_test/with_symbol_format"
42 | caches_action :not_url_cache_path, cache_path: ->(c) { "#{c.params[:action]}_key" }
43 | caches_action :not_url_cache_path_no_args, cache_path: -> { "#{params[:action]}_key" }
44 | caches_action :layout_false, layout: false
45 | caches_action :with_layout_proc_param, layout: ->(c) { c.params[:layout] != "false" }
46 | caches_action :with_layout_proc_param_no_args, layout: -> { params[:layout] != "false" }
47 | caches_action :record_not_found, :four_oh_four, :simple_runtime_error
48 | caches_action :streaming
49 | caches_action :invalid
50 | caches_action :accept
51 |
52 | layout "talk_from_action"
53 |
54 | def index
55 | @cache_this = CacheContent.to_s
56 | render plain: @cache_this
57 | end
58 |
59 | def redirected
60 | redirect_to action: "index"
61 | end
62 |
63 | def forbidden
64 | render plain: "Forbidden"
65 | response.status = "403 Forbidden"
66 | end
67 |
68 | def with_layout
69 | @cache_this = CacheContent.to_s
70 | render html: @cache_this, layout: true
71 | end
72 |
73 | def with_format_and_http_param
74 | @cache_this = CacheContent.to_s
75 | render plain: @cache_this
76 | end
77 |
78 | def with_symbol_format
79 | @cache_this = CacheContent.to_s
80 | render json: { timestamp: @cache_this }
81 | end
82 |
83 | def not_url_cache_path
84 | render plain: "cache_this"
85 | end
86 | alias_method :not_url_cache_path_no_args, :not_url_cache_path
87 |
88 | def record_not_found
89 | raise ActiveRecord::RecordNotFound, "oops!"
90 | end
91 |
92 | def four_oh_four
93 | render plain: "404'd!", status: 404
94 | end
95 |
96 | def simple_runtime_error
97 | raise "oops!"
98 | end
99 |
100 | alias_method :show, :index
101 | alias_method :edit, :index
102 | alias_method :destroy, :index
103 | alias_method :custom_cache_path, :index
104 | alias_method :symbol_cache_path, :index
105 | alias_method :layout_false, :with_layout
106 | alias_method :with_layout_proc_param, :with_layout
107 | alias_method :with_layout_proc_param_no_args, :with_layout
108 |
109 | def expire
110 | expire_action controller: "action_caching_test", action: "index"
111 | head :ok
112 | end
113 |
114 | def expire_xml
115 | expire_action controller: "action_caching_test", action: "index", format: "xml"
116 | head :ok
117 | end
118 |
119 | def expire_with_url_string
120 | expire_action url_for(controller: "action_caching_test", action: "index")
121 | head :ok
122 | end
123 |
124 | def streaming
125 | render plain: "streaming", stream: true
126 | end
127 |
128 | def invalid
129 | @cache_this = CacheContent.to_s
130 |
131 | respond_to do |format|
132 | format.json { render json: @cache_this }
133 | end
134 | end
135 |
136 | def accept
137 | @cache_this = CacheContent.to_s
138 |
139 | respond_to do |format|
140 | format.html { render html: @cache_this }
141 | format.json { render json: @cache_this }
142 | end
143 | end
144 |
145 | def expire_accept
146 | if params.key?(:format)
147 | expire_action action: "accept", format: params[:format]
148 | elsif !request.format.html?
149 | expire_action action: "accept", format: request.format.to_sym
150 | else
151 | expire_action action: "accept"
152 | end
153 |
154 | head :ok
155 | end
156 |
157 | protected
158 | def cache_path_protected_method
159 | ["controller", params[:id]].compact.join("-")
160 | end
161 |
162 | if ActionPack::VERSION::STRING < "4.1"
163 | def render(options)
164 | if options.key?(:plain)
165 | super({ text: options.delete(:plain) }.merge(options))
166 | response.content_type = "text/plain"
167 | elsif options.key?(:html)
168 | super({ text: options.delete(:html) }.merge(options))
169 | response.content_type = "text/html"
170 | else
171 | super
172 | end
173 | end
174 | end
175 | end
176 |
177 | class CacheContent
178 | def self.to_s
179 | # Let Time spicy to assure that Time.now != Time.now
180 | time = Time.now.to_f + rand
181 | (time.to_s + "").html_safe
182 | end
183 | end
184 |
185 | class ActionCachingMockController
186 | attr_accessor :mock_url_for
187 | attr_accessor :mock_path
188 |
189 | def initialize
190 | yield self if block_given?
191 | end
192 |
193 | def url_for(*args)
194 | @mock_url_for
195 | end
196 |
197 | def params
198 | request.parameters
199 | end
200 |
201 | def request
202 | Object.new.instance_eval <<-EVAL
203 | def path; "#{@mock_path}" end
204 | def format; "all" end
205 | def parameters; { format: nil }; end
206 | self
207 | EVAL
208 | end
209 | end
210 |
211 | class ActionCacheTest < ActionController::TestCase
212 | tests ActionCachingTestController
213 |
214 | def setup
215 | super
216 |
217 | @routes = ActionDispatch::Routing::RouteSet.new
218 |
219 | @request.host = "hostname.com"
220 | FileUtils.mkdir_p(FILE_STORE_PATH)
221 | @path_class = ActionController::Caching::Actions::ActionCachePath
222 | @mock_controller = ActionCachingMockController.new
223 | end
224 |
225 | def teardown
226 | super
227 | FileUtils.rm_rf(File.dirname(FILE_STORE_PATH))
228 | end
229 |
230 | def test_simple_action_cache_with_http_head
231 | draw do
232 | get "/action_caching_test", to: "action_caching_test#index"
233 | end
234 |
235 | head :index
236 | assert_response :success
237 | cached_time = content_to_cache
238 | assert_equal cached_time, @response.body
239 | assert fragment_exist?("hostname.com/action_caching_test")
240 |
241 | head :index
242 | assert_response :success
243 | assert_equal cached_time, @response.body
244 | end
245 |
246 | def test_simple_action_cache
247 | draw do
248 | get "/action_caching_test", to: "action_caching_test#index"
249 | end
250 |
251 | get :index
252 | assert_response :success
253 | cached_time = content_to_cache
254 | assert_equal cached_time, @response.body
255 | assert fragment_exist?("hostname.com/action_caching_test")
256 |
257 | get :index
258 | assert_response :success
259 | assert_equal cached_time, @response.body
260 | end
261 |
262 | def test_simple_action_not_cached
263 | draw do
264 | get "/action_caching_test/destroy", to: "action_caching_test#destroy"
265 | end
266 |
267 | get :destroy
268 | assert_response :success
269 | cached_time = content_to_cache
270 | assert_equal cached_time, @response.body
271 | assert !fragment_exist?("hostname.com/action_caching_test/destroy")
272 |
273 | get :destroy
274 | assert_response :success
275 | assert_not_equal cached_time, @response.body
276 | end
277 |
278 | def test_action_cache_with_layout
279 | draw do
280 | get "/action_caching_test/with_layout", to: "action_caching_test#with_layout"
281 | end
282 |
283 | get :with_layout
284 | assert_response :success
285 | cached_time = content_to_cache
286 | assert_not_equal cached_time, @response.body
287 | assert fragment_exist?("hostname.com/action_caching_test/with_layout")
288 |
289 | get :with_layout
290 | assert_response :success
291 | assert_not_equal cached_time, @response.body
292 | assert_equal @response.body, read_fragment("hostname.com/action_caching_test/with_layout")
293 | end
294 |
295 | def test_action_cache_with_layout_and_layout_cache_false
296 | draw do
297 | get "/action_caching_test/layout_false", to: "action_caching_test#layout_false"
298 | end
299 |
300 | get :layout_false, params: { title: "Request 1" }
301 | assert_response :success
302 | cached_time = content_to_cache
303 | assert_equal "Request 1\n#{cached_time}", @response.body
304 | assert_equal cached_time, read_fragment("hostname.com/action_caching_test/layout_false")
305 |
306 | get :layout_false, params: { title: "Request 2" }
307 | assert_response :success
308 | assert_equal "Request 2\n#{cached_time}", @response.body
309 | assert_equal cached_time, read_fragment("hostname.com/action_caching_test/layout_false")
310 | end
311 |
312 | def test_action_cache_with_layout_and_layout_cache_false_via_proc
313 | draw do
314 | get "/action_caching_test/with_layout_proc_param", to: "action_caching_test#with_layout_proc_param"
315 | end
316 |
317 | get :with_layout_proc_param, params: { title: "Request 1", layout: "false" }
318 | assert_response :success
319 | cached_time = content_to_cache
320 | assert_equal "Request 1\n#{cached_time}", @response.body
321 | assert_equal cached_time, read_fragment("hostname.com/action_caching_test/with_layout_proc_param")
322 |
323 | get :with_layout_proc_param, params: { title: "Request 2", layout: "false" }
324 | assert_response :success
325 | assert_equal "Request 2\n#{cached_time}", @response.body
326 | assert_equal cached_time, read_fragment("hostname.com/action_caching_test/with_layout_proc_param")
327 | end
328 |
329 | def test_action_cache_with_layout_and_layout_cache_true_via_proc
330 | draw do
331 | get "/action_caching_test/with_layout_proc_param", to: "action_caching_test#with_layout_proc_param"
332 | end
333 |
334 | get :with_layout_proc_param, params: { title: "Request 1", layout: "true" }
335 | assert_response :success
336 | cached_time = content_to_cache
337 | assert_equal "Request 1\n#{cached_time}", @response.body
338 | assert_equal "Request 1\n#{cached_time}", read_fragment("hostname.com/action_caching_test/with_layout_proc_param")
339 |
340 | get :with_layout_proc_param, params: { title: "Request 2", layout: "true" }
341 | assert_response :success
342 | assert_equal "Request 1\n#{cached_time}", @response.body
343 | assert_equal "Request 1\n#{cached_time}", read_fragment("hostname.com/action_caching_test/with_layout_proc_param")
344 | end
345 |
346 | def test_action_cache_conditional_options
347 | draw do
348 | get "/action_caching_test", to: "action_caching_test#index"
349 | end
350 |
351 | @request.accept = "application/json"
352 | get :index
353 | assert_response :success
354 | assert !fragment_exist?("hostname.com/action_caching_test")
355 | end
356 |
357 | def test_action_cache_with_format_and_http_param
358 | draw do
359 | get "/action_caching_test/with_format_and_http_param", to: "action_caching_test#with_format_and_http_param"
360 | end
361 |
362 | get :with_format_and_http_param, format: "json"
363 | assert_response :success
364 | assert !fragment_exist?("hostname.com/action_caching_test/with_format_and_http_param.json?key=value.json")
365 | assert fragment_exist?("hostname.com/action_caching_test/with_format_and_http_param.json?key=value")
366 | end
367 |
368 | def test_action_cache_with_symbol_format
369 | draw do
370 | get "/action_caching_test/with_symbol_format", to: "action_caching_test#with_symbol_format"
371 | end
372 |
373 | get :with_symbol_format
374 | assert_response :success
375 | assert !fragment_exist?("test.host/action_caching_test/with_symbol_format")
376 | assert fragment_exist?("test.host/action_caching_test/with_symbol_format.json")
377 | end
378 |
379 | def test_action_cache_not_url_cache_path
380 | draw do
381 | get "/action_caching_test/not_url_cache_path", to: "action_caching_test#not_url_cache_path"
382 | end
383 |
384 | get :not_url_cache_path
385 | assert_response :success
386 | assert !fragment_exist?("test.host/action_caching_test/not_url_cache_path")
387 | assert fragment_exist?("not_url_cache_path_key")
388 | end
389 |
390 | def test_action_cache_with_store_options
391 | draw do
392 | get "/action_caching_test", to: "action_caching_test#index"
393 | end
394 |
395 | CacheContent.expects(:to_s).returns('12345.0').once
396 | @controller.expects(:read_fragment).with("hostname.com/action_caching_test", expires_in: 1.hour).once
397 | @controller.expects(:write_fragment).with("hostname.com/action_caching_test", "12345.0", expires_in: 1.hour).once
398 | get :index
399 | assert_response :success
400 | end
401 |
402 | def test_action_cache_with_custom_cache_path
403 | draw do
404 | get "/action_caching_test/show", to: "action_caching_test#show"
405 | end
406 |
407 | get :show
408 | assert_response :success
409 | cached_time = content_to_cache
410 | assert_equal cached_time, @response.body
411 | assert fragment_exist?("test.host/custom/show")
412 |
413 | get :show
414 | assert_response :success
415 | assert_equal cached_time, @response.body
416 | end
417 |
418 | def test_action_cache_with_custom_cache_path_in_block
419 | draw do
420 | get "/action_caching_test/edit(/:id)", to: "action_caching_test#edit"
421 | end
422 |
423 | get :edit
424 | assert_response :success
425 | assert fragment_exist?("test.host/edit")
426 |
427 | get :edit, params: { id: 1 }
428 | assert_response :success
429 | assert fragment_exist?("test.host/1;edit")
430 | end
431 |
432 | def test_action_cache_with_custom_cache_path_with_custom_object
433 | draw do
434 | get "/action_caching_test/custom_cache_path(/:id)", to: "action_caching_test#custom_cache_path"
435 | end
436 |
437 | get :custom_cache_path
438 | assert_response :success
439 | assert fragment_exist?("controller")
440 |
441 | get :custom_cache_path, params: { id: 1 }
442 | assert_response :success
443 | assert fragment_exist?("controller-1")
444 | end
445 |
446 | def test_action_cache_with_symbol_cache_path
447 | draw do
448 | get "/action_caching_test/symbol_cache_path(/:id)", to: "action_caching_test#symbol_cache_path"
449 | end
450 |
451 | get :symbol_cache_path
452 | assert_response :success
453 | assert fragment_exist?("controller")
454 |
455 | get :symbol_cache_path, params: { id: 1 }
456 | assert_response :success
457 | assert fragment_exist?("controller-1")
458 | end
459 |
460 | def test_cache_expiration
461 | draw do
462 | get "/action_caching_test", to: "action_caching_test#index"
463 | get "/action_caching_test/expire", to: "action_caching_test#expire"
464 | end
465 |
466 | get :index
467 | assert_response :success
468 | cached_time = content_to_cache
469 |
470 | get :index
471 | assert_response :success
472 | assert_equal cached_time, @response.body
473 |
474 | get :expire
475 | assert_response :success
476 |
477 | get :index
478 | assert_response :success
479 | new_cached_time = content_to_cache
480 | assert_not_equal cached_time, @response.body
481 |
482 | get :index
483 | assert_response :success
484 | assert_equal new_cached_time, @response.body
485 | end
486 |
487 | def test_cache_expiration_isnt_affected_by_request_format
488 | draw do
489 | get "/action_caching_test", to: "action_caching_test#index"
490 | get "/action_caching_test/expire", to: "action_caching_test#expire"
491 | end
492 |
493 | get :index
494 | cached_time = content_to_cache
495 |
496 | @request.request_uri = "/action_caching_test/expire.xml"
497 | get :expire, format: :xml
498 | assert_response :success
499 |
500 | get :index
501 | assert_response :success
502 | assert_not_equal cached_time, @response.body
503 | end
504 |
505 | def test_cache_expiration_with_url_string
506 | draw do
507 | get "/action_caching_test", to: "action_caching_test#index"
508 | get "/action_caching_test/expire_with_url_string", to: "action_caching_test#expire_with_url_string"
509 | end
510 |
511 | get :index
512 | cached_time = content_to_cache
513 |
514 | @request.request_uri = "/action_caching_test/expire_with_url_string"
515 | get :expire_with_url_string
516 | assert_response :success
517 |
518 | get :index
519 | assert_response :success
520 | assert_not_equal cached_time, @response.body
521 | end
522 |
523 | def test_cache_is_scoped_by_subdomain
524 | draw do
525 | get "/action_caching_test", to: "action_caching_test#index"
526 | end
527 |
528 | @request.host = "jamis.hostname.com"
529 | get :index
530 | assert_response :success
531 | jamis_cache = content_to_cache
532 |
533 | @request.host = "david.hostname.com"
534 | get :index
535 | assert_response :success
536 | david_cache = content_to_cache
537 | assert_not_equal jamis_cache, @response.body
538 |
539 | @request.host = "jamis.hostname.com"
540 | get :index
541 | assert_response :success
542 | assert_equal jamis_cache, @response.body
543 |
544 | @request.host = "david.hostname.com"
545 | get :index
546 | assert_response :success
547 | assert_equal david_cache, @response.body
548 | end
549 |
550 | def test_redirect_is_not_cached
551 | draw do
552 | get "/action_caching_test", to: "action_caching_test#index"
553 | get "/action_caching_test/redirected", to: "action_caching_test#redirected"
554 | end
555 |
556 | get :redirected
557 | assert_response :redirect
558 | get :redirected
559 | assert_response :redirect
560 | end
561 |
562 | def test_forbidden_is_not_cached
563 | draw do
564 | get "/action_caching_test/forbidden", to: "action_caching_test#forbidden"
565 | end
566 |
567 | get :forbidden
568 | assert_response :forbidden
569 | get :forbidden
570 | assert_response :forbidden
571 | end
572 |
573 | def test_xml_version_of_resource_is_treated_as_different_cache
574 | draw do
575 | get "/action_caching_test/index", to: "action_caching_test#index"
576 | get "/action_caching_test/expire_xml", to: "action_caching_test#expire_xml"
577 | end
578 |
579 | get :index, format: "xml"
580 | assert_response :success
581 | cached_time = content_to_cache
582 | assert_equal cached_time, @response.body
583 | assert fragment_exist?("hostname.com/action_caching_test/index.xml")
584 |
585 | get :index, format: "xml"
586 | assert_response :success
587 | assert_equal cached_time, @response.body
588 | assert_equal "application/xml", @response.content_type
589 |
590 | get :expire_xml
591 | assert_response :success
592 |
593 | get :index, format: "xml"
594 | assert_response :success
595 | assert_not_equal cached_time, @response.body
596 | end
597 |
598 | def test_correct_content_type_is_returned_for_cache_hit
599 | draw do
600 | get "/action_caching_test/index/:id", to: "action_caching_test#index"
601 | end
602 |
603 | # run it twice to cache it the first time
604 | get :index, params: { id: "content-type" }, format: "xml"
605 | get :index, params: { id: "content-type" }, format: "xml"
606 | assert_response :success
607 | assert_equal "application/xml", @response.content_type
608 | end
609 |
610 | def test_correct_content_type_is_returned_for_cache_hit_on_action_with_string_key
611 | draw do
612 | get "/action_caching_test/show", to: "action_caching_test#show"
613 | end
614 |
615 | # run it twice to cache it the first time
616 | get :show, format: "xml"
617 | get :show, format: "xml"
618 | assert_response :success
619 | assert_equal "application/xml", @response.content_type
620 | end
621 |
622 | def test_correct_content_type_is_returned_for_cache_hit_on_action_with_string_key_from_proc
623 | draw do
624 | get "/action_caching_test/edit/:id", to: "action_caching_test#edit"
625 | end
626 |
627 | # run it twice to cache it the first time
628 | get :edit, params: { id: 1 }, format: "xml"
629 | get :edit, params: { id: 1 }, format: "xml"
630 | assert_response :success
631 | assert_equal "application/xml", @response.content_type
632 | end
633 |
634 | def test_empty_path_is_normalized
635 | @mock_controller.mock_url_for = "http://example.org/"
636 | @mock_controller.mock_path = "/"
637 |
638 | assert_equal "example.org/index", @path_class.new(@mock_controller, {}).path
639 | end
640 |
641 | def test_file_extensions
642 | draw do
643 | get "/action_caching_test/index/*id", to: "action_caching_test#index", format: false
644 | end
645 |
646 | get :index, params: { id: "kitten.jpg" }
647 | get :index, params: { id: "kitten.jpg" }
648 |
649 | assert_response :success
650 | end
651 |
652 | if defined? ActiveRecord
653 | def test_record_not_found_returns_404_for_multiple_requests
654 | draw do
655 | get "/action_caching_test/record_not_found", to: "action_caching_test#record_not_found"
656 | end
657 |
658 | get :record_not_found
659 | assert_response 404
660 | get :record_not_found
661 | assert_response 404
662 | end
663 | end
664 |
665 | def test_four_oh_four_returns_404_for_multiple_requests
666 | draw do
667 | get "/action_caching_test/four_oh_four", to: "action_caching_test#four_oh_four"
668 | end
669 |
670 | get :four_oh_four
671 | assert_response 404
672 | get :four_oh_four
673 | assert_response 404
674 | end
675 |
676 | def test_four_oh_four_renders_content
677 | draw do
678 | get "/action_caching_test/four_oh_four", to: "action_caching_test#four_oh_four"
679 | end
680 |
681 | get :four_oh_four
682 | assert_equal "404'd!", @response.body
683 | end
684 |
685 | def test_simple_runtime_error_returns_500_for_multiple_requests
686 | draw do
687 | get "/action_caching_test/simple_runtime_error", to: "action_caching_test#simple_runtime_error"
688 | end
689 |
690 | get :simple_runtime_error
691 | assert_response 500
692 | get :simple_runtime_error
693 | assert_response 500
694 | end
695 |
696 | def test_action_caching_plus_streaming
697 | draw do
698 | get "/action_caching_test/streaming", to: "action_caching_test#streaming"
699 | end
700 |
701 | get :streaming
702 | assert_response :success
703 | assert_match(/streaming/, @response.body)
704 | assert fragment_exist?("hostname.com/action_caching_test/streaming")
705 | end
706 |
707 | def test_invalid_format_returns_not_acceptable
708 | draw do
709 | get "/action_caching_test/invalid", to: "action_caching_test#invalid"
710 | end
711 |
712 | get :invalid, format: "json"
713 | assert_response :success
714 | cached_time = content_to_cache
715 | assert_equal cached_time, @response.body
716 |
717 | assert fragment_exist?("hostname.com/action_caching_test/invalid.json")
718 |
719 | get :invalid, format: "json"
720 | assert_response :success
721 | assert_equal cached_time, @response.body
722 |
723 | get :invalid, format: "xml"
724 | assert_response :not_acceptable
725 |
726 | get :invalid, format: "\xC3\x83"
727 | assert_response :not_acceptable
728 | end
729 |
730 | def test_format_from_accept_header
731 | draw do
732 | get "/action_caching_test/accept", to: "action_caching_test#accept"
733 | get "/action_caching_test/accept/expire", to: "action_caching_test#expire_accept"
734 | end
735 |
736 | # Cache the JSON format
737 | get_json :accept
738 | json_cached_time = content_to_cache
739 | assert_cached json_cached_time, "application/json"
740 |
741 | # Check that the JSON format is cached
742 | get_json :accept
743 | assert_cached json_cached_time, "application/json"
744 |
745 | # Cache the HTML format
746 | get_html :accept
747 | html_cached_time = content_to_cache
748 | assert_cached html_cached_time
749 |
750 | # Check that it's not the JSON format
751 | assert_not_equal json_cached_time, @response.body
752 |
753 | # Check that the HTML format is cached
754 | get_html :accept
755 | assert_cached html_cached_time
756 |
757 | # Check that the JSON format is still cached
758 | get_json :accept
759 | assert_cached json_cached_time, "application/json"
760 |
761 | # Expire the JSON format
762 | get_json :expire_accept
763 | assert_response :success
764 |
765 | # Check that the HTML format is still cached
766 | get_html :accept
767 | assert_cached html_cached_time
768 |
769 | # Check the JSON format was expired
770 | get_json :accept
771 | new_json_cached_time = content_to_cache
772 | assert_cached new_json_cached_time, "application/json"
773 | assert_not_equal json_cached_time, @response.body
774 |
775 | # Expire the HTML format
776 | get_html :expire_accept
777 | assert_response :success
778 |
779 | # Check that the JSON format is still cached
780 | get_json :accept
781 | assert_cached new_json_cached_time, "application/json"
782 |
783 | # Check the HTML format was expired
784 | get_html :accept
785 | new_html_cached_time = content_to_cache
786 | assert_cached new_html_cached_time
787 | assert_not_equal html_cached_time, @response.body
788 | end
789 |
790 | def test_explicit_html_format_is_used_for_fragment_path
791 | draw do
792 | get "/action_caching_test/accept", to: "action_caching_test#accept"
793 | get "/action_caching_test/accept/expire", to: "action_caching_test#expire_accept"
794 | end
795 |
796 | get :accept, format: "html"
797 | cached_time = content_to_cache
798 | assert_cached cached_time
799 |
800 | assert fragment_exist?("hostname.com/action_caching_test/accept.html")
801 |
802 | get :accept, format: "html"
803 | cached_time = content_to_cache
804 | assert_cached cached_time
805 |
806 | get :expire_accept, format: "html"
807 | assert_response :success
808 |
809 | assert !fragment_exist?("hostname.com/action_caching_test/accept.html")
810 |
811 | get :accept, format: "html"
812 | assert_not_cached cached_time
813 | end
814 |
815 | def test_lambda_arity_with_cache_path
816 | draw do
817 | get "/action_caching_test/not_url_cache_path_no_args", to: "action_caching_test#not_url_cache_path_no_args"
818 | end
819 |
820 | get :not_url_cache_path_no_args
821 | assert_response :success
822 | assert !fragment_exist?("test.host/action_caching_test/not_url_cache_path_no_args")
823 | assert fragment_exist?("not_url_cache_path_no_args_key")
824 | end
825 |
826 | def test_lambda_arity_with_layout
827 | draw do
828 | get "/action_caching_test/with_layout_proc_param_no_args", to: "action_caching_test#with_layout_proc_param_no_args"
829 | end
830 |
831 | get :with_layout_proc_param_no_args, params: { title: "Request 1", layout: "false" }
832 | assert_response :success
833 | cached_time = content_to_cache
834 | assert_equal "Request 1\n#{cached_time}", @response.body
835 | assert_equal cached_time, read_fragment("hostname.com/action_caching_test/with_layout_proc_param_no_args")
836 |
837 | get :with_layout_proc_param_no_args, params: { title: "Request 2", layout: "false" }
838 | assert_response :success
839 | assert_equal "Request 2\n#{cached_time}", @response.body
840 | assert_equal cached_time, read_fragment("hostname.com/action_caching_test/with_layout_proc_param_no_args")
841 | end
842 |
843 | private
844 | def get_html(*args)
845 | @request.accept = "text/html"
846 | get(*args)
847 | end
848 |
849 | def get_json(*args)
850 | @request.accept = "application/json"
851 | get(*args)
852 | end
853 |
854 | def assert_cached(cache_time, content_type = "text/html")
855 | assert_response :success
856 | assert_equal cache_time, @response.body
857 | assert_equal content_type, @response.content_type
858 | end
859 |
860 | def assert_not_cached(cache_time, content_type = "text/html")
861 | assert_response :success
862 | assert_not_equal cache_time, @response.body
863 | assert_equal content_type, @response.content_type
864 | end
865 |
866 | def content_to_cache
867 | @controller.instance_variable_get(:@cache_this)
868 | end
869 |
870 | def fragment_exist?(path)
871 | @controller.fragment_exist?(path)
872 | end
873 |
874 | def read_fragment(path)
875 | @controller.read_fragment(path)
876 | end
877 |
878 | def draw(&block)
879 | @routes = ActionDispatch::Routing::RouteSet.new
880 | @routes.draw(&block)
881 | @controller.extend(@routes.url_helpers)
882 | end
883 |
884 | if ActionPack::VERSION::STRING < "5.0"
885 | def get(action, options = {})
886 | format = options.slice(:format)
887 | params = options[:params] || {}
888 | session = options[:session] || {}
889 | flash = options[:flash] || {}
890 |
891 | super(action, params.merge(format), session, flash)
892 | end
893 | end
894 | end
895 |
--------------------------------------------------------------------------------