De-dup Templates, introduce UnboundTemplate

Previously it's possible to have multiple copies of the "same" Template.
For example, if index.html.erb is found both the :en and :fr locale, it
will return a different Template object for each. The same can happen
with formats, variants, and handlers.

This commit de-duplicates templates, there will now only be one template
per file/virtual_path/locals tuple.

We need to consider virtual_path because both `render "index"`, and
`render "index.html"` can both find the same file but will have
different virtual_paths. IMO this is rare and should be
deprecated/removed, but it exists now so we need to consider it in order
to cache correctly.

This commit introduces a new UnboundTemplate class, which represents a
template with unknown locals. Template objects can be built from it by
using `#with_locals`. Currently, this is just a convenience around
caching templates, but I hope it's a helpful concept that could have
more utility in the future.
This commit is contained in:
John Hawthorn 2019-04-11 17:14:16 -07:00
parent 80e2aaa80a
commit 1fc735e5f5
4 changed files with 81 additions and 9 deletions

View File

@ -44,6 +44,7 @@ module ActionView
autoload :Rendering
autoload :RoutingUrlFor
autoload :Template
autoload :UnboundTemplate
autoload :ViewPaths
autoload_under "renderer" do

View File

@ -169,6 +169,12 @@ module ActionView
else
@pattern = DEFAULT_PATTERN
end
@unbound_templates = Concurrent::Map.new
super()
end
def clear_cache
@unbound_templates.clear
super()
end
@ -189,16 +195,19 @@ module ActionView
end
def build_template(template, virtual_path, locals)
handler, format, variant = extract_handler_and_format_and_variant(template)
@unbound_templates.compute_if_absent([template, virtual_path]) do
handler, format, variant = extract_handler_and_format_and_variant(template)
source = Template::Sources::File.new(template)
filename = File.expand_path(template)
source = Template::Sources::File.new(filename)
Template.new(source, filename, handler,
virtual_path: virtual_path,
format: format,
variant: variant,
locals: locals
)
UnboundTemplate.new(
source,
template,
handler,
virtual_path: virtual_path,
format: format,
variant: variant,
)
end.bind_locals(locals)
end
def reject_files_external_to_app(files)

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require "concurrent/map"
module ActionView
class UnboundTemplate
def initialize(source, identifer, handler, options)
@source = source
@identifer = identifer
@handler = handler
@options = options
@templates = Concurrent::Map.new(initial_capacity: 2)
end
def bind_locals(locals)
@templates[locals] ||= build_template(locals)
end
private
def build_template(locals)
options = @options.merge(locals: locals)
Template.new(
@source,
@identifer,
@handler,
options
)
end
end
end

View File

@ -13,6 +13,10 @@ module ResolverSharedTests
File.write(path, source)
end
def context
@context ||= ActionView::LookupContext.new(resolver)
end
def test_can_find_with_no_extensions
with_file "test/hello_world", "Hello default!"
@ -82,4 +86,30 @@ module ResolverSharedTests
templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:xml], variants: :any, handlers: [:erb])
assert_equal 0, templates.size
end
def test_found_template_is_cached
with_file "test/hello_world.html.erb", "Hello HTML!"
a = context.find("hello_world", "test", false, [], {})
b = context.find("hello_world", "test", false, [], {})
assert_same a, b
end
def test_same_template_from_different_details_is_same_object
with_file "test/hello_world.html.erb", "Hello plain text!"
a = context.find("hello_world", "test", false, [], locale: [:en])
b = context.find("hello_world", "test", false, [], locale: [:fr])
assert_same a, b
end
def test_virtual_path_is_preserved_with_dot
with_file "test/hello_world.html.erb", "Hello html!"
template = context.find("hello_world.html", "test", false, [], {})
assert_equal "test/hello_world.html", template.virtual_path
template = context.find("hello_world", "test", false, [], {})
assert_equal "test/hello_world", template.virtual_path
end
end