0
0
Fork 0

extracted the building of template tree

This step is a preparation to extend the functionality of the renderer.
To make the main class easier, the search for templates and building of
the tree is extracted into its own class.
This commit is contained in:
Gibheer 2013-02-12 07:38:26 +01:00
parent 8ff98ff9dd
commit f18ab69a91
6 changed files with 257 additions and 49 deletions

View File

@ -1,3 +1,5 @@
require 'zero/renderer/template_finder'
module Zero
# the base renderer for getting render containers
#
@ -34,7 +36,7 @@ module Zero
# @param template_path [String] a string to templates
# @param type_map [Hash] a map of simple types to complex ones
def initialize(template_path, type_map = {})
@template_path = template_path + '/'
@template_path = template_path
@type_map = type_map
end
@ -56,23 +58,7 @@ module Zero
# the wanted template.
# @return [Self] returns the object
def read_template_path!
# TODO clean up later
@templates = {}
search_files.each do |file|
parts = file.gsub(/#{template_path}/, '').split('.')
@templates[parts[0]] ||= {}
# Set default value
types = 'default'
# Overwrite default value, if it's set in template path
if parts.count > 2 then
types = parts[1]
end
read_type(types).each do |type|
@templates[parts[0]][type] = file
end
end
@templates = TemplateFinder.new(template_path, @type_map).get_templates
end
# render a template
@ -89,32 +75,6 @@ module Zero
private
# search in `template_path` for templates beginning with `template_name`
# @api private
# @param template_name [String] the name of the template
# @return [#each] a list of all templates found
def search_files
Dir[template_path + '**/*.*']
end
# gets the type information from a file and converts it to an array of
# possible matching types
# @api private
# @param short_notation [String] a short notation of a type, like `html`
# @return [Array] a list of matching types, like `text/html`
def read_type(short_notation)
to_type_list(type_map[short_notation] || short_notation)
end
# convert a map to an array if it is not one
# @api private
# @param original_map [Object] the type(s) to convert
# @return [Array] a list of objects
def to_type_list(original_map)
return original_map if original_map.respond_to?(:each)
[original_map]
end
# get the prepared template for the name and type
# @api private
# @param name [String] the name of the template

View File

@ -0,0 +1,164 @@
module Zero
class Renderer
# finds templates in a path and builds a map for the renderer to use
#
# When this class is feeded with a path and a type map it will generate
# a map of templates and types for the renderer to use.
# For that to work, it first needs a path ending on '/' and a map of type
# names to mime types. The short type name is used in template names to find
# out, for which mime types they are built, so that they can be rendered
# for the correct request.
#
# The template files can be named in two different formats
# * filename.extension
# * filename.type.extension
# The type is used to access the `type_map`. It will be used to find all
# mime types this template can be used to answer. If no type is given in the
# filename, the type will be set to `default`.
# So `default` can be used in the `type_map` to map these files too.
#
# @example building a TemplateFinder
# As an example, lets assume we have the following files in our path
# * `index.erb`
# * `index.json.erb`
#
# We want these to render for either html requests or json requests. To make
# this work, we need to build a TemplateFinder like following
#
# TemplateFinder.new('path/', {
# 'default' => ['text/html', '*/*'],
# 'json' => ['application/json']
# })
#
# This will build a structure, so that requests with 'text/html' will render
# `index.erb`.
class TemplateFinder
# the search mask to search for files
# @example foo/bar/**/*.*
MARK_ALL_FILES = '**/*.*'
# for finding the last slash
SLASH_END = '/'
# empty string to replace the path in the filename
EMPTY_STRING = ''
# split filename at this character
SPLIT_CHAR = '.'
# default type
DEFAULT_TYPE = 'default'
# the path to all templates
# @api private
# @returns [String] the path given at initialization
attr_reader :path
# a map of simple type names to a list of mime types
# @api private
# @example 'html' => ['text/html', 'text/xml', 'text/html+xml']
# @returns [Hash] a hash with types to mime types
attr_reader :type_map
# this returns the regex for the specified path
# @api private
# @returns [Regex] the regex built from the path
attr_reader :path_regex
# initialize a new template finder
#
# @example
# TemplateFinder.new('foo/bar/', {
# 'default' => ['text/html', 'text/xml'],
# 'json' => ['application/json']
# })
# @param path [String] the path to all templates ending on '/'
# @param type_map [Hash] a map of short type names to mime types
def initialize(path, type_map)
raise ArgumentError.new("Has to end on '/'!") if path[-1] != SLASH_END
@path = path
@type_map = sanity_map(type_map)
@path_regex = /#{path}/
end
# traverses the template path to gather all templates
#
# This function traverses the template path, collects and sorts all
# templates into the target types given at initialization.
# @return [Hash] the map of type to template
def get_templates
result = Hash.new {|hash, key| hash[key] = {} }
search_files.each do |file|
key, value = add_template(file)
result[key] = result[key].merge(value)
end
result
end
private
# returns a list of files found at @path
#
# This method returns all files found in @path, which look like a template.
# Look for `MARK_ALL_FILES` for the eact schema.
# @api private
# @return [Array] a list of all files found
def search_files
Dir[@path + MARK_ALL_FILES]
end
# splits the path into a filename and its type
#
# This function takes a filepath and extracts the filename and short
# notation for the type.
# The filename is later used at rendering time to find the template.
# @api private
# @param filepath [String] the filename to split
# @return [Array] an Array of the following example `[filename, type]`
def get_fields(filepath)
filename, *options = filepath.gsub(@path_regex, EMPTY_STRING).split(SPLIT_CHAR)
[filename, (options.length == 1 ? DEFAULT_TYPE : options[0])]
end
# add a template with its type variants
#
# This method adds a template with all type variants to the map of all
# types and templates.
# @api private
# @param filename [String] the short name of the template
# @param type [String] the short type of the template
# @param path [String] the actual path to the template
# @return [Array] a hashable array for the end result:
def add_template(path)
filename, type = get_fields(path)
result = [filename, {}]
get_types(type).each do |mime_type|
result[1][mime_type] = path
end
result
end
# get the types for the shorthand type
#
# This method returns all types associated with the short notation
# of this type in the type_map.
# @api private
# @param short_type [String] the short notation of a type
# @return [Array] a list of all types found in the type_map
def get_types(short_type)
return [short_type] unless @type_map.has_key?(short_type)
@type_map[short_type]
end
# make a cleanup of the map
#
# This function converts all map values to arrays, to make the processing
# easier.
# @api private
# @param map [Hash] a type map
# @return [Hash] the cleaned up map
def sanity_map(map)
map.each do |key, value|
map[key] = [value] unless value.respond_to?(:each)
end
end
end
end
end

View File

@ -1,14 +1,14 @@
require 'spec_helper'
describe Zero::Renderer, 'read_template_path!' do
describe Zero::Renderer, '#read_template_path!' do
subject { Zero::Renderer.new(template_path, type_map) }
let(:template_path) { 'foo' }
let(:template_path) { 'foo/' }
let(:file_list) { ['foo/welcome/index.html.erb'] }
before :each do
Dir.stub(:[]) do |arg|
if arg == 'foo/**/*.*'
file_list
file_list
else
[]
end
@ -58,7 +58,7 @@ describe Zero::Renderer, 'read_template_path!' do
end
it 'creates an empty templates list without templates in path' do
subject = Zero::Renderer.new("bar", {})
subject = Zero::Renderer.new("bar/", {})
subject.read_template_path!
subject.templates.should eq({})

View File

@ -2,7 +2,7 @@ require 'spec_helper'
describe Zero::Renderer, '#render' do
subject { Zero::Renderer.new(template_path, type_map) }
let(:template_path) { 'spec/fixtures/templates' }
let(:template_path) { 'spec/fixtures/templates/' }
let(:type_map) {{
'html' => ['text/html', 'text/xml', '*/*'],
'json' => ['application/json', 'plain/text']

View File

@ -0,0 +1,65 @@
require 'spec_helper'
describe Zero::Renderer::TemplateFinder, '#initialize' do
subject { described_class.new(template_path, type_map) }
let(:template_path) { 'foo/' }
let(:file_list) { ['foo/welcome/index.html.erb'] }
before :each do
Dir.stub(:[]) do |arg|
if arg == 'foo/**/*.*'
file_list
else
[]
end
end
end
shared_examples_for 'a template loader' do
it 'creates a template tree' do
subject.get_templates['welcome/index'].should eq(result)
end
end
context 'without mapping' do
let(:type_map) { {} }
let(:result) { { 'html' => 'foo/welcome/index.html.erb' } }
it_behaves_like 'a template loader'
end
context 'with a single mapping' do
let(:type_map) { {'html' => 'text/html' } }
let(:result) { { 'text/html' => 'foo/welcome/index.html.erb' } }
it_behaves_like 'a template loader'
end
context 'with multiple mappings' do
let(:type_map) { {'html' => ['text/html', 'text/xml'] } }
let(:result) { {
'text/html' => 'foo/welcome/index.html.erb',
'text/xml' => 'foo/welcome/index.html.erb'
} }
it_behaves_like 'a template loader'
end
context 'with default template' do
let(:file_list) {['foo/welcome/index.erb']}
let(:type_map) { {'default' => ['text/html', 'text/xml'] } }
let(:result) { {
'text/html' => 'foo/welcome/index.erb',
'text/xml' => 'foo/welcome/index.erb'
} }
it_behaves_like 'a template loader'
end
it 'creates an empty templates list without templates in path' do
subject = Zero::Renderer.new("bar/", {})
subject.read_template_path!
subject.templates.should eq({})
end
end

View File

@ -0,0 +1,19 @@
require 'spec_helper'
describe Zero::Renderer::TemplateFinder, '#initialize' do
subject { described_class.new(template_path, type_map) }
let(:template_path) { 'foo/' }
let(:type_map) { {'html' => ['text/html']} }
its(:path) { should be(template_path) }
its(:path_regex) { should eq(/#{template_path}/) }
its(:type_map) { should be(type_map) }
context 'with broken path' do
let(:template_path) { 'foo' }
it "raises an error" do
expect { subject }.to raise_error(ArgumentError, "Has to end on '/'!")
end
end
end