2017-07-23 11:36:41 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2009-04-23 18:58:38 -04:00
|
|
|
require "pathname"
|
2009-09-02 18:00:22 -04:00
|
|
|
require "active_support/core_ext/class"
|
2013-12-02 16:36:58 -05:00
|
|
|
require "active_support/core_ext/module/attribute_accessors"
|
2017-10-21 09:21:02 -04:00
|
|
|
require "action_view/template"
|
2012-05-20 01:03:34 -04:00
|
|
|
require "thread"
|
2015-12-23 12:40:23 -05:00
|
|
|
require "concurrent/map"
|
2009-04-23 18:58:38 -04:00
|
|
|
|
2009-04-14 20:22:51 -04:00
|
|
|
module ActionView
|
2010-06-20 16:26:31 -04:00
|
|
|
# = Action View Resolver
|
2009-06-17 18:32:55 -04:00
|
|
|
class Resolver
|
2011-03-19 18:21:11 -04:00
|
|
|
# Keeps all information about view path and builds virtual path.
|
2012-06-21 15:13:13 -04:00
|
|
|
class Path
|
2011-03-19 00:13:59 -04:00
|
|
|
attr_reader :name, :prefix, :partial, :virtual
|
|
|
|
alias_method :partial?, :partial
|
|
|
|
|
2011-05-09 04:42:54 -04:00
|
|
|
def self.build(name, prefix, partial)
|
2018-05-17 04:32:27 -04:00
|
|
|
virtual = +""
|
2011-05-09 04:42:54 -04:00
|
|
|
virtual << "#{prefix}/" unless prefix.empty?
|
|
|
|
virtual << (partial ? "_#{name}" : name)
|
|
|
|
new name, prefix, partial, virtual
|
2011-03-19 00:13:59 -04:00
|
|
|
end
|
|
|
|
|
2011-05-09 04:42:54 -04:00
|
|
|
def initialize(name, prefix, partial, virtual)
|
2012-06-21 15:13:13 -04:00
|
|
|
@name = name
|
|
|
|
@prefix = prefix
|
|
|
|
@partial = partial
|
|
|
|
@virtual = virtual
|
2011-03-19 00:13:59 -04:00
|
|
|
end
|
2012-06-21 15:13:13 -04:00
|
|
|
|
|
|
|
def to_str
|
|
|
|
@virtual
|
|
|
|
end
|
|
|
|
alias :to_s :to_str
|
2011-03-19 00:13:59 -04:00
|
|
|
end
|
|
|
|
|
2012-05-20 01:03:34 -04:00
|
|
|
# Threadsafe template cache
|
|
|
|
class Cache #:nodoc:
|
2015-09-19 09:56:26 -04:00
|
|
|
class SmallCache < Concurrent::Map
|
2012-12-13 08:47:33 -05:00
|
|
|
def initialize(options = {})
|
2016-08-06 13:36:34 -04:00
|
|
|
super(options.merge(initial_capacity: 2))
|
2012-12-13 08:47:33 -05:00
|
|
|
end
|
2012-05-21 09:41:04 -04:00
|
|
|
end
|
|
|
|
|
2012-12-13 08:47:33 -05:00
|
|
|
# preallocate all the default blocks for performance/memory consumption reasons
|
2016-08-16 03:30:11 -04:00
|
|
|
PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new }
|
|
|
|
PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) }
|
|
|
|
NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) }
|
|
|
|
KEY_BLOCK = lambda { |cache, key| cache[key] = SmallCache.new(&NAME_BLOCK) }
|
2012-12-13 08:47:33 -05:00
|
|
|
|
2013-04-12 10:35:02 -04:00
|
|
|
# usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
|
2012-12-13 08:47:33 -05:00
|
|
|
NO_TEMPLATES = [].freeze
|
|
|
|
|
2012-05-20 01:03:34 -04:00
|
|
|
def initialize
|
2012-12-13 08:47:33 -05:00
|
|
|
@data = SmallCache.new(&KEY_BLOCK)
|
2015-07-15 17:32:45 -04:00
|
|
|
@query_cache = SmallCache.new
|
2012-05-20 01:03:34 -04:00
|
|
|
end
|
|
|
|
|
2016-04-19 13:59:37 -04:00
|
|
|
def inspect
|
|
|
|
"#<#{self.class.name}:0x#{(object_id << 1).to_s(16)} keys=#{@data.size} queries=#{@query_cache.size}>"
|
|
|
|
end
|
|
|
|
|
2012-05-20 01:03:34 -04:00
|
|
|
# Cache the templates returned by the block
|
|
|
|
def cache(key, name, prefix, partial, locals)
|
2019-03-14 14:33:28 -04:00
|
|
|
@data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
|
2012-05-20 01:03:34 -04:00
|
|
|
end
|
|
|
|
|
2015-07-15 17:32:45 -04:00
|
|
|
def cache_query(query) # :nodoc:
|
2019-03-14 14:33:28 -04:00
|
|
|
@query_cache[query] ||= canonical_no_templates(yield)
|
2015-07-15 17:32:45 -04:00
|
|
|
end
|
|
|
|
|
2012-05-20 01:03:34 -04:00
|
|
|
def clear
|
2012-12-13 08:47:33 -05:00
|
|
|
@data.clear
|
2015-07-15 17:32:45 -04:00
|
|
|
@query_cache.clear
|
2012-05-20 01:03:34 -04:00
|
|
|
end
|
2012-05-21 21:31:40 -04:00
|
|
|
|
2016-05-17 14:28:40 -04:00
|
|
|
# Get the cache size. Do not call this
|
|
|
|
# method. This method is not guaranteed to be here ever.
|
|
|
|
def size # :nodoc:
|
|
|
|
size = 0
|
|
|
|
@data.each_value do |v1|
|
|
|
|
v1.each_value do |v2|
|
|
|
|
v2.each_value do |v3|
|
|
|
|
v3.each_value do |v4|
|
|
|
|
size += v4.size
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
size + @query_cache.size
|
|
|
|
end
|
|
|
|
|
2012-05-21 21:31:40 -04:00
|
|
|
private
|
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
def canonical_no_templates(templates)
|
|
|
|
templates.empty? ? NO_TEMPLATES : templates
|
2012-05-21 21:31:40 -04:00
|
|
|
end
|
2012-05-20 01:03:34 -04:00
|
|
|
end
|
|
|
|
|
2017-05-31 05:16:20 -04:00
|
|
|
cattr_accessor :caching, default: true
|
2010-12-16 15:37:48 -05:00
|
|
|
|
|
|
|
class << self
|
|
|
|
alias :caching? :caching
|
|
|
|
end
|
|
|
|
|
2010-03-07 06:49:27 -05:00
|
|
|
def initialize
|
2012-05-20 01:03:34 -04:00
|
|
|
@cache = Cache.new
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
|
|
|
|
2010-03-08 17:13:24 -05:00
|
|
|
def clear_cache
|
2012-05-20 01:03:34 -04:00
|
|
|
@cache.clear
|
2010-03-08 17:13:24 -05:00
|
|
|
end
|
|
|
|
|
2013-03-06 08:20:09 -05:00
|
|
|
# Normalizes the arguments and passes it on to find_templates.
|
2016-10-28 23:05:58 -04:00
|
|
|
def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
|
2019-02-25 18:14:53 -05:00
|
|
|
locals = locals.map(&:to_s).sort!.freeze
|
|
|
|
|
2010-10-10 03:24:17 -04:00
|
|
|
cached(key, [name, prefix, partial], details, locals) do
|
2019-04-12 18:40:33 -04:00
|
|
|
_find_all(name, prefix, partial, details, key, locals)
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2009-04-22 20:16:28 -04:00
|
|
|
end
|
2009-09-02 18:00:22 -04:00
|
|
|
|
2019-04-01 19:35:07 -04:00
|
|
|
alias :find_all_anywhere :find_all
|
|
|
|
deprecate :find_all_anywhere
|
2016-01-20 13:39:19 -05:00
|
|
|
|
2015-07-15 17:32:45 -04:00
|
|
|
def find_all_with_query(query) # :nodoc:
|
|
|
|
@cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
|
|
|
|
end
|
|
|
|
|
2009-06-17 18:32:55 -04:00
|
|
|
private
|
|
|
|
|
2019-04-12 18:40:33 -04:00
|
|
|
def _find_all(name, prefix, partial, details, key, locals)
|
|
|
|
find_templates(name, prefix, partial, details, locals)
|
|
|
|
end
|
|
|
|
|
2013-01-06 04:43:30 -05:00
|
|
|
delegate :caching?, to: :class
|
2010-03-07 06:49:27 -05:00
|
|
|
|
2009-06-17 18:32:55 -04:00
|
|
|
# This is what child classes implement. No defaults are needed
|
|
|
|
# because Resolver guarantees that the arguments are present and
|
|
|
|
# normalized.
|
2019-04-01 19:35:07 -04:00
|
|
|
def find_templates(name, prefix, partial, details, locals = [])
|
|
|
|
raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
|
2010-10-07 07:26:58 -04:00
|
|
|
end
|
|
|
|
|
2010-12-27 02:44:51 -05:00
|
|
|
# Handles templates caching. If a key is given and caching is on
|
2010-10-10 03:24:17 -04:00
|
|
|
# always check the cache before hitting the resolver. Otherwise,
|
2012-05-20 01:03:34 -04:00
|
|
|
# it always hits the resolver but if the key is present, check if the
|
|
|
|
# resolver is fresher before returning it.
|
2016-12-24 08:08:23 -05:00
|
|
|
def cached(key, path_info, details, locals)
|
2010-10-10 03:24:17 -04:00
|
|
|
name, prefix, partial = path_info
|
|
|
|
|
2012-05-20 01:03:34 -04:00
|
|
|
if key
|
|
|
|
@cache.cache(key, name, prefix, partial, locals) do
|
2019-02-25 19:33:05 -05:00
|
|
|
yield
|
2012-05-21 15:01:02 -04:00
|
|
|
end
|
2012-05-20 01:03:34 -04:00
|
|
|
else
|
2019-02-25 19:33:05 -05:00
|
|
|
yield
|
2010-10-07 09:50:20 -04:00
|
|
|
end
|
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
2009-04-14 20:22:51 -04:00
|
|
|
|
2011-05-09 05:17:24 -04:00
|
|
|
# An abstract class that implements a Resolver with path semantics.
|
|
|
|
class PathResolver < Resolver #:nodoc:
|
2016-08-06 13:36:34 -04:00
|
|
|
EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
|
2013-12-03 05:17:01 -05:00
|
|
|
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
|
2011-03-19 00:13:59 -04:00
|
|
|
|
2016-10-28 23:05:58 -04:00
|
|
|
def initialize(pattern = nil)
|
2019-03-18 02:53:39 -04:00
|
|
|
if pattern
|
|
|
|
ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
|
|
|
|
@pattern = pattern
|
|
|
|
else
|
|
|
|
@pattern = DEFAULT_PATTERN
|
|
|
|
end
|
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.
2019-04-11 20:14:16 -04:00
|
|
|
@unbound_templates = Concurrent::Map.new
|
|
|
|
super()
|
|
|
|
end
|
|
|
|
|
|
|
|
def clear_cache
|
|
|
|
@unbound_templates.clear
|
2011-03-19 00:13:59 -04:00
|
|
|
super()
|
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-10-07 07:26:58 -04:00
|
|
|
private
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2019-04-12 18:40:33 -04:00
|
|
|
def _find_all(name, prefix, partial, details, key, locals)
|
2016-08-06 13:55:02 -04:00
|
|
|
path = Path.build(name, prefix, partial)
|
2019-04-12 18:40:33 -04:00
|
|
|
query(path, details, details[:formats], locals, cache: !!key)
|
2016-08-06 13:55:02 -04:00
|
|
|
end
|
2009-09-02 18:00:22 -04:00
|
|
|
|
2019-04-12 13:20:04 -04:00
|
|
|
def query(path, details, formats, locals, cache:)
|
2018-09-12 13:50:38 -04:00
|
|
|
template_paths = find_template_paths_from_details(path, details)
|
2019-04-01 19:35:07 -04:00
|
|
|
template_paths = reject_files_external_to_app(template_paths)
|
2010-05-17 11:41:54 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
template_paths.map do |template|
|
2019-04-12 13:20:04 -04:00
|
|
|
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)
|
2016-08-06 13:55:02 -04:00
|
|
|
end
|
2015-06-20 17:37:01 -04:00
|
|
|
end
|
2009-04-23 18:58:38 -04:00
|
|
|
|
2019-04-12 13:20:04 -04:00
|
|
|
def build_unbound_template(template, virtual_path)
|
|
|
|
handler, format, variant = extract_handler_and_format_and_variant(template)
|
|
|
|
source = Template::Sources::File.new(template)
|
|
|
|
|
|
|
|
UnboundTemplate.new(
|
|
|
|
source,
|
|
|
|
template,
|
|
|
|
handler,
|
|
|
|
virtual_path: virtual_path,
|
|
|
|
format: format,
|
|
|
|
variant: variant,
|
|
|
|
)
|
2019-03-18 14:57:05 -04:00
|
|
|
end
|
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
def reject_files_external_to_app(files)
|
|
|
|
files.reject { |filename| !inside_path?(@path, filename) }
|
|
|
|
end
|
2016-01-20 13:39:19 -05:00
|
|
|
|
2018-09-12 13:50:38 -04:00
|
|
|
def find_template_paths_from_details(path, details)
|
|
|
|
query = build_query(path, details)
|
|
|
|
find_template_paths(query)
|
|
|
|
end
|
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
def find_template_paths(query)
|
|
|
|
Dir[query].uniq.reject do |filename|
|
|
|
|
File.directory?(filename) ||
|
|
|
|
# deals with case-insensitive file systems.
|
|
|
|
!File.fnmatch(query, filename, File::FNM_EXTGLOB)
|
|
|
|
end
|
2015-06-20 17:37:01 -04:00
|
|
|
end
|
2014-05-10 14:52:13 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
def inside_path?(path, filename)
|
|
|
|
filename = File.expand_path(filename)
|
|
|
|
path = File.join(path, "")
|
|
|
|
filename.start_with?(path)
|
|
|
|
end
|
2016-01-20 13:39:19 -05:00
|
|
|
|
2016-09-14 04:57:52 -04:00
|
|
|
# Helper for building query glob string based on resolver's pattern.
|
2016-08-06 13:55:02 -04:00
|
|
|
def build_query(path, details)
|
|
|
|
query = @pattern.dup
|
2011-08-16 18:16:45 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
|
|
|
|
query.gsub!(/:prefix(\/)?/, prefix)
|
2011-08-16 18:16:45 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
|
|
|
|
query.gsub!(/:action/, partial)
|
2011-03-19 00:13:59 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
details.each do |ext, candidates|
|
|
|
|
if ext == :variants && candidates == :any
|
|
|
|
query.gsub!(/:#{ext}/, "*")
|
|
|
|
else
|
|
|
|
query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
|
|
|
|
end
|
2016-03-01 03:58:40 -05:00
|
|
|
end
|
2011-03-19 00:13:59 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
File.expand_path(query, @path)
|
|
|
|
end
|
2011-03-19 00:13:59 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
def escape_entry(entry)
|
2018-02-27 23:33:37 -05:00
|
|
|
entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
|
2016-08-06 13:55:02 -04:00
|
|
|
end
|
2011-08-16 18:16:45 -04:00
|
|
|
|
2016-09-14 04:57:52 -04:00
|
|
|
# Extract handler, formats and variant from path. If a format cannot be found neither
|
|
|
|
# from the path, or the handler, we should return the array of formats given
|
|
|
|
# to the resolver.
|
2019-02-26 01:56:13 -05:00
|
|
|
def extract_handler_and_format_and_variant(path)
|
2018-02-27 23:33:37 -05:00
|
|
|
pieces = File.basename(path).split(".")
|
2016-08-06 13:55:02 -04:00
|
|
|
pieces.shift
|
2012-10-29 11:22:59 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
extension = pieces.pop
|
2012-10-29 11:22:59 -04:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
handler = Template.handler_for_extension(extension)
|
|
|
|
format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
|
2019-02-25 14:53:55 -05:00
|
|
|
format = if format
|
2019-02-25 14:58:19 -05:00
|
|
|
Template::Types[format]&.ref
|
2019-02-25 14:53:55 -05:00
|
|
|
else
|
|
|
|
if handler.respond_to?(:default_format) # default_format can return nil
|
|
|
|
handler.default_format
|
|
|
|
else
|
2019-02-25 22:33:50 -05:00
|
|
|
nil
|
2019-02-25 14:53:55 -05:00
|
|
|
end
|
|
|
|
end
|
2013-12-03 05:17:01 -05:00
|
|
|
|
2019-02-25 14:53:55 -05:00
|
|
|
# Template::Types[format] and handler.default_format can return nil
|
2019-02-25 22:33:50 -05:00
|
|
|
[handler, format, variant]
|
2016-08-06 13:55:02 -04:00
|
|
|
end
|
2009-04-14 20:22:51 -04:00
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2019-03-18 02:53:39 -04:00
|
|
|
# A resolver that loads files from the filesystem.
|
2009-09-02 18:00:22 -04:00
|
|
|
class FileSystemResolver < PathResolver
|
2019-04-01 15:22:57 -04:00
|
|
|
attr_reader :path
|
|
|
|
|
2016-10-28 23:05:58 -04:00
|
|
|
def initialize(path, pattern = nil)
|
2009-09-02 18:00:22 -04:00
|
|
|
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
|
2011-03-19 00:13:59 -04:00
|
|
|
super(pattern)
|
2010-06-23 19:04:41 -04:00
|
|
|
@path = File.expand_path(path)
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-10-07 07:26:58 -04:00
|
|
|
def to_s
|
|
|
|
@path.to_s
|
|
|
|
end
|
|
|
|
alias :to_path :to_s
|
|
|
|
|
2010-03-08 10:32:40 -05:00
|
|
|
def eql?(resolver)
|
|
|
|
self.class.equal?(resolver.class) && to_path == resolver.to_path
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2010-03-08 10:32:40 -05:00
|
|
|
alias :== :eql?
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
2010-10-10 17:11:50 -04:00
|
|
|
|
2011-05-09 05:17:24 -04:00
|
|
|
# An Optimized resolver for Rails' most common case.
|
|
|
|
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
|
2019-03-18 02:53:39 -04:00
|
|
|
def initialize(path)
|
|
|
|
super(path)
|
|
|
|
end
|
|
|
|
|
2018-09-12 13:50:38 -04:00
|
|
|
private
|
2011-05-09 05:17:24 -04:00
|
|
|
|
2018-09-12 13:50:38 -04:00
|
|
|
def find_template_paths_from_details(path, details)
|
|
|
|
# Instead of checking for every possible path, as our other globs would
|
|
|
|
# do, scan the directory for files with the right prefix.
|
|
|
|
query = "#{escape_entry(File.join(@path, path))}*"
|
|
|
|
|
|
|
|
regex = build_regex(path, details)
|
|
|
|
|
|
|
|
Dir[query].uniq.reject do |filename|
|
|
|
|
# This regex match does double duty of finding only files which match
|
|
|
|
# details (instead of just matching the prefix) and also filtering for
|
|
|
|
# case-insensitive file systems.
|
2018-12-12 16:29:29 -05:00
|
|
|
!regex.match?(filename) ||
|
2018-09-12 13:50:38 -04:00
|
|
|
File.directory?(filename)
|
|
|
|
end.sort_by do |filename|
|
|
|
|
# Because we scanned the directory, instead of checking for files
|
|
|
|
# one-by-one, they will be returned in an arbitrary order.
|
|
|
|
# We can use the matches found by the regex and sort by their index in
|
|
|
|
# details.
|
|
|
|
match = filename.match(regex)
|
|
|
|
EXTENSIONS.keys.reverse.map do |ext|
|
2018-09-12 21:21:18 -04:00
|
|
|
if ext == :variants && details[ext] == :any
|
|
|
|
match[ext].nil? ? 0 : 1
|
|
|
|
elsif match[ext].nil?
|
2018-09-12 13:50:38 -04:00
|
|
|
# No match should be last
|
|
|
|
details[ext].length
|
|
|
|
else
|
|
|
|
found = match[ext].to_sym
|
|
|
|
details[ext].index(found)
|
|
|
|
end
|
|
|
|
end
|
Lock down new `ImplicitRender` behavior for 5.0 RC
1. Conceptually revert #20276
The feature was implemented for the `responders` gem. In the end,
they did not need that feature, and have found a better fix (see
plataformatec/responders#131).
`ImplicitRender` is the place where Rails specifies our default
policies for the case where the user did not explicitly tell us
what to render, essentially describing a set of heuristics. If
the gem (or the user) knows exactly what they want, they could
just perform the correct `render` to avoid falling through to
here, as `responders` did (the user called `respond_with`).
Reverting the patch allows us to avoid exploding the complexity
and defining “the fallback for a fallback” policies.
2. `respond_to` and templates are considered exhaustive enumerations
If the user specified a list of formats/variants in a `respond_to`
block, anything that is not explicitly included should result
in an `UnknownFormat` error (which is then caught upstream to
mean “406 Not Acceptable” by default). This is already how it
works before this commit.
Same goes for templates – if the user defined a set of templates
(usually in the file system), that set is now considered exhaustive,
which means that “missing” templates are considered `UnknownFormat`
errors (406).
3. To keep API endpoints simple, the implicit render behavior for
actions with no templates defined at all (regardless of formats,
locales, variants, etc) are defaulted to “204 No Content”. This
is a strictly narrower version of the feature landed in #19036 and
#19377.
4. To avoid confusion when interacting in the browser, these actions
will raise an `UnknownFormat` error for “interactive” requests
instead. (The precise definition of “interactive” requests might
change – the spirit here is to give helpful messages and avoid
confusions.)
Closes #20666, #23062, #23077, #23564
[Godfrey Chan, Jon Moss, Kasper Timm Hansen, Mike Clark, Matthew Draper]
2016-02-23 12:41:26 -05:00
|
|
|
end
|
2018-09-12 13:50:38 -04:00
|
|
|
end
|
2013-12-03 05:17:01 -05:00
|
|
|
|
2018-09-12 13:50:38 -04:00
|
|
|
def build_regex(path, details)
|
|
|
|
query = escape_entry(File.join(@path, path))
|
|
|
|
exts = EXTENSIONS.map do |ext, prefix|
|
|
|
|
match =
|
|
|
|
if ext == :variants && details[ext] == :any
|
|
|
|
".*?"
|
|
|
|
else
|
|
|
|
details[ext].compact.uniq.map { |e| Regexp.escape(e) }.join("|")
|
|
|
|
end
|
|
|
|
prefix = Regexp.escape(prefix)
|
|
|
|
"(#{prefix}(?<#{ext}>#{match}))?"
|
|
|
|
end.join
|
|
|
|
|
|
|
|
%r{\A#{query}#{exts}\z}
|
|
|
|
end
|
2011-05-09 05:17:24 -04:00
|
|
|
end
|
|
|
|
|
2010-10-10 17:11:50 -04:00
|
|
|
# The same as FileSystemResolver but does not allow templates to store
|
|
|
|
# a virtual path since it is invalid for such resolvers.
|
2011-05-09 05:17:24 -04:00
|
|
|
class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
|
2019-04-01 19:35:07 -04:00
|
|
|
private_class_method :new
|
|
|
|
|
2010-10-10 17:11:50 -04:00
|
|
|
def self.instances
|
|
|
|
[new(""), new("/")]
|
|
|
|
end
|
2019-03-18 14:57:05 -04:00
|
|
|
|
2019-04-12 13:20:04 -04:00
|
|
|
def build_unbound_template(template, _)
|
|
|
|
super(template, nil)
|
2019-03-18 14:57:05 -04:00
|
|
|
end
|
2019-04-01 19:35:07 -04:00
|
|
|
|
|
|
|
def reject_files_external_to_app(files)
|
|
|
|
files
|
|
|
|
end
|
2010-10-10 17:11:50 -04:00
|
|
|
end
|
2009-12-28 19:28:26 -05:00
|
|
|
end
|