Encapsulate "details" into TemplateDetails

When dealing with the "details" for a template: locale, format,
variant, and handler, previously we would store these in an ad-hoc way
every place we did. Often as a hash or as separate instance variables on
a class.

This PR attempts to simplify this by encapsulating known details on a
template in a new ActionView::TemplateDetails class, and requested
details in ActionView::TemplateDetails::Requested.

This allowed extracting and simplifying filtering and sorting logic from
the Resolver class as well as extracting default format logic from
UnboundTemplate.

As well as reducing complexity, in the future this should make it
possible to provide suggestions on missing template errors due to
mismatched details, and might allow improved performance.

At least for now these new classes are private (:nodoc)

Co-authored-by: John Crepezzi <john.crepezzi@gmail.com>
This commit is contained in:
John Hawthorn 2021-05-11 16:32:02 -07:00
parent f55010a179
commit f8f9a085cc
4 changed files with 97 additions and 57 deletions

@ -44,6 +44,7 @@ module ActionView
autoload :Rendering
autoload :RoutingUrlFor
autoload :Template
autoload :TemplateDetails
autoload :TemplatePath
autoload :UnboundTemplate
autoload :ViewPaths

@ -13,9 +13,9 @@ class Resolver
Path = ActionView::TemplatePath
deprecate_constant :Path
TemplateDetails = Struct.new(:path, :locale, :handler, :format, :variant)
class PathParser # :nodoc:
ParsedPath = Struct.new(:path, :details)
def build_path_regex
handlers = Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|")
formats = Template::Types.symbols.map { |x| Regexp.escape(x) }.join("|")
@ -39,13 +39,13 @@ def parse(path)
@regex ||= build_path_regex
match = @regex.match(path)
path = TemplatePath.build(match[:action], match[:prefix] || "", !!match[:partial])
TemplateDetails.new(
path,
details = TemplateDetails.new(
match[:locale]&.to_sym,
match[:handler]&.to_sym,
match[:format]&.to_sym,
match[:variant]
match[:variant]&.to_sym
)
ParsedPath.new(path, details)
end
end
@ -205,10 +205,11 @@ def all_template_paths # :nodoc:
private
def _find_all(name, prefix, partial, details, key, locals)
path = TemplatePath.build(name, prefix, partial)
query(path, details, details[:formats], locals, cache: !!key)
requested_details = TemplateDetails::Requested.new(**details)
query(path, requested_details, locals, cache: !!key)
end
def query(path, details, formats, locals, cache:)
def query(path, requested_details, locals, cache:)
cache = cache ? @unbound_templates : Concurrent::Map.new
unbound_templates =
@ -216,7 +217,7 @@ def query(path, details, formats, locals, cache:)
unbound_templates_from_path(path)
end
filter_and_sort_by_details(unbound_templates, details).map do |unbound_template|
filter_and_sort_by_details(unbound_templates, requested_details).map do |unbound_template|
unbound_template.bind_locals(locals)
end
end
@ -226,17 +227,15 @@ def source_for_template(template)
end
def build_unbound_template(template)
details = @path_parser.parse(template.from(@path.size + 1))
parsed = @path_parser.parse(template.from(@path.size + 1))
details = parsed.details
source = source_for_template(template)
UnboundTemplate.new(
source,
template,
details.handler,
virtual_path: details.path.virtual,
locale: details.locale,
format: details.format,
variant: details.variant,
details: details,
virtual_path: parsed.path.virtual,
)
end
@ -257,39 +256,18 @@ def unbound_templates_from_path(path)
end
end
def filter_and_sort_by_details(templates, details)
locale = details[:locale]
formats = details[:formats]
variants = details[:variants]
handlers = details[:handlers]
results = templates.map do |template|
locale_match = details_match_sort_key(template.locale, locale) || next
format_match = details_match_sort_key(template.format, formats) || next
variant_match =
if variants == :any
template.variant ? 1 : 0
else
details_match_sort_key(template.variant&.to_sym, variants) || next
end
handler_match = details_match_sort_key(template.handler, handlers) || next
[template, [locale_match, format_match, variant_match, handler_match]]
def filter_and_sort_by_details(templates, requested_details)
filtered_templates = templates.select do |template|
template.details.matches?(requested_details)
end
results.compact!
results.sort_by!(&:last) if results.size > 1
results.map!(&:first)
results
end
def details_match_sort_key(have, want)
if have
want.index(have)
else
want.size
if filtered_templates.count > 1
filtered_templates.sort_by! do |template|
template.details.sort_key_for(requested_details)
end
end
filtered_templates
end
# Safe glob within @path

@ -0,0 +1,67 @@
# frozen_string_literal: true
module ActionView
class TemplateDetails # :nodoc:
class Requested
attr_reader :locale, :handlers, :formats, :variants
def initialize(locale:, handlers:, formats:, variants:)
@locale = locale
@handlers = handlers
@formats = formats
@variants = variants
end
end
attr_reader :locale, :handler, :format, :variant
def initialize(locale, handler, format, variant)
@locale = locale
@handler = handler
@format = format
@variant = variant
end
def matches?(requested)
return if format && !requested.formats.include?(format)
return if locale && !requested.locale.include?(locale)
unless requested.variants == :any
return if variant && !requested.variants.include?(variant)
end
return if handler && !requested.handlers.include?(handler)
true
end
def sort_key_for(requested)
locale_match = details_match_sort_key(locale, requested.locale)
format_match = details_match_sort_key(format, requested.formats)
variant_match =
if requested.variants == :any
variant ? 1 : 0
else
details_match_sort_key(variant, requested.variants)
end
handler_match = details_match_sort_key(handler, requested.handlers)
[locale_match, format_match, variant_match, handler_match]
end
def handler_class
Template.handler_for_extension(handler)
end
def format_or_default
format || handler_class.try(:default_format)
end
private
def details_match_sort_key(have, want)
if have
want.index(have)
else
want.size
end
end
end
end

@ -4,16 +4,13 @@
module ActionView
class UnboundTemplate
attr_reader :handler, :format, :variant, :locale, :virtual_path
attr_reader :virtual_path, :details
delegate :locale, :format, :variant, :handler, to: :@details
def initialize(source, identifier, handler, format:, variant:, locale:, virtual_path:)
def initialize(source, identifier, details:, virtual_path:)
@source = source
@identifier = identifier
@handler = handler
@format = format
@variant = variant
@locale = locale
@details = details
@virtual_path = virtual_path
@templates = Concurrent::Map.new(initial_capacity: 2)
@ -25,16 +22,13 @@ def bind_locals(locals)
private
def build_template(locals)
handler = Template.handler_for_extension(@handler)
format = @format || handler.try(:default_format)
Template.new(
@source,
@identifier,
handler,
details.handler_class,
format: format,
variant: @variant,
format: details.format_or_default,
variant: variant&.to_s,
virtual_path: @virtual_path,
locals: locals