aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib/action_view
diff options
context:
space:
mode:
Diffstat (limited to 'actionview/lib/action_view')
-rw-r--r--actionview/lib/action_view/digestor.rb30
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb45
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb1
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb40
-rw-r--r--actionview/lib/action_view/log_subscriber.rb18
-rw-r--r--actionview/lib/action_view/lookup_context.rb16
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb8
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb24
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb41
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake4
-rw-r--r--actionview/lib/action_view/template.rb23
-rw-r--r--actionview/lib/action_view/template/error.rb4
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb25
13 files changed, 133 insertions, 146 deletions
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 359b2f810c..f3c5d6c8df 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -4,13 +4,11 @@ require 'monitor'
module ActionView
class Digestor
- cattr_reader(:cache)
- @@cache = Concurrent::Map.new
@@digest_monitor = Monitor.new
class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
def call(env)
- ActionView::Digestor.cache.clear
+ ActionView::LookupContext::DetailsKey.clear
app.call(env)
end
end
@@ -22,42 +20,42 @@ 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(options)
- options.assert_valid_keys(:name, :finder, :dependencies, :partial)
+ def digest(name:, finder:, **options)
+ options.assert_valid_keys(:dependencies, :partial)
- cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
+ cache_key = ([ name ].compact + Array.wrap(options[:dependencies])).join('.')
# this is a correctly done double-checked locking idiom
# (Concurrent::Map's lookups have volatile semantics)
- @@cache[cache_key] || @@digest_monitor.synchronize do
- @@cache.fetch(cache_key) do # re-check under lock
- compute_and_store_digest(cache_key, options)
+ finder.digest_cache[cache_key] || @@digest_monitor.synchronize do
+ finder.digest_cache.fetch(cache_key) do # re-check under lock
+ compute_and_store_digest(cache_key, name, finder, options)
end
end
end
private
- def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock
- klass = if options[:partial] || options[:name].include?("/_")
+ 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 = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
+ pre_stored = finder.digest_cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
PartialDigestor
else
Digestor
end
- @@cache[cache_key] = stored_digest = klass.new(options).digest
+ 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
- @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
+ finder.digest_cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
end
end
attr_reader :name, :finder, :options
- def initialize(name:, finder:, **options)
+ def initialize(name, finder, options = {})
@name, @finder = name, finder
@options = options
end
@@ -80,7 +78,7 @@ module ActionView
def nested_dependencies
dependencies.collect do |dependency|
- dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies
+ dependencies = PartialDigestor.new(dependency, finder).nested_dependencies
dependencies.any? ? { dependency => dependencies } : dependency
end
end
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 401f398721..4c7c4b91c6 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -126,44 +126,29 @@ module ActionView
#
# Now all you have to do is change that timestamp when the helper method changes.
#
- # === Automatic Collection Caching
+ # === Collection Caching
#
- # When rendering collections such as:
+ # When rendering a collection of objects that each use the same partial, a `cached`
+ # option can be passed.
+ # For collections rendered such:
#
- # <%= render @notifications %>
- # <%= render partial: 'notifications/notification', collection: @notifications %>
+ # <%= render partial: 'notifications/notification', collection: @notifications, cached: true %>
#
- # If the notifications/_notification partial starts with a cache call as:
+ # The `cached: true` will make Action View's rendering read several templates
+ # from cache at once instead of one call per template.
#
- # <% cache notification do %>
- # <%= notification.name %>
- # <% end %>
- #
- # The collection can then automatically use any cached renders for that
- # template by reading them at once instead of one by one.
- #
- # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for
- # more information on what cache calls make a template eligible for this
- # collection caching.
- #
- # The automatic cache multi read can be turned off like so:
+ # Templates in the collection not already cached are written to cache.
#
- # <%= render @notifications, cache: false %>
+ # Works great alongside individual template fragment caching.
+ # For instance if the template the collection renders is cached like:
#
- # === Explicit Collection Caching
- #
- # If the partial template doesn't start with a clean cache call as
- # mentioned above, you can still benefit from collection caching by
- # adding a special comment format anywhere in the template, like:
- #
- # <%# Template Collection: notification %>
- # <% my_helper_that_calls_cache(some_arg, notification) do %>
- # <%= notification.name %>
+ # # notifications/_notification.html.erb
+ # <% cache notification do %>
+ # <%# ... %>
# <% end %>
#
- # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>,
- # so it's important that you type it out just so.
- # You can only declare one collection in a partial template file.
+ # Any collection renders will find those cached templates when attempting
+ # to read multiple templates at once.
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
name_options = options.slice(:skip_digest, :virtual_path)
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index 2562504896..42e7358a1d 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -154,6 +154,7 @@ module ActionView
options.each_pair do |key, value|
if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
value.each_pair do |k, v|
+ next if v.nil?
output << sep
output << prefix_tag_option(key, k, v, escape)
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 3a4561a083..87218821ed 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -329,8 +329,8 @@ module ActionView
inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
if params
- params.each do |param_name, value|
- inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param)
+ to_form_params(params).each do |param|
+ inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value])
end
end
content_tag('form', inner_tags, form_options)
@@ -595,6 +595,42 @@ module ActionView
def method_tag(method)
tag('input', type: 'hidden', name: '_method', value: method.to_s)
end
+
+ # Returns an array of hashes each containing :name and :value keys
+ # suitable for use as the names and values of form input fields:
+ #
+ # to_form_params(name: 'David', nationality: 'Danish')
+ # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}]
+ #
+ # to_form_params(country: {name: 'Denmark'})
+ # # => [{name: 'country[name]', value: 'Denmark'}]
+ #
+ # to_form_params(countries: ['Denmark', 'Sweden']})
+ # # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}]
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # to_form_params({ name: 'Denmark' }, 'country')
+ # # => [{name: 'country[name]', value: 'Denmark'}]
+ def to_form_params(attribute, namespace = nil) # :nodoc:
+ params = []
+ case attribute
+ when Hash
+ attribute.each do |key, value|
+ prefix = namespace ? "#{namespace}[#{key}]" : key
+ params.push(*to_form_params(value, prefix))
+ end
+ when Array
+ array_prefix = "#{namespace}[]"
+ attribute.each do |value|
+ params.push(*to_form_params(value, array_prefix))
+ end
+ else
+ params << { name: namespace, value: attribute.to_param }
+ end
+
+ params.sort_by { |pair| pair[:name] }
+ end
end
end
end
diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb
index 9047dbdd85..aa38db2a3a 100644
--- a/actionview/lib/action_view/log_subscriber.rb
+++ b/actionview/lib/action_view/log_subscriber.rb
@@ -20,7 +20,15 @@ module ActionView
end
end
alias :render_partial :render_template
- alias :render_collection :render_template
+
+ def render_collection(event)
+ identifier = event.payload[:identifier] || 'templates'
+
+ info do
+ " Rendered collection of #{from_rails_root(identifier)}" \
+ " #{render_count(event.payload)} (#{event.duration.round(1)}ms)"
+ end
+ end
def logger
ActionView::Base.logger
@@ -38,6 +46,14 @@ module ActionView
def rails_root
@root ||= "#{Rails.root}/"
end
+
+ def render_count(payload)
+ if payload[:cache_hits]
+ "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
+ else
+ "[#{payload[:count]} times]"
+ end
+ end
end
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 126f289f55..86afedaa2d 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -55,9 +55,7 @@ module ActionView
class DetailsKey #:nodoc:
alias :eql? :equal?
- alias :object_hash :hash
- attr_reader :hash
@details_keys = Concurrent::Map.new
def self.get(details)
@@ -72,8 +70,16 @@ module ActionView
@details_keys.clear
end
+ def self.empty?; @details_keys.empty?; end
+
+ def self.digest_caches
+ @details_keys.values.map(&:digest_cache)
+ end
+
+ attr_reader :digest_cache
+
def initialize
- @hash = object_hash
+ @digest_cache = Concurrent::Map.new
end
end
@@ -200,6 +206,10 @@ module ActionView
self.view_paths = view_paths
end
+ def digest_cache
+ details_key.digest_cache
+ end
+
def initialize_details(target, details)
registered_details.each do |k|
target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call
diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
index aa77a77acf..23e672a95f 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -35,8 +35,12 @@ module ActionView
end
end
- def instrument(name, options={})
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
+ def instrument(name, **options)
+ options[:identifier] ||= (@template && @template.identifier) || @path
+
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
+ yield payload
+ end
end
def prepend_formats(formats)
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index bdbf03191a..514804b08e 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -294,7 +294,7 @@ module ActionView
def render(context, options, block)
setup(context, options, block)
- identifier = (@template = find_partial) ? @template.identifier : @path
+ @template = find_partial
@lookup_context.rendered_format ||= begin
if @template && @template.formats.present?
@@ -305,11 +305,9 @@ module ActionView
end
if @collection
- instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
- render_collection
- end
+ render_collection
else
- instrument(:partial, :identifier => identifier) do
+ instrument(:partial) do
render_partial
end
end
@@ -318,15 +316,17 @@ module ActionView
private
def render_collection
- return nil if @collection.blank?
+ instrument(:collection, count: @collection.size) do |payload|
+ return nil if @collection.blank?
- if @options.key?(:spacer_template)
- spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
- end
+ if @options.key?(:spacer_template)
+ spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
+ end
- cache_collection_render do
- @template ? collection_with_template : collection_without_template
- end.join(spacer).html_safe
+ cache_collection_render(payload) do
+ @template ? collection_with_template : collection_without_template
+ end.join(spacer).html_safe
+ end
end
def render_partial
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
index 5a294744e8..f7deba94ce 100644
--- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/object/try'
-
module ActionView
module CollectionCaching # :nodoc:
extend ActiveSupport::Concern
@@ -11,38 +9,25 @@ module ActionView
end
private
- def cache_collection_render
- return yield unless cache_collection?
+ def cache_collection_render(instrumentation_payload)
+ return yield unless @options[:cached]
keyed_collection = collection_by_cache_keys
- partial_cache = collection_cache.read_multi(*keyed_collection.keys)
+ cached_partials = collection_cache.read_multi(*keyed_collection.keys)
+ instrumentation_payload[:cache_hits] = cached_partials.size
- @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values
- rendered_partials = @collection.any? ? yield.dup : []
+ @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
+ rendered_partials = @collection.empty? ? [] : yield
- fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do
- rendered_partials.shift
+ index = 0
+ fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do
+ rendered_partials[index].tap { index += 1 }
end
end
- def cache_collection?
- @options.fetch(:cache, automatic_cache_eligible?)
- end
-
- def automatic_cache_eligible?
- @template && !callable_cache_key? &&
- @template.eligible_for_collection_caching?(as: @options[:as])
- end
-
- def callable_cache_key?
- @options[:cache].respond_to?(:call)
- end
-
def collection_by_cache_keys
- seed = callable_cache_key? ? @options[:cache] : ->(i) { i }
-
@collection.each_with_object({}) do |item, hash|
- hash[expanded_cache_key(seed.call(item))] = item
+ hash[expanded_cache_key(item)] = item
end
end
@@ -52,10 +37,10 @@ module ActionView
end
def fetch_or_cache_partial(cached_partials, order_by:)
- order_by.map do |key|
- cached_partials.fetch(key) do
+ order_by.map do |cache_key|
+ cached_partials.fetch(cache_key) do
yield.tap do |rendered_partial|
- collection_cache.write(key, rendered_partial, @options[:cache_options])
+ collection_cache.write(cache_key, rendered_partial)
end
end
end
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake
index f394c319c1..9932ff8b6d 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(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies
+ puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).nested_dependencies
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(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies
+ puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).dependencies
end
class CacheDigests
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 15fc2b71a3..169ee55fdc 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -130,7 +130,6 @@ module ActionView
@source = source
@identifier = identifier
@handler = handler
- @cache_name = extract_resource_cache_name
@compiled = false
@original_encoding = nil
@locals = details[:locals] || []
@@ -166,10 +165,6 @@ module ActionView
@type ||= Types[@formats.first] if @formats.first
end
- def eligible_for_collection_caching?(as: nil)
- @cache_name == (as || inferred_cache_name).to_s
- end
-
# Receives a view object and return a template similar to self by using @virtual_path.
#
# This method is useful if you have a template object but it does not contain its source
@@ -355,23 +350,5 @@ module ActionView
ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block)
end
end
-
- EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/
-
- def extract_resource_cache_name
- if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match
- match[:resource_name]
- end
- end
-
- def resource_cache_call_match
- if @handler.respond_to?(:resource_cache_call_pattern)
- @source.match(@handler.resource_cache_call_pattern)
- end
- end
-
- def inferred_cache_name
- @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze)
- end
end
end
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index ccee785d3e..3f38c3d2b9 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -135,13 +135,13 @@ module ActionView
end
def formatted_code_for(source_code, line_counter, indent, output)
- start_value = (output == :html) ? {} : ""
+ start_value = (output == :html) ? {} : []
source_code.inject(start_value) do |result, line|
line_counter += 1
if output == :html
result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line])
else
- result << "%#{indent}s: %s\n" % [line_counter, line]
+ result << "%#{indent}s: %s" % [line_counter, line]
end
end
end
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 1f8459c24b..85a100ed4c 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -123,31 +123,6 @@ module ActionView
).src
end
- # Returns Regexp to extract a cached resource's name from a cache call at the
- # first line of a template.
- # The extracted cache name is captured as :resource_name.
- #
- # <% cache notification do %> # => notification
- #
- # The pattern should support templates with a beginning comment:
- #
- # <%# Still extractable even though there's a comment %>
- # <% cache notification do %> # => notification
- #
- # But fail to extract a name if a resource association is cached.
- #
- # <% cache notification.event do %> # => nil
- def resource_cache_call_pattern
- /\A
- (?:<%\#.*%>)* # optional initial comment
- \s* # followed by optional spaces or newlines
- <%\s*cache[\(\s] # followed by an ERB call to cache
- \s* # followed by optional spaces or newlines
- (?<resource_name>\w+) # capture the cache call argument as :resource_name
- [\s\)] # followed by a space or close paren
- /xm
- end
-
private
def valid_encoding(string, encoding)