Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
Merge pull request #19 from dasch/refactor-dependency-tracking
Browse files Browse the repository at this point in the history
Allow registering custom dependency trackers
  • Loading branch information
dhh committed Feb 25, 2013
2 parents 1bf04bb + 25ced24 commit 951d3a2
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 58 deletions.
29 changes: 17 additions & 12 deletions Gemfile.lock
Expand Up @@ -3,43 +3,48 @@ PATH
specs:
cache_digests (0.2.0)
actionpack (>= 3.2)
thread_safe

GEM
remote: https://rubygems.org/
specs:
actionpack (3.2.8)
activemodel (= 3.2.8)
activesupport (= 3.2.8)
actionpack (3.2.12)
activemodel (= 3.2.12)
activesupport (= 3.2.12)
builder (~> 3.0.0)
erubis (~> 2.7.0)
journey (~> 1.0.4)
rack (~> 1.4.0)
rack (~> 1.4.5)
rack-cache (~> 1.2)
rack-test (~> 0.6.1)
sprockets (~> 2.1.3)
activemodel (3.2.8)
activesupport (= 3.2.8)
sprockets (~> 2.2.1)
activemodel (3.2.12)
activesupport (= 3.2.12)
builder (~> 3.0.0)
activesupport (3.2.8)
activesupport (3.2.12)
i18n (~> 0.6)
multi_json (~> 1.0)
builder (3.0.3)
atomic (1.0.1)
builder (3.0.4)
erubis (2.7.0)
hike (1.2.1)
i18n (0.6.1)
journey (1.0.4)
minitest (2.12.1)
multi_json (1.3.6)
rack (1.4.1)
multi_json (1.6.1)
rack (1.4.5)
rack-cache (1.2)
rack (>= 0.4)
rack-test (0.6.2)
rack (>= 1.0)
rake (0.9.2.2)
sprockets (2.1.3)
sprockets (2.2.2)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
thread_safe (0.1.0)
atomic
tilt (1.3.3)

PLATFORMS
Expand Down
1 change: 1 addition & 0 deletions cache_digests.gemspec
Expand Up @@ -8,6 +8,7 @@ Gem::Specification.new do |s|
s.required_ruby_version = '>= 1.9'

s.add_dependency 'actionpack', '>= 3.2'
s.add_dependency 'thread_safe'

s.add_development_dependency 'rake'
s.add_development_dependency 'minitest'
Expand Down
89 changes: 89 additions & 0 deletions lib/cache_digests/dependency_tracker.rb
@@ -0,0 +1,89 @@
require 'thread_safe'

module CacheDigests
class DependencyTracker
@trackers = ThreadSafe::Cache.new

def self.find_dependencies(name, template)
tracker = @trackers[template.handler]

if tracker.present?
tracker.call(name, template)
else
[]
end
end

def self.register_tracker(extension, tracker)
handler = ActionView::Template.handler_for_extension(extension)
@trackers[handler] = tracker
end

def self.remove_tracker(handler)
@trackers.delete(handler)
end

class ERBTracker
EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/

# Matches:
# render partial: "comments/comment", collection: commentable.comments
# render "comments/comments"
# render 'comments/comments'
# render('comments/comments')
#
# render(@topic) => render("topics/topic")
# render(topics) => render("topics/topic")
# render(message.topics) => render("topics/topic")
RENDER_DEPENDENCY = /
render\s* # render, followed by optional whitespace
\(? # start an optional parenthesis for the render call
(partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture
([@a-z"'][@a-z_\/\."']+) # the template name itself -- 2nd capture
/x

def self.call(name, template)
new(name, template).dependencies
end

def initialize(name, template)
@name, @template = name, template
end

def dependencies
render_dependencies + explicit_dependencies
end

private
attr_reader :name, :template

def source
template.source
end

def directory
name.split("/")[0..-2].join("/")
end

def render_dependencies
source.scan(RENDER_DEPENDENCY).
collect(&:second).uniq.

# render(@topic) => render("topics/topic")
# render(topics) => render("topics/topic")
# render(message.topics) => render("topics/topic")
collect { |name| name.sub(/\A@?([a-z]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }.

# render("headline") => render("message/headline")
collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }.

# replace quotes from string renders
collect { |name| name.gsub(/["']/, "") }
end

def explicit_dependencies
source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
end
end
end
end
1 change: 1 addition & 0 deletions lib/cache_digests/engine.rb
Expand Up @@ -13,6 +13,7 @@ class Engine < ::Rails::Engine

config.to_prepare do
CacheDigests::TemplateDigestor.logger = Rails.logger
DependencyTracker.register_tracker :erb, DependencyTracker::ERBTracker
end
end
end
Expand Down
50 changes: 6 additions & 44 deletions lib/cache_digests/template_digestor.rb
@@ -1,27 +1,10 @@
require 'active_support/core_ext'
require 'active_support/cache'
require 'logger'
require 'cache_digests/dependency_tracker'

module CacheDigests
class TemplateDigestor
EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/

# Matches:
# render partial: "comments/comment", collection: commentable.comments
# render "comments/comments"
# render 'comments/comments'
# render('comments/comments')
#
# render(@topic) => render("topics/topic")
# render(topics) => render("topics/topic")
# render(message.topics) => render("topics/topic")
RENDER_DEPENDENCY = /
render\s* # render, followed by optional whitespace
\(? # start an optional parenthesis for the render call
(partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture
([@a-z"'][@a-z_\/\."']+) # the template name itself -- 2nd capture
/x

cattr_accessor(:cache) { ActiveSupport::Cache::MemoryStore.new }
cattr_accessor(:cache_prefix)

Expand Down Expand Up @@ -51,7 +34,7 @@ def digest
end

def dependencies
render_dependencies + explicit_dependencies
DependencyTracker.find_dependencies(name, template)
rescue ActionView::MissingTemplate
[] # File doesn't exist, so no dependencies
end
Expand All @@ -68,18 +51,17 @@ def logical_name
name.gsub(%r|/_|, "/")
end

def directory
name.split("/")[0..-2].join("/")
end

def partial?
options[:partial] || name.include?("/_")
end

def source
@source ||= finder.find(logical_name, [], partial?, formats: [ format ]).source
template.source
end

def template
@template ||= finder.find(logical_name, [], partial?, formats: [ format ])
end

def dependency_digest
template_digests = dependencies.collect do |template_name|
Expand All @@ -89,26 +71,6 @@ def dependency_digest
(template_digests + injected_dependencies).join("-")
end

def render_dependencies
source.scan(RENDER_DEPENDENCY).
collect(&:second).uniq.

# render(@topic) => render("topics/topic")
# render(topics) => render("topics/topic")
# render(message.topics) => render("topics/topic")
collect { |name| name.sub(/\A@?([a-z]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }.

# render("headline") => render("message/headline")
collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }.

# replace quotes from string renders
collect { |name| name.gsub(/["']/, "") }
end

def explicit_dependencies
source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
end

def injected_dependencies
Array.wrap(options[:dependencies])
end
Expand Down
49 changes: 49 additions & 0 deletions test/dependency_tracker_test.rb
@@ -0,0 +1,49 @@
require 'cache_digests/test_helper'

class NeckbeardTracker
def self.call(name, template)
["foo/#{name}"]
end
end

module ActionView
class Template
def self.handler_for_extension(extension)
extension
end
end
end

class DependencyTrackerTest < MiniTest::Unit::TestCase
class FakeTemplate
attr_reader :source, :handler

def initialize(source, handler)
@source, @handler = source, handler
end
end

def tracker
CacheDigests::DependencyTracker
end

def setup
tracker.register_tracker(:neckbeard, NeckbeardTracker)
end

def teardown
tracker.remove_tracker(:neckbeard)
end

def test_finds_tracker_by_template_handler
template = FakeTemplate.new("boo/hoo", :neckbeard)
dependencies = tracker.find_dependencies("boo/hoo", template)
assert_equal ["foo/boo/hoo"], dependencies
end

def test_returns_empty_array_if_no_tracker_registered_for_handler
template = FakeTemplate.new("boo/hoo", :hater)
dependencies = tracker.find_dependencies("boo/hoo", template)
assert_equal [], dependencies
end
end
7 changes: 5 additions & 2 deletions test/template_digestor_test.rb
Expand Up @@ -7,10 +7,11 @@ class MissingTemplate < StandardError
end

class FixtureTemplate
attr_reader :source
attr_reader :source, :handler

def initialize(template_path)
def initialize(template_path, handler = :erb)
@source = File.read(template_path)
@handler = handler
rescue Errno::ENOENT
raise ActionView::MissingTemplate
end
Expand All @@ -28,12 +29,14 @@ def find(logical_name, keys, partial, options)
class TemplateDigestorTest < MiniTest::Unit::TestCase
def setup
FileUtils.cp_r FixtureFinder::FIXTURES_DIR, FixtureFinder::TMP_DIR
CacheDigests::DependencyTracker.register_tracker :erb, CacheDigests::DependencyTracker::ERBTracker
end

def teardown
FileUtils.rm_r FixtureFinder::TMP_DIR
CacheDigests::TemplateDigestor.cache.clear
CacheDigests::TemplateDigestor.cache_prefix = nil
CacheDigests::DependencyTracker.remove_tracker :erb
end

def test_top_level_change_reflected
Expand Down

0 comments on commit 951d3a2

Please sign in to comment.