From 4ceaf55aea720036356f9e71540449924eef958d Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 28 Jan 2019 14:03:40 -0800 Subject: Deprecate `with_fallbacks` using a block This patch changes `with_fallbacks` to be a factory method that returns a new instance of a lookup context which contains the fallback view paths in addition to the controller specific view paths. Since the lookup context is more "read only", we may be able to cache them --- actionview/lib/action_view/lookup_context.rb | 24 ++++++++++++++++----- .../lib/action_view/renderer/template_renderer.rb | 8 +++---- actionview/test/template/lookup_context_test.rb | 25 ++++++++++++++++------ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index cc262dc2b7..c2f6439e26 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -3,6 +3,7 @@ require "concurrent/map" require "active_support/core_ext/module/remove_method" require "active_support/core_ext/module/attribute_accessors" +require "active_support/deprecation" require "action_view/template/resolver" module ActionView @@ -132,11 +133,24 @@ module ActionView # Adds fallbacks to the view paths. Useful in cases when you are rendering # a :file. def with_fallbacks - view_paths = @view_paths - @view_paths = build_view_paths((view_paths.paths + self.class.fallbacks).uniq) - yield - ensure - @view_paths = view_paths + view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq) + + if block_given? + ActiveSupport::Deprecation.warn <<~eowarn + Calling `with_fallbacks` with a block is deprecated. Call methods on + the lookup context returned by `with_fallbacks` instead. + eowarn + + begin + _view_paths = @view_paths + @view_paths = view_paths + yield + ensure + @view_paths = _view_paths + end + else + ActionView::LookupContext.new(view_paths, @details, @prefixes) + end end private diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index fb5539e0db..6a50ccff7f 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -28,7 +28,7 @@ module ActionView elsif options.key?(:html) Template::HTML.new(options[:html], formats.first) elsif options.key?(:file) - with_fallbacks { find_file(options[:file], nil, false, keys, @details) } + @lookup_context.with_fallbacks.find_file(options[:file], nil, false, keys, @details) elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, locals: keys) @@ -36,7 +36,7 @@ module ActionView if options[:template].respond_to?(:render) options[:template] else - find_template(options[:template], options[:prefixes], false, keys, @details) + @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details) end else raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option." @@ -82,9 +82,9 @@ module ActionView when String begin if layout.start_with?("/") - with_fallbacks { find_template(layout, nil, false, [], details) } + @lookup_context.with_fallbacks.find_template(layout, nil, false, [], details) else - find_template(layout, nil, false, [], details) + @lookup_context.find_template(layout, nil, false, [], details) end rescue ActionView::MissingTemplate all_details = @details.merge(formats: @lookup_context.default_formats) diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index 0894925c9f..290f832794 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -122,19 +122,32 @@ class LookupContextTest < ActiveSupport::TestCase test "adds fallbacks to view paths when required" do assert_equal 1, @lookup_context.view_paths.size - @lookup_context.with_fallbacks do - assert_equal 3, @lookup_context.view_paths.size - assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("") - assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/") + assert_deprecated do + @lookup_context.with_fallbacks do + assert_equal 3, @lookup_context.view_paths.size + assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("") + assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/") + end end + + @lookup_context = @lookup_context.with_fallbacks + + assert_equal 3, @lookup_context.view_paths.size + assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("") + assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/") end test "add fallbacks just once in nested fallbacks calls" do - @lookup_context.with_fallbacks do + assert_deprecated do @lookup_context.with_fallbacks do - assert_equal 3, @lookup_context.view_paths.size + @lookup_context.with_fallbacks do + assert_equal 3, @lookup_context.view_paths.size + end end end + + @lookup_context = @lookup_context.with_fallbacks.with_fallbacks + assert_equal 3, @lookup_context.view_paths.size end test "generates a new details key for each details hash" do -- cgit v1.2.3 From bd3bea1598919e177ca6e56d23ae2fc9d8d5e22e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 28 Jan 2019 14:12:08 -0800 Subject: Remove `find_template` and `find_file` delegate methods This reduces the surface area of our API and removes a Liskov issue. Both TemplateRenderer and PartialRenderer inherit from AbstractRenderer, but since PartialRenderer implements it's own `find_template` that is private, and has the wrong method signature, an instance of PartialRenderer cannot be substituted for an instance of AbstractRenderer renderer. Removing the superclass implementation solves both issues. --- actionview/lib/action_view/renderer/abstract_renderer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 20b2523cac..37f07eabc2 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -17,7 +17,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?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context + delegate :template_exists?, :any_templates?, :with_layout_format, :formats, to: :@lookup_context def initialize(lookup_context) @lookup_context = lookup_context -- cgit v1.2.3 From e98b51300ab1c58b790cbaf9a27bd277184579ac Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 28 Jan 2019 14:22:32 -0800 Subject: Remove `@view` instance variable from the partial renderer Similar to 1853b0d0abf87dfdd4c3a277c3badb17ca19652e --- .../lib/action_view/renderer/partial_renderer.rb | 33 +++++++++++----------- .../partial_renderer/collection_caching.rb | 16 +++++------ .../collection_cache_association_loading.rb | 4 +-- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index f175c30aa1..42c6a97682 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -307,31 +307,31 @@ module ActionView end if @collection - render_collection + render_collection(context) else - render_partial + render_partial(context) end end private - def render_collection + def render_collection(view) 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) + spacer = find_template(@options[:spacer_template], @locals.keys).render(view, @locals) end - cache_collection_render(payload) do - @template ? collection_with_template : collection_without_template + cache_collection_render(payload, view) do + @template ? collection_with_template(view) : collection_without_template(view) end.join(spacer).html_safe end end - def render_partial + def render_partial(view) instrument(:partial) do |payload| - view, locals, block = @view, @locals, @block + locals, block = @locals, @block object, as = @object, @variable if !block && (layout = @options[:layout]) @@ -359,7 +359,6 @@ module ActionView # set to that string. Otherwise, the +options[:partial]+ object must # respond to +to_partial_path+ in order to setup the path. def setup(context, options, block) - @view = context @options = options @block = block @@ -381,10 +380,10 @@ module ActionView @collection = collection_from_object || collection_from_options if @collection - paths = @collection_data = @collection.map { |o| partial_path(o) } + paths = @collection_data = @collection.map { |o| partial_path(o, context) } @path = paths.uniq.one? ? paths.first : nil else - @path = partial_path + @path = partial_path(@object, context) end end @@ -423,8 +422,8 @@ module ActionView @lookup_context.find_template(path, prefixes, true, locals, @details) end - def collection_with_template - view, locals, template = @view, @locals, @template + def collection_with_template(view) + locals, template = @locals, @template as, counter, iteration = @variable, @variable_counter, @variable_iteration if layout = @options[:layout] @@ -445,8 +444,8 @@ module ActionView end end - def collection_without_template - view, locals, collection_data = @view, @locals, @collection_data + def collection_without_template(view) + locals, collection_data = @locals, @collection_data cache = {} keys = @locals.keys @@ -474,7 +473,7 @@ module ActionView # # If +prefix_partial_path_with_controller_namespace+ is true, then this # method will prefix the partial paths with a namespace. - def partial_path(object = @object) + def partial_path(object, view) object = object.to_model if object.respond_to?(:to_model) path = if object.respond_to?(:to_partial_path) @@ -483,7 +482,7 @@ module ActionView raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.") end - if @view.prefix_partial_path_with_controller_namespace + if view.prefix_partial_path_with_controller_namespace prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) else path 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 5aa6f77902..2d4a171726 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -11,13 +11,13 @@ module ActionView end private - def cache_collection_render(instrumentation_payload) + def cache_collection_render(instrumentation_payload, view) return yield unless @options[:cached] # Result is a hash with the key represents the # key used for cache lookup and the value is the item # on which the partial is being rendered - keyed_collection = collection_by_cache_keys + keyed_collection = collection_by_cache_keys(view) # Pull all partials from cache # Result is a hash, key matches the entry in @@ -51,21 +51,21 @@ module ActionView @options[:cached].respond_to?(:call) end - def collection_by_cache_keys + def collection_by_cache_keys(view) seed = callable_cache_key? ? @options[:cached] : ->(i) { i } @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(seed.call(item))] = item + hash[expanded_cache_key(seed.call(item), view)] = item end end - def expanded_cache_key(key) - key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path)) + def expanded_cache_key(key, view) + key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path(view))) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end - def digest_path - @digest_path ||= @view.digest_path_from_virtual(@template.virtual_path) + def digest_path(view) + @digest_path ||= view.digest_path_from_virtual(@template.virtual_path) end # `order_by` is an enumerable object containing keys of the cache, diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb index b5129e4239..4ef19f0929 100644 --- a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb +++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb @@ -20,12 +20,12 @@ module ActiveRecord end end - def collection_without_template + def collection_without_template(_) @relation.preload_associations(@collection) if @relation super end - def collection_with_template + def collection_with_template(_) @relation.preload_associations(@collection) if @relation super end -- cgit v1.2.3 From 793455e594dc98ea026c7665dd0de3ae8829cf73 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 28 Jan 2019 14:40:30 -0800 Subject: Remove default parameters from method signature This method is private, and we always pass something in. --- actionview/lib/action_view/renderer/template_renderer.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index 6a50ccff7f..c36baeffcd 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -12,7 +12,7 @@ module ActionView @lookup_context.rendered_format ||= (template.formats.first || formats.first) - render_template(context, template, options[:layout], options[:locals]) + render_template(context, template, options[:layout], options[:locals] || {}) end private @@ -45,9 +45,7 @@ module ActionView # Renders the given template. A string representing the layout can be # supplied as well. - def render_template(view, template, layout_name = nil, locals = nil) - locals ||= {} - + def render_template(view, template, layout_name, locals) render_with_layout(view, layout_name, locals) do |layout| instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do template.render(view, locals) { |*name| view._layout_for(*name) } -- cgit v1.2.3 From 5bb1ad59b1fea70c82d0a3b654ef81a0a31c715c Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 28 Jan 2019 14:55:12 -0800 Subject: Pull `@template` in to a local variable This gets the PartialRenderer to be a bit closer to the TemplateRenderer. TemplateRenderer already keeps its template in a local variable. --- .../lib/action_view/renderer/abstract_renderer.rb | 2 -- .../lib/action_view/renderer/partial_renderer.rb | 31 +++++++++++----------- .../partial_renderer/collection_caching.rb | 16 +++++------ .../collection_cache_association_loading.rb | 4 +-- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 37f07eabc2..c2a88868a8 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -38,8 +38,6 @@ module ActionView end def instrument(name, **options) # :doc: - options[:identifier] ||= (@template && @template.identifier) || @path - ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| yield payload end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 42c6a97682..478400e016 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -296,41 +296,42 @@ module ActionView def render(context, options, block) setup(context, options, block) - @template = find_partial + template = find_partial @lookup_context.rendered_format ||= begin - if @template && @template.formats.present? - @template.formats.first + if template && template.formats.first + template.formats.first else formats.first end end if @collection - render_collection(context) + render_collection(context, template) else - render_partial(context) + render_partial(context, template) end end private - def render_collection(view) - instrument(:collection, count: @collection.size) do |payload| + def render_collection(view, template) + identifier = (template && template.identifier) || @path + instrument(:collection, identifier: identifier, 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 - cache_collection_render(payload, view) do - @template ? collection_with_template(view) : collection_without_template(view) + cache_collection_render(payload, view, template) do + template ? collection_with_template(view, template) : collection_without_template(view) end.join(spacer).html_safe end end - def render_partial(view) - instrument(:partial) do |payload| + def render_partial(view, template) + instrument(:partial, identifier: template.identifier) do |payload| locals, block = @locals, @block object, as = @object, @variable @@ -341,12 +342,12 @@ module ActionView object = locals[as] if object.nil? # Respect object when object is false locals[as] = object if @has_object - content = @template.render(view, locals) do |*name| + content = template.render(view, locals) do |*name| view._layout_for(*name, &block) end content = layout.render(view, locals) { content } if layout - payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path] + payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path] content end end @@ -422,8 +423,8 @@ module ActionView @lookup_context.find_template(path, prefixes, true, locals, @details) end - def collection_with_template(view) - locals, template = @locals, @template + def collection_with_template(view, template) + locals = @locals as, counter, iteration = @variable, @variable_counter, @variable_iteration if layout = @options[:layout] 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 2d4a171726..b2c4fe90f4 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -11,13 +11,13 @@ module ActionView end private - def cache_collection_render(instrumentation_payload, view) + def cache_collection_render(instrumentation_payload, view, template) return yield unless @options[:cached] # Result is a hash with the key represents the # key used for cache lookup and the value is the item # on which the partial is being rendered - keyed_collection = collection_by_cache_keys(view) + keyed_collection = collection_by_cache_keys(view, template) # Pull all partials from cache # Result is a hash, key matches the entry in @@ -51,21 +51,21 @@ module ActionView @options[:cached].respond_to?(:call) end - def collection_by_cache_keys(view) + def collection_by_cache_keys(view, template) seed = callable_cache_key? ? @options[:cached] : ->(i) { i } @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(seed.call(item), view)] = item + hash[expanded_cache_key(seed.call(item), view, template)] = item end end - def expanded_cache_key(key, view) - key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path(view))) + def expanded_cache_key(key, view, template) + key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path(view, template))) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end - def digest_path(view) - @digest_path ||= view.digest_path_from_virtual(@template.virtual_path) + def digest_path(view, template) + @digest_path ||= view.digest_path_from_virtual(template.virtual_path) end # `order_by` is an enumerable object containing keys of the cache, diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb index 4ef19f0929..dfaac4eefb 100644 --- a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb +++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb @@ -20,12 +20,12 @@ module ActiveRecord end end - def collection_without_template(_) + def collection_without_template(*) @relation.preload_associations(@collection) if @relation super end - def collection_with_template(_) + def collection_with_template(*) @relation.preload_associations(@collection) if @relation super end -- cgit v1.2.3 From bf9b7325f8814a4d90267e59697e5125f40a69c3 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 28 Jan 2019 15:01:48 -0800 Subject: Cache the digest path on the stack. We can remove the ivar by caching the digest on the stack --- .../renderer/partial_renderer/collection_caching.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 b2c4fe90f4..388f9e5e56 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -54,20 +54,18 @@ module ActionView def collection_by_cache_keys(view, template) seed = callable_cache_key? ? @options[:cached] : ->(i) { i } + digest_path = view.digest_path_from_virtual(template.virtual_path) + @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(seed.call(item), view, template)] = item + hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item end end - def expanded_cache_key(key, view, template) - key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path(view, template))) + def expanded_cache_key(key, view, template, digest_path) + key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end - def digest_path(view, template) - @digest_path ||= view.digest_path_from_virtual(template.virtual_path) - end - # `order_by` is an enumerable object containing keys of the cache, # all keys are passed in whether found already or not. # -- cgit v1.2.3 From 736db1586e559bfa104d8486558cd8108b26d166 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 28 Jan 2019 15:09:13 -0800 Subject: Remove `with_layout_format` delegation That method doesn't exist on LookupContext, so the delegate doesn't make sense. --- actionview/lib/action_view/renderer/abstract_renderer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index c2a88868a8..ae366ce54a 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -17,7 +17,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 :template_exists?, :any_templates?, :with_layout_format, :formats, to: :@lookup_context + delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context def initialize(lookup_context) @lookup_context = lookup_context -- cgit v1.2.3