aboutsummaryrefslogtreecommitdiffstats
path: root/actionview
diff options
context:
space:
mode:
Diffstat (limited to 'actionview')
-rw-r--r--actionview/lib/action_view.rb2
-rw-r--r--actionview/lib/action_view/file_template.rb33
-rw-r--r--actionview/lib/action_view/template.rb37
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb2
-rw-r--r--actionview/lib/action_view/template/resolver.rb42
-rw-r--r--actionview/lib/action_view/template/sources.rb13
-rw-r--r--actionview/lib/action_view/template/sources/file.rb17
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb4
-rw-r--r--actionview/lib/action_view/unbound_template.rb32
-rw-r--r--actionview/test/template/file_system_resolver_test.rb12
-rw-r--r--actionview/test/template/optimized_file_system_resolver_test.rb12
-rw-r--r--actionview/test/template/resolver_shared_tests.rb148
-rw-r--r--actionview/test/template/template_test.rb24
13 files changed, 283 insertions, 95 deletions
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
index f398ba0ff0..7f85bf2a5e 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -44,7 +44,7 @@ module ActionView
autoload :Rendering
autoload :RoutingUrlFor
autoload :Template
- autoload :FileTemplate
+ autoload :UnboundTemplate
autoload :ViewPaths
autoload_under "renderer" do
diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb
deleted file mode 100644
index dea02176eb..0000000000
--- a/actionview/lib/action_view/file_template.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require "action_view/template"
-
-module ActionView
- class FileTemplate < Template
- def initialize(filename, handler, details)
- @filename = filename
-
- super(nil, filename, handler, details)
- end
-
- def source
- File.binread @filename
- end
-
- def refresh(_)
- self
- end
-
- # Exceptions are marshalled when using the parallel test runner with DRb, so we need
- # to ensure that references to the template object can be marshalled as well. This means forgoing
- # the marshalling of the compiler mutex and instantiating that again on unmarshalling.
- def marshal_dump # :nodoc:
- [ @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ]
- end
-
- def marshal_load(array) # :nodoc:
- @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array
- @compile_mutex = Mutex.new
- end
- end
-end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 94f8a194a0..b80bf56c1b 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -117,13 +117,14 @@ module ActionView
autoload :Handlers
autoload :HTML
autoload :Inline
+ autoload :Sources
autoload :Text
autoload :Types
end
extend Template::Handlers
- attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
+ attr_reader :identifier, :handler, :original_encoding, :updated_at
attr_reader :variable, :format, :variant, :locals, :virtual_path
def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: nil)
@@ -164,6 +165,7 @@ module ActionView
deprecate def formats; Array(format); end
deprecate def variants=(_); end
deprecate def variants; [variant]; end
+ deprecate def refresh(_); self; end
# Returns whether the underlying handler supports streaming. If so,
# a streaming buffer *may* be passed when it starts rendering.
@@ -190,25 +192,6 @@ module ActionView
@type ||= Types[format]
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
- # anymore since it was already compiled. In such cases, all you need to do is to call
- # refresh passing in the view object.
- #
- # Notice this method raises an error if the template to be refreshed does not have a
- # virtual path set (true just for inline templates).
- def refresh(view)
- raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path
- lookup = view.lookup_context
- pieces = @virtual_path.split("/")
- name = pieces.pop
- partial = !!name.sub!(/^_/, "")
- lookup.disable_cache do
- lookup.find_template(name, [ pieces.join("/") ], partial, @locals)
- end
- end
-
def short_identifier
@short_identifier ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier
end
@@ -217,6 +200,10 @@ module ActionView
"#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
end
+ def source
+ @source.to_s
+ end
+
# This method is responsible for properly setting the encoding of the
# source. Until this point, we assume that the source is BINARY data.
# If no additional information is supplied, we assume the encoding is
@@ -298,9 +285,6 @@ module ActionView
compile(mod)
end
- # Just discard the source if we have a virtual path. This
- # means we can get the template back.
- @source = nil if @virtual_path
@compiled = true
end
end
@@ -368,12 +352,7 @@ module ActionView
e.sub_template_of(self)
raise e
else
- template = self
- unless template.source
- template = refresh(view)
- template.encode!
- end
- raise Template::Error.new(template)
+ raise Template::Error.new(self)
end
end
diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb
index 307b852440..e602aa117a 100644
--- a/actionview/lib/action_view/template/handlers/erb/erubi.rb
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -25,7 +25,7 @@ module ActionView
src = @src
view = Class.new(ActionView::Base) {
include action_view_erb_handler_context._routes.url_helpers
- class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0)
+ class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
}.empty
view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index d5cb3c823a..ce53eb046d 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -118,7 +118,7 @@ module ActionView
locals = locals.map(&:to_s).sort!.freeze
cached(key, [name, prefix, partial], details, locals) do
- find_templates(name, prefix, partial, details, locals)
+ _find_all(name, prefix, partial, details, key, locals)
end
end
@@ -131,6 +131,10 @@ module ActionView
private
+ def _find_all(name, prefix, partial, details, key, locals)
+ find_templates(name, prefix, partial, details, locals)
+ end
+
delegate :caching?, to: :class
# This is what child classes implement. No defaults are needed
@@ -169,33 +173,51 @@ module ActionView
else
@pattern = DEFAULT_PATTERN
end
+ @unbound_templates = Concurrent::Map.new
+ super()
+ end
+
+ def clear_cache
+ @unbound_templates.clear
super()
end
private
- def find_templates(name, prefix, partial, details, locals)
+ def _find_all(name, prefix, partial, details, key, locals)
path = Path.build(name, prefix, partial)
- query(path, details, details[:formats], locals)
+ query(path, details, details[:formats], locals, cache: !!key)
end
- def query(path, details, formats, locals)
+ def query(path, details, formats, locals, cache:)
template_paths = find_template_paths_from_details(path, details)
template_paths = reject_files_external_to_app(template_paths)
template_paths.map do |template|
- build_template(template, path.virtual, locals)
+ unbound_template =
+ if cache
+ @unbound_templates.compute_if_absent([template, path.virtual]) do
+ build_unbound_template(template, path.virtual)
+ end
+ else
+ build_unbound_template(template, path.virtual)
+ end
+
+ unbound_template.bind_locals(locals)
end
end
- def build_template(template, virtual_path, locals)
+ def build_unbound_template(template, virtual_path)
handler, format, variant = extract_handler_and_format_and_variant(template)
+ source = Template::Sources::File.new(template)
- FileTemplate.new(File.expand_path(template), handler,
+ UnboundTemplate.new(
+ source,
+ template,
+ handler,
virtual_path: virtual_path,
format: format,
variant: variant,
- locals: locals
)
end
@@ -361,8 +383,8 @@ module ActionView
[new(""), new("/")]
end
- def build_template(template, virtual_path, locals)
- super(template, nil, locals)
+ def build_unbound_template(template, _)
+ super(template, nil)
end
def reject_files_external_to_app(files)
diff --git a/actionview/lib/action_view/template/sources.rb b/actionview/lib/action_view/template/sources.rb
new file mode 100644
index 0000000000..8b89535741
--- /dev/null
+++ b/actionview/lib/action_view/template/sources.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActionView
+ class Template
+ module Sources
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :File
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/sources/file.rb b/actionview/lib/action_view/template/sources/file.rb
new file mode 100644
index 0000000000..2d3a7dec7f
--- /dev/null
+++ b/actionview/lib/action_view/template/sources/file.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionView
+ class Template
+ module Sources
+ class File
+ def initialize(filename)
+ @filename = filename
+ end
+
+ def to_s
+ ::File.binread @filename
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index a97fb71b26..1bedf44934 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -23,7 +23,7 @@ module ActionView #:nodoc:
private
- def query(path, exts, _, locals)
+ def query(path, exts, _, locals, cache:)
query = +""
EXTENSIONS.each do |ext, prefix|
query << "(" << exts[ext].map { |e| e && Regexp.escape("#{prefix}#{e}") }.join("|") << "|)"
@@ -47,7 +47,7 @@ module ActionView #:nodoc:
end
class NullResolver < PathResolver
- def query(path, exts, _, locals)
+ def query(path, exts, _, locals, cache:)
handler, format, variant = extract_handler_and_format_and_variant(path)
[ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant, locals: locals)]
end
diff --git a/actionview/lib/action_view/unbound_template.rb b/actionview/lib/action_view/unbound_template.rb
new file mode 100644
index 0000000000..db69b6d016
--- /dev/null
+++ b/actionview/lib/action_view/unbound_template.rb
@@ -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
diff --git a/actionview/test/template/file_system_resolver_test.rb b/actionview/test/template/file_system_resolver_test.rb
new file mode 100644
index 0000000000..aa03fdcb13
--- /dev/null
+++ b/actionview/test/template/file_system_resolver_test.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "template/resolver_shared_tests"
+
+class FileSystemResolverTest < ActiveSupport::TestCase
+ include ResolverSharedTests
+
+ def resolver
+ ActionView::FileSystemResolver.new(tmpdir)
+ end
+end
diff --git a/actionview/test/template/optimized_file_system_resolver_test.rb b/actionview/test/template/optimized_file_system_resolver_test.rb
new file mode 100644
index 0000000000..c0c64357ce
--- /dev/null
+++ b/actionview/test/template/optimized_file_system_resolver_test.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "template/resolver_shared_tests"
+
+class OptimizedFileSystemResolverTest < ActiveSupport::TestCase
+ include ResolverSharedTests
+
+ def resolver
+ ActionView::OptimizedFileSystemResolver.new(tmpdir)
+ end
+end
diff --git a/actionview/test/template/resolver_shared_tests.rb b/actionview/test/template/resolver_shared_tests.rb
new file mode 100644
index 0000000000..8b47c5bc89
--- /dev/null
+++ b/actionview/test/template/resolver_shared_tests.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+module ResolverSharedTests
+ attr_reader :tmpdir
+
+ def run(*args)
+ capture_exceptions do
+ Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super }
+ end
+ end
+
+ def with_file(filename, source = "File at #{filename}")
+ path = File.join(tmpdir, filename)
+ FileUtils.mkdir_p(File.dirname(path))
+ 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!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello default!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_nil templates[0].format
+ assert_nil templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::Raw, templates[0].handler
+ end
+
+ def test_can_find_with_just_handler
+ with_file "test/hello_world.erb", "Hello erb!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello erb!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_nil templates[0].format
+ assert_nil templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler
+ end
+
+ def test_can_find_with_format_and_handler
+ with_file "test/hello_world.text.builder", "Hello plain text!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html, :text], variants: [:phone], handlers: [:erb, :builder])
+ assert_equal 1, templates.size
+ assert_equal "Hello plain text!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_equal :text, templates[0].format
+ assert_nil templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::Builder, templates[0].handler
+ end
+
+ def test_can_find_with_variant_format_and_handler
+ with_file "test/hello_world.html+phone.erb", "Hello plain text!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello plain text!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_equal :html, templates[0].format
+ assert_equal "phone", templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler
+ end
+
+ def test_can_find_with_any_variant_format_and_handler
+ with_file "test/hello_world.html+phone.erb", "Hello plain text!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: :any, handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello plain text!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_equal :html, templates[0].format
+ assert_equal "phone", templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler
+ end
+
+ def test_doesnt_find_template_with_wrong_details
+ with_file "test/hello_world.html.erb", "Hello plain text!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:xml], variants: :any, handlers: [:builder])
+ assert_equal 0, templates.size
+
+ 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_different_templates_when_cache_disabled
+ with_file "test/hello_world.html.erb", "Hello HTML!"
+
+ a = context.find("hello_world", "test", false, [], {})
+ b = context.disable_cache { context.find("hello_world", "test", false, [], {}) }
+ c = context.find("hello_world", "test", false, [], {})
+
+ # disable_cache should give us a new object
+ assert_not_same a, b
+
+ # but it should not clear the cache
+ assert_same a, c
+ end
+
+ def test_same_template_from_different_details_is_same_object
+ with_file "test/hello_world.html.erb", "Hello HTML!"
+
+ 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_templates_with_optional_locale_shares_common_object
+ with_file "test/hello_world.text.erb", "Generic plain text!"
+ with_file "test/hello_world.fr.text.erb", "Texte en Francais!"
+
+ en = context.find_all("hello_world", "test", false, [], locale: [:en])
+ fr = context.find_all("hello_world", "test", false, [], locale: [:fr])
+
+ assert_equal 1, en.size
+ assert_equal 2, fr.size
+
+ assert_equal "Generic plain text!", en[0].source
+ assert_equal "Texte en Francais!", fr[0].source
+ assert_equal "Generic plain text!", fr[1].source
+
+ assert_same en[0], fr[1]
+ 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
diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb
index 71fb99115b..049e0bc129 100644
--- a/actionview/test/template/template_test.rb
+++ b/actionview/test/template/template_test.rb
@@ -91,10 +91,10 @@ class TestERBTemplate < ActiveSupport::TestCase
assert_equal "<%= hello %>", render
end
- def test_template_loses_its_source_after_rendering
+ def test_template_does_not_lose_its_source_after_rendering
@template = new_template
render
- assert_nil @template.source
+ assert_equal "<%= hello %>", @template.source
end
def test_template_does_not_lose_its_source_after_rendering_if_it_does_not_have_a_virtual_path
@@ -121,24 +121,10 @@ class TestERBTemplate < ActiveSupport::TestCase
assert_equal "hellopartialhello", render
end
- def test_refresh_with_templates
+ def test_refresh_is_deprecated
@template = new_template("Hello", virtual_path: "test/foo/bar", locals: [:key])
- assert_called_with(@context.lookup_context, :find_template, ["bar", %w(test/foo), false, [:key]], returns: "template") do
- assert_equal "template", @template.refresh(@context)
- end
- end
-
- def test_refresh_with_partials
- @template = new_template("Hello", virtual_path: "test/_foo", locals: [:key])
- assert_called_with(@context.lookup_context, :find_template, ["foo", %w(test), true, [:key]], returns: "partial") do
- assert_equal "partial", @template.refresh(@context)
- end
- end
-
- def test_refresh_raises_an_error_without_virtual_path
- @template = new_template("Hello", virtual_path: nil)
- assert_raise RuntimeError do
- @template.refresh(@context)
+ assert_deprecated do
+ assert_same @template, @template.refresh(@context)
end
end