aboutsummaryrefslogtreecommitdiffstats
path: root/actionview
diff options
context:
space:
mode:
Diffstat (limited to 'actionview')
-rw-r--r--actionview/CHANGELOG.md52
-rw-r--r--actionview/lib/action_view/digestor.rb147
-rw-r--r--actionview/lib/action_view/gem_version.rb2
-rw-r--r--actionview/lib/action_view/lookup_context.rb33
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb2
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake4
-rw-r--r--actionview/lib/action_view/template/resolver.rb8
-rw-r--r--actionview/lib/action_view/view_paths.rb2
-rw-r--r--actionview/test/template/digestor_test.rb14
9 files changed, 149 insertions, 115 deletions
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index bd7ce14e04..d084f2b1e0 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,30 @@
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* Collection rendering can cache and fetch multiple partials at once.
+
+ Collections rendered as:
+
+ ```ruby
+ <%= render partial: 'notifications/notification', collection: @notifications, as: :notification, cached: true %>
+ ```
+
+ will read several partials from cache at once. The templates in the collection
+ that haven't been cached already will automatically be written to cache. Works
+ great alongside individual template fragment caching. For instance if the
+ template the collection renders is cached like:
+
+ ```ruby
+ # notifications/_notification.html.erb
+ <% cache notification do %>
+ <%# ... %>
+ <% end %>
+ ```
+
+ Then any collection renders shares that cache when attempting to read multiple
+ ones at once.
+
+ *Kasper Timm Hansen*
+
* Add support for nested hashes/arrays to `:params` option of `button_to` helper.
*James Coleman*
@@ -197,31 +224,6 @@
*Ulisses Almeida*
-* Collection rendering can cache and fetch multiple partials at once.
-
- Collections rendered as:
-
- ```ruby
- <%= render partial: 'notifications/notification', collection: @notifications, as: :notification, cached: true %>
- ```
-
- will read several partials from cache at once. The templates in the collection
- that haven't been cached already will automatically be written to cache. Works
- great alongside individual template fragment caching. For instance if the
- template the collection renders is cached like:
-
- ```ruby
- # notifications/_notification.html.erb
- <% cache notification do %>
- <%# ... %>
- <% end %>
- ```
-
- Then any collection renders shares that cache when attempting to read multiple
- ones at once.
-
- *Kasper Timm Hansen*
-
* Fixed a dependency tracker bug that caused template dependencies not
count layouts as dependencies for partials.
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index f3c5d6c8df..b99d1af998 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -4,7 +4,7 @@ require 'monitor'
module ActionView
class Digestor
- @@digest_monitor = Monitor.new
+ @@digest_mutex = Mutex.new
class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
def call(env)
@@ -20,111 +20,104 @@ module ActionView
# * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
# * <tt>dependencies</tt> - An array of dependent views
# * <tt>partial</tt> - Specifies whether the template is a partial
- def digest(name:, finder:, **options)
- options.assert_valid_keys(:dependencies, :partial)
-
- cache_key = ([ name ].compact + Array.wrap(options[:dependencies])).join('.')
+ def digest(name:, finder:, dependencies: [])
+ dependencies ||= []
+ cache_key = ([ name ].compact + dependencies).join('.')
# this is a correctly done double-checked locking idiom
# (Concurrent::Map's lookups have volatile semantics)
- finder.digest_cache[cache_key] || @@digest_monitor.synchronize do
+ finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
finder.digest_cache.fetch(cache_key) do # re-check under lock
- compute_and_store_digest(cache_key, name, finder, options)
+ partial = name.include?("/_")
+ root = tree(name, finder, partial)
+ dependencies.each do |injected_dep|
+ root.children << Injected.new(injected_dep, nil, nil)
+ end
+ finder.digest_cache[cache_key] = root.digest(finder)
end
end
end
- private
- def compute_and_store_digest(cache_key, name, finder, options) # called under @@digest_monitor lock
- klass = if options[:partial] || name.include?("/_")
- # Prevent re-entry or else recursive templates will blow the stack.
- # There is no need to worry about other threads seeing the +false+ value,
- # as they will then have to wait for this thread to let go of the @@digest_monitor lock.
- pre_stored = finder.digest_cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
- PartialDigestor
- else
- Digestor
- end
-
- finder.digest_cache[cache_key] = stored_digest = klass.new(name, finder, options).digest
- ensure
- # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
- finder.digest_cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
- end
- end
-
- attr_reader :name, :finder, :options
+ def logger
+ ActionView::Base.logger || NullLogger
+ end
- def initialize(name, finder, options = {})
- @name, @finder = name, finder
- @options = options
- end
+ # Create a dependency tree for template named +name+.
+ def tree(name, finder, partial = false, seen = {})
+ logical_name = name.gsub(%r|/_|, "/")
- def digest
- Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
- logger.debug " Cache digest for #{template.inspect}: #{digest}"
- end
- rescue ActionView::MissingTemplate
- logger.error " Couldn't find template for digesting: #{name}"
- ''
- end
+ if finder.disable_cache { finder.exists?(logical_name, [], partial) }
+ template = finder.disable_cache { finder.find(logical_name, [], partial) }
- def dependencies
- DependencyTracker.find_dependencies(name, template, finder.view_paths)
- rescue ActionView::MissingTemplate
- logger.error " '#{name}' file doesn't exist, so no dependencies"
- []
- end
+ if node = seen[template.identifier] # handle cycles in the tree
+ node
+ else
+ node = seen[template.identifier] = Node.create(name, logical_name, template, partial)
- def nested_dependencies
- dependencies.collect do |dependency|
- dependencies = PartialDigestor.new(dependency, finder).nested_dependencies
- dependencies.any? ? { dependency => dependencies } : dependency
+ deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
+ deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file|
+ node.children << tree(dep_file, finder, true, seen)
+ end
+ node
+ end
+ else
+ logger.error " '#{name}' file doesn't exist, so no dependencies"
+ logger.error " Couldn't find template for digesting: #{name}"
+ seen[name] ||= Missing.new(name, logical_name, nil)
+ end
end
end
- private
- class NullLogger
- def self.debug(_); end
- def self.error(_); end
- end
+ class Node
+ attr_reader :name, :logical_name, :template, :children
- def logger
- ActionView::Base.logger || NullLogger
+ def self.create(name, logical_name, template, partial)
+ klass = partial ? Partial : Node
+ klass.new(name, logical_name, template, [])
end
- def logical_name
- name.gsub(%r|/_|, "/")
+ def initialize(name, logical_name, template, children = [])
+ @name = name
+ @logical_name = logical_name
+ @template = template
+ @children = children
end
- def partial?
- false
+ def digest(finder, stack = [])
+ Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
end
- def template
- @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) }
+ def dependency_digest(finder, stack)
+ children.map do |node|
+ if stack.include?(node)
+ false
+ else
+ finder.digest_cache[node.name] ||= begin
+ stack.push node
+ node.digest(finder, stack).tap { stack.pop }
+ end
+ end
+ end.join("-")
end
- def source
- template.source
+ def to_dep_map
+ children.any? ? { name => children.map(&:to_dep_map) } : name
end
+ end
- def dependency_digest
- template_digests = dependencies.collect do |template_name|
- Digestor.digest(name: template_name, finder: finder, partial: true)
- end
+ class Partial < Node; end
- (template_digests + injected_dependencies).join("-")
- end
+ class Missing < Node
+ def digest(finder, _ = []) '' end
+ end
- def injected_dependencies
- Array.wrap(options[:dependencies])
- end
- end
+ class Injected < Node
+ def digest(finder, _ = []) name end
+ end
- class PartialDigestor < Digestor # :nodoc:
- def partial?
- true
+ class NullLogger
+ def self.debug(_); end
+ def self.error(_); end
end
end
end
diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb
index bb5c96cb39..efb565bf59 100644
--- a/actionview/lib/action_view/gem_version.rb
+++ b/actionview/lib/action_view/gem_version.rb
@@ -8,7 +8,7 @@ module ActionView
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 86afedaa2d..626c4b8f5e 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -70,8 +70,6 @@ module ActionView
@details_keys.clear
end
- def self.empty?; @details_keys.empty?; end
-
def self.digest_caches
@details_keys.values.map(&:digest_cache)
end
@@ -138,6 +136,11 @@ module ActionView
end
alias :template_exists? :exists?
+ def any?(name, prefixes = [], partial = false)
+ @view_paths.exists?(*args_for_any(name, prefixes, partial))
+ end
+ alias :any_templates? :any?
+
# Adds fallbacks to the view paths. Useful in cases when you are rendering
# a :file.
def with_fallbacks
@@ -174,6 +177,32 @@ module ActionView
[user_details, details_key]
end
+ def args_for_any(name, prefixes, partial) # :nodoc:
+ name, prefixes = normalize_name(name, prefixes)
+ details, details_key = detail_args_for_any
+ [name, prefixes, partial || false, details, details_key]
+ end
+
+ def detail_args_for_any # :nodoc:
+ @detail_args_for_any ||= begin
+ details = {}
+
+ registered_details.each do |k|
+ if k == :variants
+ details[k] = :any
+ else
+ details[k] = Accessors::DEFAULT_PROCS[k].call
+ end
+ end
+
+ if @cache
+ [details, DetailsKey.get(details)]
+ else
+ [details, nil]
+ end
+ end
+ end
+
# Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template
# name instead of the prefix.
diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
index 23e672a95f..1dddf53df0 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -15,7 +15,7 @@ module ActionView
# that new object is called in turn. This abstracts the setup and rendering
# into a separate classes for partials and templates.
class AbstractRenderer #:nodoc:
- delegate :find_template, :find_file, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
+ delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake
index 9932ff8b6d..045bdf5691 100644
--- a/actionview/lib/action_view/tasks/dependencies.rake
+++ b/actionview/lib/action_view/tasks/dependencies.rake
@@ -2,13 +2,13 @@ namespace :cache_digests do
desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
task :nested_dependencies => :environment do
abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
- puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).nested_dependencies
+ puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:to_dep_map)
end
desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
task :dependencies => :environment do
abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
- puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).dependencies
+ puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:name)
end
class CacheDigests
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 8a675cd521..b6de0b03bf 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -222,7 +222,7 @@ module ActionView
end
def find_template_paths(query)
- Dir[query].reject do |filename|
+ Dir[query].uniq.reject do |filename|
File.directory?(filename) ||
# deals with case-insensitive file systems.
!File.fnmatch(query, filename, File::FNM_EXTGLOB)
@@ -340,7 +340,11 @@ module ActionView
query = escape_entry(File.join(@path, path))
exts = EXTENSIONS.map do |ext, prefix|
- "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
+ if ext == :variants && details[ext] == :any
+ "{#{prefix}*,}"
+ else
+ "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
+ end
end.join
query + exts
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index 37722013ce..b46fe06b01 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -10,7 +10,7 @@ module ActionView
self._view_paths.freeze
end
- delegate :template_exists?, :view_paths, :formats, :formats=,
+ delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
:locale, :locale=, :to => :lookup_context
module ClassMethods
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index a9a6de250c..d4c5048bde 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -146,6 +146,11 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_nested_template_deps
+ nested_deps = ["messages/header", {"comments/comments"=>["comments/comment"]}, "messages/actions/move", "events/event", "messages/something_missing", "messages/something_missing_1", "messages/message", "messages/form"]
+ assert_equal nested_deps, nested_dependencies("messages/show")
+ end
+
def test_recursion_in_renders
assert digest("level/recursion") # assert recursion is possible
assert_not_nil digest("level/recursion") # assert digest is stored
@@ -313,16 +318,17 @@ class TemplateDigestorTest < ActionView::TestCase
options = options.dup
finder.variants = options.delete(:variants) || []
-
- ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options))
+ ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || []))
end
def dependencies(template_name)
- ActionView::Digestor.new(template_name, finder).dependencies
+ tree = ActionView::Digestor.tree(template_name, finder)
+ tree.children.map(&:name)
end
def nested_dependencies(template_name)
- ActionView::Digestor.new(template_name, finder).nested_dependencies
+ tree = ActionView::Digestor.tree(template_name, finder)
+ tree.children.map(&:to_dep_map)
end
def finder