diff --git a/lib/zero/renderer.rb b/lib/zero/renderer.rb index b2b7c94..2a848c2 100644 --- a/lib/zero/renderer.rb +++ b/lib/zero/renderer.rb @@ -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 diff --git a/lib/zero/renderer/template_finder.rb b/lib/zero/renderer/template_finder.rb new file mode 100644 index 0000000..39fb8cb --- /dev/null +++ b/lib/zero/renderer/template_finder.rb @@ -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 diff --git a/spec/unit/zero/renderer/read_template_path_bang_spec.rb b/spec/unit/zero/renderer/read_template_path_bang_spec.rb index 8655777..948a3cc 100644 --- a/spec/unit/zero/renderer/read_template_path_bang_spec.rb +++ b/spec/unit/zero/renderer/read_template_path_bang_spec.rb @@ -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({}) diff --git a/spec/unit/zero/renderer/render_spec.rb b/spec/unit/zero/renderer/render_spec.rb index 30d2225..3270ac3 100644 --- a/spec/unit/zero/renderer/render_spec.rb +++ b/spec/unit/zero/renderer/render_spec.rb @@ -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'] diff --git a/spec/unit/zero/renderer/template_finder/get_templates_spec.rb b/spec/unit/zero/renderer/template_finder/get_templates_spec.rb new file mode 100644 index 0000000..5928e66 --- /dev/null +++ b/spec/unit/zero/renderer/template_finder/get_templates_spec.rb @@ -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 diff --git a/spec/unit/zero/renderer/template_finder/initialize_spec.rb b/spec/unit/zero/renderer/template_finder/initialize_spec.rb new file mode 100644 index 0000000..5ed9502 --- /dev/null +++ b/spec/unit/zero/renderer/template_finder/initialize_spec.rb @@ -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