Skip to content

Commit

Permalink
Experimental support for lambda sections
Browse files Browse the repository at this point in the history
  • Loading branch information
frohmut authored and cbroglie committed Sep 20, 2021
1 parent b2d8af2 commit 08d1d98
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 6 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ This library is an implementation of the Mustache template language in Go.

### Mustache Spec Compliance

[mustache/spec](https://github.com/mustache/spec) contains the formal standard for Mustache, and it is included as a submodule (using v1.2.1) for testing compliance. All of the tests pass (big thanks to [kei10in](https://github.com/kei10in)), with the exception of the null interpolation tests added in v1.2.1. The optional inheritance and lambda support has not been implemented.
[mustache/spec](https://github.com/mustache/spec) contains the formal standard for Mustache, and it is included as a submodule (using v1.2.1) for testing compliance. All of the tests pass (big thanks to [kei10in](https://github.com/kei10in)), with the exception of the null interpolation tests added in v1.2.1. There is experimental support for a subset of the optional lambda functionality (thanks to [fromhut](https://github.com/fromhut)). The optional inheritance functionality has not been implemented.

----

Expand Down
69 changes: 66 additions & 3 deletions mustache.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ var (
AllowMissingVariables = true
)

// RenderFunc is provided to lambda functions for rendering.
type RenderFunc func(text string) (string, error)

// LambdaFunc is the signature for lambda functions.
type LambdaFunc func(text string, render RenderFunc) (string, error)

// A TagType represents the specific type of mustache tag that a Tag
// represents. The zero TagType is not a valid type.
type TagType uint
Expand Down Expand Up @@ -347,7 +353,6 @@ func (tmpl *Template) parseSection(section *sectionElement) error {
switch tag[0] {
case '!':
//ignore comment
break
case '#', '^':
name := strings.TrimSpace(tag[1:])
se := sectionElement{name, tag[0] == '^', tmpl.curline, []interface{}{}}
Expand Down Expand Up @@ -423,7 +428,6 @@ func (tmpl *Template) parse() error {
switch tag[0] {
case '!':
//ignore comment
break
case '#', '^':
name := strings.TrimSpace(tag[1:])
se := sectionElement{name, tag[0] == '^', tmpl.curline, []interface{}{}}
Expand Down Expand Up @@ -528,7 +532,7 @@ Outer:
if allowMissing {
return reflect.Value{}, nil
}
return reflect.Value{}, fmt.Errorf("Missing variable %q", name)
return reflect.Value{}, fmt.Errorf("missing variable %q", name)
}

func isEmpty(v reflect.Value) bool {
Expand Down Expand Up @@ -589,6 +593,32 @@ func renderSection(section *sectionElement, contextChain []interface{}, buf io.W
}
case reflect.Map, reflect.Struct:
contexts = append(contexts, value)
case reflect.Func:
if val.Type().NumIn() != 2 || val.Type().NumOut() != 2 {
return fmt.Errorf("lambda %q doesn't match required LambaFunc signature", section.name)
}
var text bytes.Buffer
if err := getSectionText(section.elems, &text); err != nil {
return err
}
render := func(text string) (string, error) {
tmpl, err := ParseString(text)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tmpl.renderTemplate(contextChain, &buf); err != nil {
return "", err
}
return buf.String(), nil
}
in := []reflect.Value{reflect.ValueOf(text.String()), reflect.ValueOf(render)}
res := val.Call(in)
if !res[1].IsNil() {
return fmt.Errorf("lambda %q: %w", section.name, res[1].Interface().(error))
}
fmt.Fprint(buf, res[0].String())
return nil
default:
// Spec: Non-false sections have their value at the top of context,
// accessible as {{.}} or through the parent context. This gives
Expand All @@ -613,6 +643,39 @@ func renderSection(section *sectionElement, contextChain []interface{}, buf io.W
return nil
}

func getSectionText(elements []interface{}, buf io.Writer) error {
for _, element := range elements {
if err := getElementText(element, buf); err != nil {
return err
}
}
return nil
}

func getElementText(element interface{}, buf io.Writer) error {
switch elem := element.(type) {
case *textElement:
fmt.Fprintf(buf, "%s", elem.text)
case *varElement:
fmt.Fprintf(buf, "{{%s}}", elem.name)
case *sectionElement:
if elem.inverted {
fmt.Fprintf(buf, "{{^%s}}", elem.name)
} else {
fmt.Fprintf(buf, "{{#%s}}", elem.name)
}
for _, nelem := range elem.elems {
if err := getElementText(nelem, buf); err != nil {
return err
}
}
fmt.Fprintf(buf, "{{/%s}}", elem.name)
default:
return fmt.Errorf("unexpected element type %T", elem)
}
return nil
}

func renderElement(element interface{}, contextChain []interface{}, buf io.Writer) error {
switch elem := element.(type) {
case *textElement:
Expand Down
84 changes: 83 additions & 1 deletion mustache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func TestMissing(t *testing.T) {
output, err := Render(test.tmpl, test.context)
if err == nil {
t.Errorf("%q expected missing variable error but got %q", test.tmpl, output)
} else if !strings.Contains(err.Error(), "Missing variable") {
} else if !strings.Contains(err.Error(), "missing variable") {
t.Errorf("%q expected missing variable error but got %q", test.tmpl, err.Error())
}
}
Expand Down Expand Up @@ -349,6 +349,88 @@ func TestMultiContext(t *testing.T) {
}
}

func TestLambda(t *testing.T) {
tmpl := `{{#lambda}}Hello {{name}}. {{#sub}}{{.}} {{/sub}}{{^negsub}}nothing{{/negsub}}{{/lambda}}`
data := map[string]interface{}{
"name": "world",
"sub": []string{"subv1", "subv2"},
"lambda": func(text string, render RenderFunc) (string, error) {
res, err := render(text)
return res + "!", err
},
}

output, err := Render(tmpl, data)
if err != nil {
t.Fatal(err)
}
expect := "Hello world. subv1 subv2 nothing!"
if output != expect {
t.Fatalf("TestLambda expected %q got %q", expect, output)
}
}

func TestLambdaStruct(t *testing.T) {
tmpl := `{{#Lambda}}Hello {{Name}}. {{#Sub}}{{.}} {{/Sub}}{{^Negsub}}nothing{{/Negsub}}{{/Lambda}}`
data := struct {
Name string
Sub []string
Lambda LambdaFunc
}{
Name: "world",
Sub: []string{"subv1", "subv2"},
Lambda: func(text string, render RenderFunc) (string, error) {
res, err := render(text)
return res + "!", err
},
}

output, err := Render(tmpl, data)
if err != nil {
t.Fatal(err)
}
expect := "Hello world. subv1 subv2 nothing!"
if output != expect {
t.Fatalf("TestLambdaStruct expected %q got %q", expect, output)
}
}

func TestLambdaError(t *testing.T) {
tmpl := `{{#lambda}}{{/lambda}}`
data := map[string]interface{}{
"lambda": func(text string, render RenderFunc) (string, error) {
return "", fmt.Errorf("test err")
},
}
_, err := Render(tmpl, data)
if err == nil {
t.Fatal("nil error")
}

expect := `lambda "lambda": test err`
if err.Error() != expect {
t.Fatalf("TestLambdaError expected %q got %q", expect, err.Error())
}
}

func TestLambdaWrongSignature(t *testing.T) {
tmpl := `{{#lambda}}{{/lambda}}`
data := map[string]interface{}{
"lambda": func(text string, render RenderFunc, _ string) (string, error) {
return render(text)
},
}
_, err := Render(tmpl, data)
if err == nil {
t.Fatal("nil error")
}

expect := `lambda "lambda" doesn't match required LambaFunc signature`
if err.Error() != expect {
t.Fatalf("TestLambdaWrongSignature expected %q got %q", expect, err.Error())
}
}

var malformed = []Test{
{`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "", fmt.Errorf("line 1: empty tag")},
{`{{}}`, nil, "", fmt.Errorf("line 1: empty tag")},
Expand Down
37 changes: 36 additions & 1 deletion spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@ var disabledTests = map[string]map[string]struct{}{
"Triple Mustache Null Interpolation": struct{}{},
"Ampersand Null Interpolation": struct{}{},
},
"~lambdas.json": {}, // not implemented
"~inheritance.json": {}, // not implemented
"~lambdas.json": {
"Interpolation": struct{}{},
"Interpolation - Expansion": struct{}{},
"Interpolation - Alternate Delimiters": struct{}{},
"Interpolation - Multiple Calls": struct{}{},
"Escaping": struct{}{},
"Section - Alternate Delimiters": struct{}{},
"Inverted Section": struct{}{},
},
}

type specTest struct {
Expand Down Expand Up @@ -79,6 +87,12 @@ func runTest(t *testing.T, file string, test *specTest) {
}
}

// We can't generate lambda functions at runtime; instead we define them in
// code to match the spec tests and inject them here at runtime.
if file == "~lambdas.json" {
test.Data.(map[string]interface{})["lambda"] = lambdas[test.Name]
}

var out string
var err error
if len(test.Partials) > 0 {
Expand All @@ -97,3 +111,24 @@ func runTest(t *testing.T, file string, test *specTest) {

t.Logf("[%s %s]: Passed", file, test.Name)
}

// Define the lambda functions to match those in the spec tests. The javascript
// implementations from the spec tests are included for reference.
var lambdas = map[string]LambdaFunc{
"Section": func(text string, render RenderFunc) (string, error) {
// function(txt) { return (txt == "{{x}}" ? "yes" : "no") }
if text == "{{x}}" {
return "yes", nil
} else {
return "no", nil
}
},
"Section - Expansion": func(text string, render RenderFunc) (string, error) {
// function(txt) { return txt + "{{planet}}" + txt }
return render(text + "{{planet}}" + text)
},
"Section - Multiple Calls": func(text string, render RenderFunc) (string, error) {
// function(txt) { return "__" + txt + "__" }
return render("__" + text + "__")
},
}

0 comments on commit 08d1d98

Please sign in to comment.