Refactor ActionView::Resolver

This commit is contained in:
Yehuda Katz + Carl Lerche 2009-09-02 15:00:22 -07:00
parent dd34691b8d
commit f3fc5c4b5f
8 changed files with 118 additions and 136 deletions

2
.gitignore vendored

@ -1,3 +1,4 @@
.DS_Store
debug.log debug.log
doc/rdoc doc/rdoc
activemodel/doc activemodel/doc
@ -13,6 +14,7 @@ actionpack/pkg
activemodel/test/fixtures/fixture_database.sqlite3 activemodel/test/fixtures/fixture_database.sqlite3
actionmailer/pkg actionmailer/pkg
activesupport/pkg activesupport/pkg
actionpack/test/tmp
activesupport/test/fixtures/isolation_test activesupport/test/fixtures/isolation_test
railties/pkg railties/pkg
railties/test/500.html railties/test/500.html

@ -489,7 +489,7 @@ def create!(method_name, *parameters) #:nodoc:
# "the_template_file.text.html.erb", etc.). Only do this if parts # "the_template_file.text.html.erb", etc.). Only do this if parts
# have not already been specified manually. # have not already been specified manually.
# if @parts.empty? # if @parts.empty?
template_root.find_all_by_parts(@template, {}, template_path).each do |template| template_root.find_all(@template, {}, template_path).each do |template|
@parts << Part.new( @parts << Part.new(
:content_type => template.mime_type ? template.mime_type.to_s : "text/plain", :content_type => template.mime_type ? template.mime_type.to_s : "text/plain",
:disposition => "inline", :disposition => "inline",

@ -200,7 +200,7 @@ def find_layout(*args)
end end
def layout_list #:nodoc: def layout_list #:nodoc:
Array(view_paths).sum([]) { |path| Dir["#{path.to_str}/layouts/**/*"] } Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] }
end end
memoize :layout_list memoize :layout_list

@ -40,6 +40,7 @@ def self.load_all!
autoload :MissingTemplate, 'action_view/base' autoload :MissingTemplate, 'action_view/base'
autoload :Partials, 'action_view/render/partials' autoload :Partials, 'action_view/render/partials'
autoload :Resolver, 'action_view/template/resolver' autoload :Resolver, 'action_view/template/resolver'
autoload :PathResolver, 'action_view/template/resolver'
autoload :PathSet, 'action_view/paths' autoload :PathSet, 'action_view/paths'
autoload :Rendering, 'action_view/render/rendering' autoload :Rendering, 'action_view/render/rendering'
autoload :Renderable, 'action_view/template/renderable' autoload :Renderable, 'action_view/template/renderable'

@ -1,9 +1,28 @@
require "pathname" require "pathname"
require "active_support/core_ext/class"
require "action_view/template/template" require "action_view/template/template"
module ActionView module ActionView
# Abstract superclass # Abstract superclass
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 ||= 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, :allow_nil => false) do
TemplateHandlers.extensions
end
def initialize(options = {}) def initialize(options = {})
@cache = options[:cache] @cache = options[:cache]
@cached = {} @cached = {}
@ -11,15 +30,18 @@ def initialize(options = {})
# Normalizes the arguments and passes it on to find_template # Normalizes the arguments and passes it on to find_template
def find(*args) def find(*args)
find_all_by_parts(*args).first find_all(*args).first
end end
def find_all_by_parts(name, details = {}, prefix = nil, partial = nil) def find_all(name, details = {}, prefix = nil, partial = nil)
details[:locales] = [I18n.locale] details = normalize_details(details)
name = name.to_s.gsub(handler_matcher, '').split("/") name, prefix = normalize_name(name, prefix)
find_templates(name.pop, details, [prefix, *name].compact.join("/"), partial)
cached([name, details, prefix, partial]) do
find_templates(name, details, prefix, partial)
end
end end
private private
# This is what child classes implement. No defaults are needed # This is what child classes implement. No defaults are needed
@ -28,29 +50,24 @@ def find_all_by_parts(name, details = {}, prefix = nil, partial = nil)
def find_templates(name, details, prefix, partial) def find_templates(name, details, prefix, partial)
raise NotImplementedError raise NotImplementedError
end end
def valid_handlers def normalize_details(details)
@valid_handlers ||= TemplateHandlers.extensions details = details.dup
registered_details.each do |k, v|
details[k] = v.call(details[k])
end
details
end end
def handler_matcher # Support legacy foo.erb names even though we now ignore .erb
@handler_matcher ||= begin # as well as incorrectly putting part of the path in the template
e = valid_handlers.join('|') # name instead of the prefix.
/\.(?:#{e})$/ def normalize_name(name, prefix)
end handlers = TemplateHandlers.extensions.join('|')
end name = name.to_s.gsub(/\.(?:#{handlers})$/, '')
def handler_glob parts = name.split('/')
@handler_glob ||= begin return parts.pop, [prefix, *parts].compact.join("/")
e = TemplateHandlers.extensions.map{|h| ".#{h}"}.join(",")
"{#{e}}"
end
end
def formats_glob
@formats_glob ||= begin
'{' + Mime::SET.symbols.map { |l| ".#{l}," }.join + '}'
end
end end
def cached(key) def cached(key)
@ -60,67 +77,49 @@ def cached(key)
end end
end end
class FileSystemResolver < Resolver class PathResolver < Resolver
def self.cached_glob EXTENSION_ORDER = [:locale, :formats, :handlers]
@@cached_glob ||= {}
end
def initialize(path, options = {})
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
super(options)
@path = Pathname.new(path).expand_path
end
def to_s def to_s
@path.to_s @path.to_s
end end
alias to_path to_s alias to_path to_s
def find_templates(name, details, prefix, partial, root = "#{@path}/") def find_templates(name, details, prefix, partial)
if glob = details_to_glob(name, details, prefix, partial, root) path = build_path(name, details, prefix, partial)
cached(glob) do query(path, EXTENSION_ORDER.map { |ext| details[ext] })
Dir[glob].map do |path|
next if File.directory?(path)
source = File.read(path)
identifier = Pathname.new(path).expand_path.to_s
Template.new(source, identifier, *path_to_details(path))
end.compact
end
end
end end
private private
# :api: plugin def build_path(name, details, prefix, partial)
def details_to_glob(name, details, prefix, partial, root) path = ""
self.class.cached_glob[[name, prefix, partial, details, root]] ||= begin path << "#{prefix}/" unless prefix.empty?
path = "" path << (partial ? "_#{name}" : name)
path << "#{prefix}/" unless prefix.empty? path
path << (partial ? "_#{name}" : name)
extensions = ""
[:locales, :formats].each do |k|
# TODO: OMG NO
if details[k] == [:"*/*"]
extensions << formats_glob if k == :formats
elsif exts = details[k]
extensions << '{' + exts.map {|e| ".#{e},"}.join + '}'
else
extensions << formats_glob if k == :formats
end
end
"#{root}#{path}#{extensions}#{handler_glob}"
end
end end
# TODO: fix me def query(path, exts)
# :api: plugin query = "#{@path}/#{path}"
exts.each do |ext|
query << '{' << ext.map {|e| e && ".#{e}" }.join(',') << '}'
end
Dir[query].map do |path|
next if File.directory?(path)
source = File.read(path)
identifier = Pathname.new(path).expand_path.to_s
Template.new(source, identifier, *path_to_details(path))
end.compact
end
# # TODO: fix me
# # :api: plugin
def path_to_details(path) def path_to_details(path)
# [:erb, :format => :html, :locale => :en, :partial => true/false] # [:erb, :format => :html, :locale => :en, :partial => true/false]
if m = path.match(%r'/(_)?[\w-]+(\.[\w-]+)*\.(\w+)$') if m = path.match(%r'(?:^|/)(_)?[\w-]+(\.[\w-]+)*\.(\w+)$')
partial = m[1] == '_' partial = m[1] == '_'
details = (m[2]||"").split('.').reject { |e| e.empty? } details = (m[2]||"").split('.').reject { |e| e.empty? }
handler = Template.handler_class_for_extension(m[3]) handler = Template.handler_class_for_extension(m[3])
@ -133,13 +132,32 @@ def path_to_details(path)
end end
end end
class FileSystemResolverWithFallback < FileSystemResolver class FileSystemResolver < PathResolver
def initialize(path, options = {})
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
super(options)
@path = Pathname.new(path).expand_path
end
end
def find_templates(name, details, prefix, partial) # OMG HAX
templates = super # TODO: remove hax
return super(name, details, prefix, partial, '') if templates.empty? class FileSystemResolverWithFallback < Resolver
templates def initialize(path, options = {})
super(options)
@paths = [FileSystemResolver.new(path, options), FileSystemResolver.new("", options), FileSystemResolver.new("/", options)]
end end
def find_templates(*args)
@paths.each do |p|
template = p.find_templates(*args)
return template unless template.empty?
end
[]
end
def to_s
@paths.first.to_s
end
end end
end end

@ -1,70 +1,28 @@
module ActionView #:nodoc: module ActionView #:nodoc:
class FixtureResolver < Resolver class FixtureResolver < PathResolver
def initialize(hash = {}, options = {}) def initialize(hash = {}, options = {})
super(options) super(options)
@hash = hash @hash = hash
end end
def find_templates(name, details, prefix, partial)
if regexp = details_to_regexp(name, details, prefix, partial)
cached(regexp) do
templates = []
@hash.select { |k,v| k =~ regexp }.each do |path, source|
templates << Template.new(source, path, *path_to_details(path))
end
templates.sort_by {|t| -t.details.values.compact.size }
end
end
end
private private
def formats_regexp def or_extensions(array)
@formats_regexp ||= begin "(?:" << array.map {|e| e && Regexp.escape(".#{e}")}.join("|") << ")"
formats = Mime::SET.symbols
'(?:' + formats.map { |l| "\\.#{Regexp.escape(l.to_s)}" }.join('|') + ')?'
end
end end
def handler_regexp def query(path, exts)
e = TemplateHandlers.extensions.map{|h| "\\.#{Regexp.escape(h.to_s)}"}.join("|") query = Regexp.escape(path)
"(?:#{e})" exts.each do |ext|
end query << '(?:' << ext.map {|e| e && Regexp.escape(".#{e}") }.join('|') << ')'
def details_to_regexp(name, details, prefix, partial)
path = ""
path << "#{prefix}/" unless prefix.empty?
path << (partial ? "_#{name}" : name)
extensions = ""
[:locales, :formats].each do |k|
# TODO: OMG NO
if details[k] == [:"*/*"]
extensions << formats_regexp if k == :formats
elsif exts = details[k]
extensions << '(?:' + exts.map {|e| "\\.#{Regexp.escape(e.to_s)}"}.join('|') + ')?'
else
extensions << formats_regexp if k == :formats
end
end end
%r'^#{Regexp.escape(path)}#{extensions}#{handler_regexp}$' templates = []
end @hash.select { |k,v| k =~ /^#{query}$/ }.each do |path, source|
templates << Template.new(source, path, *path_to_details(path))
# TODO: fix me
# :api: plugin
def path_to_details(path)
# [:erb, :format => :html, :locale => :en, :partial => true/false]
if m = path.match(%r'(_)?[\w-]+((?:\.[\w-]+)*)\.(\w+)$')
partial = m[1] == '_'
details = (m[2]||"").split('.').reject { |e| e.empty? }
handler = Template.handler_class_for_extension(m[3])
format = Mime[details.last] && details.pop.to_sym
locale = details.last && details.pop.to_sym
return handler, :format => format, :locale => locale, :partial => partial
end end
templates.sort_by {|t| -t.details.values.compact.size }
end end
end end
end end

@ -3,7 +3,7 @@
module RenderFile module RenderFile
class BasicController < ActionController::Base class BasicController < ActionController::Base
self.view_paths = "." self.view_paths = File.dirname(__FILE__)
def index def index
render :file => File.join(File.dirname(__FILE__), *%w[.. fixtures test hello_world]) render :file => File.join(File.dirname(__FILE__), *%w[.. fixtures test hello_world])

@ -14,6 +14,9 @@ def test_template_gets_recompiled_when_using_different_keys_in_local_assigns
assert_equal "two", render(:file => "test/render_file_with_locals_and_default.erb", :locals => { :secret => "two" }) assert_equal "two", render(:file => "test/render_file_with_locals_and_default.erb", :locals => { :secret => "two" })
end end
# This is broken in 1.8.6 (not supported in Rails 3.0) because the cache uses a Hash
# key. Since Ruby 1.8.6 implements Hash#hash using the hash's object_id, it will never
# successfully get a cache hit here.
def test_template_changes_are_not_reflected_with_cached_templates def test_template_changes_are_not_reflected_with_cached_templates
assert_equal "Hello world!", render(:file => "test/hello_world.erb") assert_equal "Hello world!", render(:file => "test/hello_world.erb")
modify_template "test/hello_world.erb", "Goodbye world!" do modify_template "test/hello_world.erb", "Goodbye world!" do