Move details to lookup_context and make resolvers use the cache key.

This commit is contained in:
José Valim 2010-03-08 20:39:15 +01:00
parent 67a6725bf9
commit bdf5096816
5 changed files with 117 additions and 140 deletions

@ -101,19 +101,9 @@ def _normalize_options(options)
end end
options[:template] ||= (options[:action] || action_name).to_s options[:template] ||= (options[:action] || action_name).to_s
details = _normalize_details(options)
lookup_context.update_details(details)
options options
end end
def _normalize_details(options)
details = {}
details[:formats] = Array(options[:format]) if options[:format]
details[:locale] = Array(options[:locale]) if options[:locale]
details
end
def _process_options(options) def _process_options(options)
end end

@ -1,14 +1,30 @@
require 'active_support/core_ext/object/try'
module ActionView module ActionView
# LookupContext is the object responsible to hold all information required to lookup # LookupContext is the object responsible to hold all information required to lookup
# templates, i.e. view paths and details. The LookupContext is also responsible to # templates, i.e. view paths and details. The LookupContext is also responsible to
# generate a key, given to view paths, used in the resolver cache lookup. Since # generate a key, given to view paths, used in the resolver cache lookup. Since
# this key is generated just once during the request, it speeds up all cache accesses. # this key is generated just once during the request, it speeds up all cache accesses.
class LookupContext #:nodoc: class LookupContext #:nodoc:
attr_reader :details, :view_paths
mattr_accessor :fallbacks mattr_accessor :fallbacks
@@fallbacks = [FileSystemResolver.new(""), FileSystemResolver.new("/")] @@fallbacks = [FileSystemResolver.new(""), FileSystemResolver.new("/")]
mattr_accessor :registered_details
self.registered_details = {}
def self.register_detail(name, options = {})
registered_details[name] = lambda do |value|
value = (value.blank? || options[:accessible] == false) ?
Array(yield) : Array(value)
value |= [nil] unless options[:allow_nil] == false
value
end
end
register_detail(:formats) { Mime::SET.symbols }
register_detail(:locale, :accessible => false) { [I18n.locale] }
register_detail(:handlers, :accessible => false) { Template::Handlers.extensions }
class DetailsKey #:nodoc: class DetailsKey #:nodoc:
attr_reader :details attr_reader :details
alias :eql? :equal? alias :eql? :equal?
@ -22,75 +38,108 @@ def self.get(details)
def initialize(details) def initialize(details)
@details, @hash = details, details.hash @details, @hash = details, details.hash
end end
def outdated?(details)
@details != details
end
end end
def initialize(view_paths, details = {}) def initialize(view_paths, details = {})
@details, @details_key = details, nil
self.view_paths = view_paths self.view_paths = view_paths
self.details = details
@details_key = nil
end end
# Shortcut to read formats from details. module ViewPaths
def formats attr_reader :view_paths
@details[:formats]
# Whenever setting view paths, makes a copy so we can manipulate then in
# instance objects as we wish.
def view_paths=(paths)
@view_paths = ActionView::Base.process_view_paths(paths)
end
def find_template(name, prefix = nil, partial = false)
key = details_key
@view_paths.find(name, key.details, prefix, partial || false, key)
end
def template_exists?(name, prefix = nil, partial = false)
key = details_key
@view_paths.exists?(name, key.details, prefix, partial || false, key)
end
# Add fallbacks to the view paths. Useful in cases you are rendering a file.
def with_fallbacks
added_resolvers = 0
self.class.fallbacks.each do |resolver|
next if view_paths.include?(resolver)
view_paths.push(resolver)
added_resolvers += 1
end
yield
ensure
added_resolvers.times { view_paths.pop }
end
end end
# Shortcut to set formats in details. module Details
def formats=(value) def details
self.details = @details.merge(:formats => Array(value)) @details = normalize_details(@details)
end end
# Whenever setting view paths, makes a copy so we can manipulate then in def details=(new_details)
# instance objects as we wish. @details = new_details
def view_paths=(paths) details
@view_paths = ActionView::Base.process_view_paths(paths) end
end
# Setter for details. Everything this method is invoked, we need to nullify # TODO This is too expensive. Revisit this.
# the details key if it changed. def details_key
def details=(details) latest_details = self.details
@details = details @details_key = nil if @details_key.try(:outdated?, latest_details)
@details_key = nil if @details_key && @details_key.details != details @details_key ||= DetailsKey.get(latest_details)
end end
def details_key # Shortcut to read formats from details.
@details_key ||= DetailsKey.get(details) unless details.empty? def formats
end self.details[:formats]
end
# Update the details keys by merging the given hash into the current # Shortcut to set formats in details.
# details hash. If a block is given, the details are modified just during def formats=(value)
# the execution of the block and reverted to the previous value after. self.details = @details.merge(:formats => value)
def update_details(new_details) end
old_details = self.details
self.details = old_details.merge(new_details)
if block_given? # Update the details keys by merging the given hash into the current
begin # details hash. If a block is given, the details are modified just during
yield # the execution of the block and reverted to the previous value after.
ensure def update_details(new_details)
self.details = old_details old_details = self.details
self.details = old_details.merge(new_details)
if block_given?
begin
yield
ensure
self.details = old_details
end
end end
end end
end
# Added fallbacks to the view paths. Useful in cases you are rendering a file. protected
def with_fallbacks
added_resolvers = 0 def normalize_details(details)
self.class.fallbacks.each do |resolver| details = details.dup
next if view_paths.include?(resolver) # TODO: Refactor this concern out of the resolver
view_paths.push(resolver) details.delete(:formats) if details[:formats] == [:"*/*"]
added_resolvers += 1 self.class.registered_details.each do |k, v|
details[k] = v.call(details[k])
end
details
end end
yield
ensure
added_resolvers.times { view_paths.pop }
end end
def find_template(name, prefix = nil, partial = false) include Details
@view_paths.find(name, details, prefix, partial || false, details_key) include ViewPaths
end
def template_exists?(name, prefix = nil, partial = false)
@view_paths.exists?(name, details, prefix, partial || false, details_key)
end
end end
end end

@ -11,7 +11,7 @@ def #{method}(*args)
def find(path, details = {}, prefix = nil, partial = false, key=nil) def find(path, details = {}, prefix = nil, partial = false, key=nil)
each do |resolver| each do |resolver|
if template = resolver.find(path, details, prefix, partial) if template = resolver.find(path, details, prefix, partial, key)
return template return template
end end
end end
@ -21,7 +21,7 @@ def find(path, details = {}, prefix = nil, partial = false, key=nil)
def exists?(path, details = {}, prefix = nil, partial = false, key=nil) def exists?(path, details = {}, prefix = nil, partial = false, key=nil)
each do |resolver| each do |resolver|
if resolver.find(path, details, prefix, partial) if resolver.find(path, details, prefix, partial, key)
return true return true
end end
end end

@ -5,24 +5,9 @@
module ActionView module ActionView
class Resolver class Resolver
class_inheritable_accessor(:registered_details)
self.registered_details = {}
def self.register_detail(name, options = {})
registered_details[name] = lambda do |val|
val = Array.wrap(val || yield)
val |= [nil] unless options[:allow_nil] == false
val
end
end
register_detail(:locale) { [I18n.locale] }
register_detail(:formats) { Mime::SET.symbols }
register_detail(:handlers) { Template::Handlers.extensions }
def initialize def initialize
@cached = {} @cached = Hash.new { |h1,k1| h1[k1] =
Hash.new { |h2,k2| h2[k2] = Hash.new { |h3, k3| h3[k3] = {} } } }
end end
def find(*args) def find(*args)
@ -30,11 +15,10 @@ def find(*args)
end end
# Normalizes the arguments and passes it on to find_template. # Normalizes the arguments and passes it on to find_template.
def find_all(name, details = {}, prefix = nil, partial = nil) def find_all(name, details = {}, prefix = nil, partial = nil, key=nil)
details = normalize_details(details)
name, prefix = normalize_name(name, prefix) name, prefix = normalize_name(name, prefix)
cached([name, details, prefix, partial]) do cached(key, prefix, name, partial) do
find_templates(name, details, prefix, partial) find_templates(name, details, prefix, partial)
end end
end end
@ -52,16 +36,6 @@ def find_templates(name, details, prefix, partial)
raise NotImplementedError raise NotImplementedError
end end
def normalize_details(details)
details = details.dup
# TODO: Refactor this concern out of the resolver
details.delete(:formats) if details[:formats] == [:"*/*"]
registered_details.each do |k, v|
details[k] = v.call(details[k])
end
details
end
# Support legacy foo.erb names even though we now ignore .erb # Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template # as well as incorrectly putting part of the path in the template
# name instead of the prefix. # name instead of the prefix.
@ -73,10 +47,14 @@ def normalize_name(name, prefix)
return parts.pop, [prefix, *parts].compact.join("/") return parts.pop, [prefix, *parts].compact.join("/")
end end
def cached(key) def cached(key, prefix, name, partial)
return yield unless caching? return yield unless key && caching?
return @cached[key] if @cached.key?(key) scope = @cached[key][prefix][name]
@cached[key] = yield if scope.key?(partial)
scope[partial]
else
scope[partial] = yield
end
end end
end end

@ -16,11 +16,7 @@ def _prefix
"renderer/string.erb" => "With String", "renderer/string.erb" => "With String",
"renderer/symbol.erb" => "With Symbol", "renderer/symbol.erb" => "With Symbol",
"string/with_path.erb" => "With String With Path", "string/with_path.erb" => "With String With Path",
"some/file.erb" => "With File", "some/file.erb" => "With File"
"with_format.html.erb" => "With html format",
"with_format.xml.erb" => "With xml format",
"with_locale.en.erb" => "With en locale",
"with_locale.pl.erb" => "With pl locale"
)] )]
def template def template
@ -54,22 +50,6 @@ def string_with_path
def symbol def symbol
render :symbol render :symbol
end end
def with_html_format
render :template => "with_format", :format => :html
end
def with_xml_format
render :template => "with_format", :format => :xml
end
def with_en_locale
render :template => "with_locale"
end
def with_pl_locale
render :template => "with_locale", :locale => :pl
end
end end
class TestRenderer < ActiveSupport::TestCase class TestRenderer < ActiveSupport::TestCase
@ -117,26 +97,6 @@ def test_render_string_with_path
@controller.process(:string_with_path) @controller.process(:string_with_path)
assert_equal "With String With Path", @controller.response_body assert_equal "With String With Path", @controller.response_body
end end
def test_render_with_html_format
@controller.process(:with_html_format)
assert_equal "With html format", @controller.response_body
end
def test_render_with_xml_format
@controller.process(:with_xml_format)
assert_equal "With xml format", @controller.response_body
end
def test_render_with_en_locale
@controller.process(:with_en_locale)
assert_equal "With en locale", @controller.response_body
end
def test_render_with_pl_locale
@controller.process(:with_pl_locale)
assert_equal "With pl locale", @controller.response_body
end
end end
end end
end end