Permalink
Browse files

Refactor ActionView::Resolver

  • Loading branch information...
Yehuda Katz + Carl Lerche
Yehuda Katz + Carl Lerche committed Sep 2, 2009
1 parent dd34691 commit f3fc5c4b5f36db37edc8ab553a35b06b48226c0a
View
@@ -1,3 +1,4 @@
+.DS_Store
debug.log
doc/rdoc
activemodel/doc
@@ -13,6 +14,7 @@ actionpack/pkg
activemodel/test/fixtures/fixture_database.sqlite3
actionmailer/pkg
activesupport/pkg
+actionpack/test/tmp
activesupport/test/fixtures/isolation_test
railties/pkg
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
# have not already been specified manually.
# 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(
:content_type => template.mime_type ? template.mime_type.to_s : "text/plain",
:disposition => "inline",
@@ -200,7 +200,7 @@ def find_layout(*args)
end
def layout_list #:nodoc:
- Array(view_paths).sum([]) { |path| Dir["#{path.to_str}/layouts/**/*"] }
+ Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] }
end
memoize :layout_list
@@ -40,6 +40,7 @@ def self.load_all!
autoload :MissingTemplate, 'action_view/base'
autoload :Partials, 'action_view/render/partials'
autoload :Resolver, 'action_view/template/resolver'
+ autoload :PathResolver, 'action_view/template/resolver'
autoload :PathSet, 'action_view/paths'
autoload :Rendering, 'action_view/render/rendering'
autoload :Renderable, 'action_view/template/renderable'
@@ -1,25 +1,47 @@
require "pathname"
+require "active_support/core_ext/class"
require "action_view/template/template"
module ActionView
# Abstract superclass
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 = {})
@cache = options[:cache]
@cached = {}
end
# Normalizes the arguments and passes it on to find_template
def find(*args)
- find_all_by_parts(*args).first
+ find_all(*args).first
end
-
- def find_all_by_parts(name, details = {}, prefix = nil, partial = nil)
- details[:locales] = [I18n.locale]
- name = name.to_s.gsub(handler_matcher, '').split("/")
- find_templates(name.pop, details, [prefix, *name].compact.join("/"), partial)
+
+ def find_all(name, details = {}, prefix = nil, partial = nil)
+ details = normalize_details(details)
+ name, prefix = normalize_name(name, prefix)
+
+ cached([name, details, prefix, partial]) do
+ find_templates(name, details, prefix, partial)
+ end
end
-
+
private
# 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)
raise NotImplementedError
end
-
- def valid_handlers
- @valid_handlers ||= TemplateHandlers.extensions
- end
- def handler_matcher
- @handler_matcher ||= begin
- e = valid_handlers.join('|')
- /\.(?:#{e})$/
+ def normalize_details(details)
+ details = details.dup
+ registered_details.each do |k, v|
+ details[k] = v.call(details[k])
end
+ details
end
- def handler_glob
- @handler_glob ||= begin
- e = TemplateHandlers.extensions.map{|h| ".#{h}"}.join(",")
- "{#{e}}"
- end
- end
-
- def formats_glob
- @formats_glob ||= begin
- '{' + Mime::SET.symbols.map { |l| ".#{l}," }.join + '}'
- end
+ # Support legacy foo.erb names even though we now ignore .erb
+ # as well as incorrectly putting part of the path in the template
+ # name instead of the prefix.
+ def normalize_name(name, prefix)
+ handlers = TemplateHandlers.extensions.join('|')
+ name = name.to_s.gsub(/\.(?:#{handlers})$/, '')
+
+ parts = name.split('/')
+ return parts.pop, [prefix, *parts].compact.join("/")
end
def cached(key)
@@ -60,67 +77,49 @@ def cached(key)
end
end
- class FileSystemResolver < Resolver
+ class PathResolver < Resolver
- def self.cached_glob
- @@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
+ EXTENSION_ORDER = [:locale, :formats, :handlers]
def to_s
@path.to_s
end
alias to_path to_s
- def find_templates(name, details, prefix, partial, root = "#{@path}/")
- if glob = details_to_glob(name, details, prefix, partial, root)
- cached(glob) do
- 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
+ def find_templates(name, details, prefix, partial)
+ path = build_path(name, details, prefix, partial)
+ query(path, EXTENSION_ORDER.map { |ext| details[ext] })
end
-
+
private
- # :api: plugin
- def details_to_glob(name, details, prefix, partial, root)
- self.class.cached_glob[[name, prefix, partial, details, root]] ||= begin
- path = ""
- path << "#{prefix}/" unless prefix.empty?
- 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}"
+ def build_path(name, details, prefix, partial)
+ path = ""
+ path << "#{prefix}/" unless prefix.empty?
+ path << (partial ? "_#{name}" : name)
+ path
+ end
+
+ def query(path, exts)
+ 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
+ # # 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+)$')
+ 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])
@@ -133,13 +132,32 @@ def path_to_details(path)
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
+
+ # OMG HAX
+ # TODO: remove hax
+ class FileSystemResolverWithFallback < Resolver
+ def initialize(path, options = {})
+ super(options)
+ @paths = [FileSystemResolver.new(path, options), FileSystemResolver.new("", options), FileSystemResolver.new("/", options)]
+ end
- def find_templates(name, details, prefix, partial)
- templates = super
- return super(name, details, prefix, partial, '') if templates.empty?
- templates
+ 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
@@ -1,70 +1,28 @@
module ActionView #:nodoc:
- class FixtureResolver < Resolver
+ class FixtureResolver < PathResolver
def initialize(hash = {}, options = {})
super(options)
@hash = hash
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
- def formats_regexp
- @formats_regexp ||= begin
- formats = Mime::SET.symbols
- '(?:' + formats.map { |l| "\\.#{Regexp.escape(l.to_s)}" }.join('|') + ')?'
- end
- end
-
- def handler_regexp
- e = TemplateHandlers.extensions.map{|h| "\\.#{Regexp.escape(h.to_s)}"}.join("|")
- "(?:#{e})"
+ def or_extensions(array)
+ "(?:" << array.map {|e| e && Regexp.escape(".#{e}")}.join("|") << ")"
end
- 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
+ def query(path, exts)
+ query = Regexp.escape(path)
+ exts.each do |ext|
+ query << '(?:' << ext.map {|e| e && Regexp.escape(".#{e}") }.join('|') << ')'
end
- %r'^#{Regexp.escape(path)}#{extensions}#{handler_regexp}$'
- end
-
- # 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
+ templates = []
+ @hash.select { |k,v| k =~ /^#{query}$/ }.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
@@ -3,7 +3,7 @@
module RenderFile
class BasicController < ActionController::Base
- self.view_paths = "."
+ self.view_paths = File.dirname(__FILE__)
def index
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" })
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
assert_equal "Hello world!", render(:file => "test/hello_world.erb")
modify_template "test/hello_world.erb", "Goodbye world!" do

0 comments on commit f3fc5c4

Please sign in to comment.