cache_digests 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,51 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cache_digests (0.1.0)
5
+ actionpack (>= 3.2)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actionpack (3.2.3)
11
+ activemodel (= 3.2.3)
12
+ activesupport (= 3.2.3)
13
+ builder (~> 3.0.0)
14
+ erubis (~> 2.7.0)
15
+ journey (~> 1.0.1)
16
+ rack (~> 1.4.0)
17
+ rack-cache (~> 1.2)
18
+ rack-test (~> 0.6.1)
19
+ sprockets (~> 2.1.2)
20
+ activemodel (3.2.3)
21
+ activesupport (= 3.2.3)
22
+ builder (~> 3.0.0)
23
+ activesupport (3.2.3)
24
+ i18n (~> 0.6)
25
+ multi_json (~> 1.0)
26
+ builder (3.0.0)
27
+ erubis (2.7.0)
28
+ hike (1.2.1)
29
+ i18n (0.6.0)
30
+ journey (1.0.3)
31
+ minitest (2.12.1)
32
+ multi_json (1.3.2)
33
+ rack (1.4.1)
34
+ rack-cache (1.2)
35
+ rack (>= 0.4)
36
+ rack-test (0.6.1)
37
+ rack (>= 1.0)
38
+ rake (0.9.2.2)
39
+ sprockets (2.1.2)
40
+ hike (~> 1.2)
41
+ rack (~> 1.0)
42
+ tilt (~> 1.1, != 1.3.0)
43
+ tilt (1.3.3)
44
+
45
+ PLATFORMS
46
+ ruby
47
+
48
+ DEPENDENCIES
49
+ cache_digests!
50
+ minitest
51
+ rake
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 David Heinemeier Hansson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,139 @@
1
+ Cache Digests
2
+ =============
3
+
4
+ Russian-doll caching schemes are hard to maintain when nested templates are updated. The manual approach works something like this:
5
+
6
+ ```HTML+ERB
7
+ # app/views/projects/show.html.erb
8
+ <% cache [ "v1", project ] do %>
9
+ <h1>All documents</h1>
10
+ <%= render project.documents %>
11
+
12
+ <h1>All todolists</h1>
13
+ <%= render project.todolists %>
14
+ <% end %>
15
+
16
+
17
+ # app/views/documents/_document.html.erb
18
+ <% cache [ "v1", document ] do %>
19
+ My document: <%= document.name %>
20
+
21
+ <%= render document.comments %>
22
+ <% end %>
23
+
24
+
25
+ # app/views/todolists/_todolist.html.erb
26
+ <% cache [ "v1", todolist ] do %>
27
+ My todolist: <%= todolist.name %>
28
+
29
+ <%= render document.comments %>
30
+ <% end %>
31
+
32
+ # app/views/comments/_comment.html.erb
33
+ <% cache [ "v1", comment ] do %>
34
+ My comment: <%= comment.body %>
35
+ <% end %>
36
+ ```
37
+
38
+ Now if I change app/views/comments/_comment.html.erb, I'll be forced to manually track down and bump the other three templates. And there's no visual reference in app/views/projects/show.html.erb that this template even depends on the comment template.
39
+
40
+ That puts a serious cramp in our rocking caching style.
41
+
42
+ Enter Cache Digests: With this plugin, all calls to #cache in the view will automatically append a digest of that template _and_ all of it's dependencies! So you no longer need to manually increment versions in the specific templates you're working on or care about what other templates are depending on the change you make.
43
+
44
+ Our code from above can just look like:
45
+
46
+ ```HTML+ERB
47
+ # app/views/projects/show.html.erb
48
+ <% cache project do %>
49
+ ...
50
+
51
+ # app/views/documents/_document.html.erb
52
+ <% cache document do %>
53
+ ...
54
+
55
+ # app/views/todolists/_todolist.html.erb
56
+ <% cache todolist do %>
57
+ ...
58
+
59
+ # app/views/comments/_comment.html.erb
60
+ <% cache comment do %>
61
+ ...
62
+ ```
63
+
64
+ The caching key for app/views/projects/show.html.erb will be something like `views/projects/605816632-20120810191209/d9fb66b120b61f46707c67ab41d93cb2`. That last bit is a MD5 of the template file itself and all of its dependencies. It'll change if you change either the template or any of the dependencies, and thus allow the cache to expire automatically.
65
+
66
+ You can use these handy rake tasks to see how deep the rabbit hole goes:
67
+
68
+ ```
69
+ $ rake cache_digests:dependencies TEMPLATE=projects/show
70
+ [
71
+ "documents/document",
72
+ "todolists/todolist"
73
+ ]
74
+
75
+
76
+ $ rake cache_digests:nested_dependencies TEMPLATE=projects/show
77
+ [
78
+ {
79
+ "documents/document": [
80
+ "comments/comment"
81
+ ]
82
+ },
83
+ {
84
+ "todolists/todolist": [
85
+ "comments/comment"
86
+ ]
87
+ }
88
+ ]
89
+ ```
90
+
91
+ Implicit dependencies
92
+ ---------------------
93
+
94
+ Most template dependencies can be derived from calls to render in the template itself. Here are some examples of render calls that Cache Digests knows how to decode:
95
+
96
+ ```ruby
97
+ render partial: "comments/comment", collection: commentable.comments
98
+ render "comments/comments"
99
+ render 'comments/comments'
100
+ render('comments/comments')
101
+
102
+ render "header" => render("comments/header")
103
+
104
+ render(@topic) => render("topics/topic")
105
+ render(topics) => render("topics/topic")
106
+ render(message.topics) => render("topics/topic")
107
+ ```
108
+
109
+ It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived:
110
+
111
+ ```ruby
112
+ render group_of_attachments
113
+ render @project.documents.where(published: true).order('created_at')
114
+ ```
115
+
116
+ You will have to rewrite those to the explicit form:
117
+
118
+ ```ruby
119
+ render partial: 'attachments/attachment', collection: group_of_attachments
120
+ render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
121
+ ```
122
+
123
+ Explicit dependencies
124
+ ---------------------
125
+
126
+ Some times you'll have template dependencies that can't be derived at all. This is typically the case when you have template rendering that happens in helpers. Here's an example:
127
+
128
+ ```HTML+ERB
129
+ <%= render_sortable_todolists @project.todolists %>
130
+ ```
131
+
132
+ You'll need to use a special comment format to call those out:
133
+
134
+ ```HTML+ERB
135
+ <%# Template Dependency: todolists/todolist %>
136
+ <%= render_sortable_todolists @project.todolists %>
137
+ ```
138
+
139
+ The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so. You can only declare one template dependency per line.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+
6
+ task :default => :test
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.libs << 'test/lib'
10
+ t.pattern = 'test/*_test.rb'
11
+ end
data/bin/bundle ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby-local-exec
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ require 'rubygems'
4
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'cache_digests'
3
+ s.version = '0.1.0'
4
+ s.author = 'David Heinemeier Hansson'
5
+ s.email = 'david@37signals.com'
6
+ s.summary = 'Nested fragment caches with (even) less situps'
7
+
8
+ s.required_ruby_version = '>= 1.9'
9
+
10
+ s.add_dependency 'actionpack', '>= 3.2'
11
+
12
+ s.add_development_dependency 'rake'
13
+ s.add_development_dependency 'minitest'
14
+
15
+ s.files = Dir["#{File.dirname(__FILE__)}/**/*"]
16
+ end
@@ -0,0 +1,4 @@
1
+ require 'digest/md5'
2
+ require 'cache_digests/template_digestor'
3
+ require 'cache_digests/fragment_helper'
4
+ require 'cache_digests/engine' if defined?(Rails)
@@ -0,0 +1,15 @@
1
+ module CacheDigests
2
+ class Engine < ::Rails::Engine
3
+ initializer 'cache_digests' do |app|
4
+ require 'cache_digests'
5
+
6
+ ActiveSupport.on_load :action_view do
7
+ ActionView::Base.send :include, CacheDigests::FragmentHelper
8
+ end
9
+
10
+ config.to_prepare do
11
+ CacheDigests::TemplateDigestor.logger = Rails.logger
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module CacheDigests
2
+ module FragmentHelper
3
+ private
4
+ # Automatically include this template's digest -- and its childrens' -- in the cache key.
5
+ def fragment_for(key, options = nil, &block)
6
+ if !explicitly_versioned_cache_key?(key)
7
+ super [*key, TemplateDigestor.digest(@virtual_path, formats.last.to_sym, lookup_context)], options, &block
8
+ else
9
+ super
10
+ end
11
+ end
12
+
13
+ def explicitly_versioned_cache_key?(key)
14
+ key.is_a?(Array) && key.first =~ /\Av\d+\Z/
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,104 @@
1
+ require 'active_support/core_ext'
2
+ require 'logger'
3
+
4
+ module CacheDigests
5
+ class TemplateDigestor
6
+ EXPLICIT_DEPENDENCY = /# Template Dependency: ([^ ]+)/
7
+
8
+ # Matches:
9
+ # render partial: "comments/comment", collection: commentable.comments
10
+ # render "comments/comments"
11
+ # render 'comments/comments'
12
+ # render('comments/comments')
13
+ #
14
+ # render(@topic) => render("topics/topic")
15
+ # render(topics) => render("topics/topic")
16
+ # render(message.topics) => render("topics/topic")
17
+ RENDER_DEPENDENCY = /
18
+ render\s? # render, followed by an optional space
19
+ \(? # start a optional parenthesis for the render call
20
+ (partial:)?\s? # naming the partial, used with collection -- 1st capture
21
+ ([@a-z"'][@a-z_\/\."']+) # the template name itself -- 2nd capture
22
+ /x
23
+
24
+ cattr_accessor(:cache) { Hash.new }
25
+ cattr_accessor(:logger, instance_reader: true)
26
+
27
+ def self.digest(name, format, finder, options = {})
28
+ cache["#{name}.#{format}"] ||= new(name, format, finder, options).digest
29
+ end
30
+
31
+ attr_reader :name, :format, :finder, :options
32
+
33
+ def initialize(name, format, finder, options = {})
34
+ @name, @format, @finder, @options = name, format, finder, options
35
+ end
36
+
37
+ def digest
38
+ Digest::MD5.hexdigest("#{name}.#{format}-#{source}-#{dependency_digest}").tap do |digest|
39
+ logger.try :info, "Cache digest for #{name}.#{format}: #{digest}"
40
+ end
41
+ rescue ActionView::MissingTemplate
42
+ logger.try :error, "Couldn't find template for digesting: #{name}.#{format}"
43
+ ''
44
+ end
45
+
46
+ def dependencies
47
+ render_dependencies + explicit_dependencies
48
+ rescue ActionView::MissingTemplate
49
+ [] # File doesn't exist, so no dependencies
50
+ end
51
+
52
+ def nested_dependencies
53
+ dependencies.collect do |dependency|
54
+ dependencies = TemplateDigestor.new(dependency, format, finder, partial: true).nested_dependencies
55
+ dependencies.any? ? { dependency => dependencies } : dependency
56
+ end
57
+ end
58
+
59
+
60
+ private
61
+ def logical_name
62
+ name.gsub(%r|/_|, "/")
63
+ end
64
+
65
+ def directory
66
+ name.split("/").first
67
+ end
68
+
69
+ def partial?
70
+ options[:partial] || name.include?("/_")
71
+ end
72
+
73
+ def source
74
+ @source ||= finder.find(logical_name, [], partial?, formats: [ format ]).source
75
+ end
76
+
77
+
78
+ def dependency_digest
79
+ dependencies.collect do |template_name|
80
+ TemplateDigestor.digest(template_name, format, finder, partial: true)
81
+ end.join("-")
82
+ end
83
+
84
+ def render_dependencies
85
+ source.scan(RENDER_DEPENDENCY).
86
+ collect(&:second).uniq.
87
+
88
+ # render(@topic) => render("topics/topic")
89
+ # render(topics) => render("topics/topic")
90
+ # render(message.topics) => render("topics/topic")
91
+ collect { |name| name.sub(/\A@?([a-z]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }.
92
+
93
+ # render("headline") => render("message/headline")
94
+ collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }.
95
+
96
+ # replace quotes from string renders
97
+ collect { |name| name.gsub(%r|["']|, "") }
98
+ end
99
+
100
+ def explicit_dependencies
101
+ source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,15 @@
1
+ namespace :cache_digests do
2
+ desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
3
+ task :nested_dependencies => :environment do
4
+ template, format = ENV['TEMPLATE'].split(".")
5
+ format ||= :html
6
+ puts JSON.pretty_generate CacheDigests::TemplateDigestor.new(template, format, ApplicationController.new.lookup_context).nested_dependencies
7
+ end
8
+
9
+ desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
10
+ task :dependencies => :environment do
11
+ template, format = ENV['TEMPLATE'].split(".")
12
+ format ||= :html
13
+ puts JSON.pretty_generate CacheDigests::TemplateDigestor.new(template, format, ApplicationController.new.lookup_context).dependencies
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ Great story, bro!
@@ -0,0 +1 @@
1
+ <%= render partial: "comments/comment", collection: commentable.comments %>
File without changes
File without changes
@@ -0,0 +1 @@
1
+ THIS BE WHERE THEM MESSAGE GO, YO!
File without changes
@@ -0,0 +1,2 @@
1
+ <%= render @messages %>
2
+ <%= render @events %>
@@ -0,0 +1,9 @@
1
+ <%# Template Dependency: messages/message %>
2
+ <%= render "header" %>
3
+ <%= render "comments/comments" %>
4
+
5
+ <%= render "messages/actions/move" %>
6
+
7
+ <%= render @message.history.events %>
8
+
9
+ <%# render "something_missing" %>
@@ -0,0 +1,4 @@
1
+ require 'cache_digests/test_helper'
2
+
3
+ class FragmentHelperTest < MiniTest::Unit::TestCase
4
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'cache_digests'
4
+
5
+ require 'minitest/unit'
6
+ MiniTest::Unit.autorun
@@ -0,0 +1,133 @@
1
+ require 'cache_digests/test_helper'
2
+ require 'fileutils'
3
+
4
+ module ActionView
5
+ class MissingTemplate < StandardError
6
+ end
7
+ end
8
+
9
+ class FixtureTemplate
10
+ attr_reader :source
11
+
12
+ def initialize(template_path)
13
+ @source = File.read(template_path)
14
+ rescue Errno::ENOENT
15
+ raise ActionView::MissingTemplate
16
+ end
17
+ end
18
+
19
+ class FixtureFinder
20
+ FIXTURES_DIR = "#{File.dirname(__FILE__)}/fixtures"
21
+ TMP_DIR = "#{File.dirname(__FILE__)}/tmp"
22
+
23
+ def find(logical_name, keys, partial, options)
24
+ FixtureTemplate.new("#{TMP_DIR}/#{partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name}.#{options[:formats].first}.erb")
25
+ end
26
+ end
27
+
28
+ class TemplateDigestorTest < MiniTest::Unit::TestCase
29
+ def setup
30
+ FileUtils.cp_r FixtureFinder::FIXTURES_DIR, FixtureFinder::TMP_DIR
31
+ end
32
+
33
+ def teardown
34
+ FileUtils.rm_r FixtureFinder::TMP_DIR
35
+ CacheDigests::TemplateDigestor.cache.clear
36
+ end
37
+
38
+ def test_top_level_change_reflected
39
+ assert_digest_difference("messages/show") do
40
+ change_template("messages/show")
41
+ end
42
+ end
43
+
44
+ def test_explicit_dependency
45
+ assert_digest_difference("messages/show") do
46
+ change_template("messages/_message")
47
+ end
48
+ end
49
+
50
+ def test_second_level_dependency
51
+ assert_digest_difference("messages/show") do
52
+ change_template("comments/_comments")
53
+ end
54
+ end
55
+
56
+ def test_second_level_dependency_within_same_directory
57
+ assert_digest_difference("messages/show") do
58
+ change_template("messages/_header")
59
+ end
60
+ end
61
+
62
+ def test_third_level_dependency
63
+ assert_digest_difference("messages/show") do
64
+ change_template("comments/_comment")
65
+ end
66
+ end
67
+
68
+ def test_logging_of_missing_template
69
+ assert_logged "Couldn't find template for digesting: messages/something_missing.html" do
70
+ digest("messages/show")
71
+ end
72
+ end
73
+
74
+ def test_nested_template_directory
75
+ assert_digest_difference("messages/show") do
76
+ change_template("messages/actions/_move")
77
+ end
78
+ end
79
+
80
+ def test_dont_generate_a_digest_for_missing_templates
81
+ assert_equal '', digest("nothing/there")
82
+ end
83
+
84
+ def test_collection_dependency
85
+ assert_digest_difference("messages/index") do
86
+ change_template("messages/_message")
87
+ end
88
+
89
+ assert_digest_difference("messages/index") do
90
+ change_template("events/_event")
91
+ end
92
+ end
93
+
94
+ def test_collection_derived_from_record_dependency
95
+ assert_digest_difference("messages/show") do
96
+ change_template("events/_event")
97
+ end
98
+ end
99
+
100
+
101
+ private
102
+ def assert_logged(message)
103
+ log = StringIO.new
104
+ CacheDigests::TemplateDigestor.logger = Logger.new(log)
105
+
106
+ yield
107
+
108
+ log.rewind
109
+ assert_match message, log.read
110
+
111
+ CacheDigests::TemplateDigestor.logger = nil
112
+ end
113
+
114
+ def assert_digest_difference(template_name)
115
+ previous_digest = digest(template_name)
116
+ CacheDigests::TemplateDigestor.cache.clear
117
+
118
+ yield
119
+
120
+ assert previous_digest != digest(template_name), "digest didn't change"
121
+ CacheDigests::TemplateDigestor.cache.clear
122
+ end
123
+
124
+ def digest(template_name)
125
+ CacheDigests::TemplateDigestor.digest(template_name, :html, FixtureFinder.new)
126
+ end
127
+
128
+ def change_template(template_name)
129
+ File.open("#{FixtureFinder::TMP_DIR}/#{template_name}.html.erb", "w") do |f|
130
+ f.write "\nTHIS WAS CHANGED!"
131
+ end
132
+ end
133
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cache_digests
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Heinemeier Hansson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: actionpack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: minitest
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description:
63
+ email: david@37signals.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - ./bin/bundle
69
+ - ./cache_digests.gemspec
70
+ - ./Gemfile
71
+ - ./Gemfile.lock
72
+ - ./lib/cache_digests/engine.rb
73
+ - ./lib/cache_digests/fragment_helper.rb
74
+ - ./lib/cache_digests/template_digestor.rb
75
+ - ./lib/cache_digests.rb
76
+ - ./lib/tasks/dependencies.rake
77
+ - ./MIT-LICENSE
78
+ - ./Rakefile
79
+ - ./README.md
80
+ - ./test/fixtures/comments/_comment.html.erb
81
+ - ./test/fixtures/comments/_comments.html.erb
82
+ - ./test/fixtures/events/_event.html.erb
83
+ - ./test/fixtures/messages/_header.html.erb
84
+ - ./test/fixtures/messages/_message.html.erb
85
+ - ./test/fixtures/messages/actions/_move.html.erb
86
+ - ./test/fixtures/messages/index.html.erb
87
+ - ./test/fixtures/messages/show.html.erb
88
+ - ./test/fragment_helper_test.rb
89
+ - ./test/lib/cache_digests/test_helper.rb
90
+ - ./test/template_digestor_test.rb
91
+ homepage:
92
+ licenses: []
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '1.9'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 1.8.23
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: Nested fragment caches with (even) less situps
115
+ test_files: []