Skip to content

Commit

Permalink
Improve Error system (#70)
Browse files Browse the repository at this point in the history
As a library user, we needed a way to translate error messages coming from the library.
This pull request introduces a way to let library users customize error messages if they want. It is as well fully retro-compatible as it doesn't change the current error messages.

What the pull request changes:

* Make the error type public
* Add an error code to let library users identify the error reason
* Add a `Reason` attribute for errors cases that need the name of the entity generating the error
* Make the `Line`, `Code` and `Reason` attribute public to let library users customize (translate) error message
* Keep the current error messages
  • Loading branch information
gsempe authored Jul 12, 2022
1 parent 3fabeb4 commit c1e7de1
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 16 deletions.
66 changes: 66 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package mustache

import (
"fmt"
)

// ErrorCode is the list of allowed values for the error's code.
type ErrorCode string

// List of values that ErrorCode can take.
const (
ErrUnmatchedOpenTag ErrorCode = "unmatched_open_tag"
ErrEmptyTag ErrorCode = "empty_tag"
ErrSectionNoClosingTag ErrorCode = "section_no_closing_tag"
ErrInterleavedClosingTag ErrorCode = "interleaved_closing_tag"
ErrInvalidMetaTag ErrorCode = "invalid_meta_tag"
ErrUnmatchedCloseTag ErrorCode = "unmatched_close_tag"
)

// ParseError represents an error during the parsing
type ParseError struct {
// Line contains the line of the error
Line int
// Code contains the error code of the error
Code ErrorCode
// Reason contains the name of the element generating the error
Reason string
}

func (e ParseError) Error() string {
return fmt.Sprintf("line %d: %s", e.Line, e.defaultMessage())
}

func (e ParseError) defaultMessage() string {
switch e.Code {
case ErrUnmatchedOpenTag:
return "unmatched open tag"
case ErrEmptyTag:
return "empty tag"
case ErrSectionNoClosingTag:
return fmt.Sprintf("Section %s has no closing tag", e.Reason)
case ErrInterleavedClosingTag:
return fmt.Sprintf("interleaved closing tag: %s", e.Reason)
case ErrInvalidMetaTag:
return "Invalid meta tag"
case ErrUnmatchedCloseTag:
return "unmatched close tag"
default:
return "unknown error"
}
}

func newError(line int, code ErrorCode) ParseError {
return ParseError{
Line: line,
Code: code,
}
}

func newErrorWithReason(line int, code ErrorCode, reason string) ParseError {
return ParseError{
Line: line,
Code: code,
Reason: reason,
}
}
23 changes: 7 additions & 16 deletions mustache.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,6 @@ type Template struct {
partial PartialProvider
}

type parseError struct {
line int
message string
}

// Tags returns the mustache tags for the given template
func (tmpl *Template) Tags() []Tag {
return extractTags(tmpl.elems)
Expand Down Expand Up @@ -174,10 +169,6 @@ func (e *partialElement) Tags() []Tag {
return nil
}

func (p parseError) Error() string {
return fmt.Sprintf("line %d: %s", p.line, p.message)
}

func (tmpl *Template) readString(s string) (string, error) {
newlines := 0
for i := tmpl.p; ; i++ {
Expand Down Expand Up @@ -270,15 +261,15 @@ func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) {

if err == io.EOF {
//put the remaining text in a block
return nil, parseError{tmpl.curline, "unmatched open tag"}
return nil, newError(tmpl.curline, ErrUnmatchedOpenTag)
}

text = text[:len(text)-len(tmpl.ctag)]

//trim the close tag off the text
tag := strings.TrimSpace(text)
if len(tag) == 0 {
return nil, parseError{tmpl.curline, "empty tag"}
return nil, newError(tmpl.curline, ErrEmptyTag)
}

eow := tmpl.p
Expand Down Expand Up @@ -334,7 +325,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error {

if err == io.EOF {
//put the remaining text in a block
return parseError{section.startline, "Section " + section.name + " has no closing tag"}
return newErrorWithReason(section.startline, ErrSectionNoClosingTag, section.name)
}

// put text into an item
Expand Down Expand Up @@ -364,7 +355,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error {
case '/':
name := strings.TrimSpace(tag[1:])
if name != section.name {
return parseError{tmpl.curline, "interleaved closing tag: " + name}
return newErrorWithReason(tmpl.curline, ErrInterleavedClosingTag, name)
}
return nil
case '>':
Expand All @@ -376,7 +367,7 @@ func (tmpl *Template) parseSection(section *sectionElement) error {
section.elems = append(section.elems, partial)
case '=':
if tag[len(tag)-1] != '=' {
return parseError{tmpl.curline, "Invalid meta tag"}
return newError(tmpl.curline, ErrInvalidMetaTag)
}
tag = strings.TrimSpace(tag[1 : len(tag)-1])
newtags := strings.SplitN(tag, " ", 2)
Expand Down Expand Up @@ -437,7 +428,7 @@ func (tmpl *Template) parse() error {
}
tmpl.elems = append(tmpl.elems, &se)
case '/':
return parseError{tmpl.curline, "unmatched close tag"}
return newError(tmpl.curline, ErrUnmatchedCloseTag)
case '>':
name := strings.TrimSpace(tag[1:])
partial, err := tmpl.parsePartial(name, textResult.padding)
Expand All @@ -447,7 +438,7 @@ func (tmpl *Template) parse() error {
tmpl.elems = append(tmpl.elems, partial)
case '=':
if tag[len(tag)-1] != '=' {
return parseError{tmpl.curline, "Invalid meta tag"}
return newError(tmpl.curline, ErrInvalidMetaTag)
}
tag = strings.TrimSpace(tag[1 : len(tag)-1])
newtags := strings.SplitN(tag, " ", 2)
Expand Down
35 changes: 35 additions & 0 deletions mustache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mustache

import (
"bytes"
"errors"
"fmt"
"os"
"path"
Expand Down Expand Up @@ -459,6 +460,40 @@ func TestMalformed(t *testing.T) {
}
}

type TestWithParseError struct {
*Test
errLine int
errCode ErrorCode
errReason string
}

var malformedWithParseError = []TestWithParseError{
{Test: &Test{`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "", fmt.Errorf("line 1: empty tag")}, errLine: 1, errCode: ErrEmptyTag, errReason: ""},
{Test: &Test{`{{}}`, nil, "", fmt.Errorf("line 1: empty tag")}, errLine: 1, errCode: ErrEmptyTag, errReason: ""},
{Test: &Test{`{{}`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, errLine: 1, errCode: ErrUnmatchedOpenTag, errReason: ""},
{Test: &Test{`{{`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, errLine: 1, errCode: ErrUnmatchedOpenTag, errReason: ""},
// invalid syntax - https://github.com/hoisie/mustache/issues/10
{Test: &Test{`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")}, errLine: 1, errCode: ErrInterleavedClosingTag, errReason: "a"},
}

func TestParseError(t *testing.T) {
for _, test := range malformedWithParseError {
output, err := Render(test.tmpl, test.context)
if err != nil {
var parseError ParseError
if errors.As(err, &parseError) {
if parseError.Line != test.errLine || parseError.Code != test.errCode || parseError.Reason != test.errReason {
t.Errorf("%q expected ParseError (line %q code %q reason %q) but got (line %q code %q reason %q)", test.tmpl, test.errLine, test.errCode, test.errReason, parseError.Line, parseError.Code, parseError.Reason)
}
} else {
t.Errorf("%q expected ParseError (line %q code %q reason %q) but got %q", test.tmpl, test.errLine, test.errCode, test.errReason, test.err.Error())
}
} else {
t.Errorf("%q expected error %q but got %q", test.tmpl, test.err.Error(), output)
}
}
}

type LayoutTest struct {
layout string
tmpl string
Expand Down

0 comments on commit c1e7de1

Please sign in to comment.