Skip to content

Commit

Permalink
extracted the building of template tree
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Gibheer committed Feb 12, 2013
1 parent 8ff98ff commit f18ab69
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 49 deletions.
48 changes: 4 additions & 44 deletions lib/zero/renderer.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'zero/renderer/template_finder'

module Zero
# the base renderer for getting render containers
#
Expand Down Expand Up @@ -34,7 +36,7 @@ class Renderer
# @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

Expand All @@ -56,23 +58,7 @@ def initialize(template_path, type_map = {})
# 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
Expand All @@ -89,32 +75,6 @@ def render(name, type, context)

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
Expand Down
164 changes: 164 additions & 0 deletions lib/zero/renderer/template_finder.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions spec/unit/zero/renderer/read_template_path_bang_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -58,7 +58,7 @@
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({})
Expand Down
2 changes: 1 addition & 1 deletion spec/unit/zero/renderer/render_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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']
Expand Down
65 changes: 65 additions & 0 deletions spec/unit/zero/renderer/template_finder/get_templates_spec.rb
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions spec/unit/zero/renderer/template_finder/initialize_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit f18ab69

Please sign in to comment.