├── 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 | * [![Build Status](https://travis-ci.org/rails/actionpack-action_caching.svg?branch=master)](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 | --------------------------------------------------------------------------------