Skip to content

Commit c7b991e

Browse files
committed
feat: New eval function to evaluate a template
This makes it possible to: * modularize a template by using an embedded template (defined with Go's built-in `define` action) as a function whose return value is the expansion of the template * post-process the expansion of a template (e.g., pipe it to Sprig's `indent` function) For additional context, see <golang/go#54748>. Sprig is unlikely to add an equivalent to this function any time soon: the function must be a closure around the template, meaning either Sprig or Go would have to change its API.
1 parent 3b871c2 commit c7b991e

File tree

3 files changed

+66
-6
lines changed

3 files changed

+66
-6
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ For example, this is a JSON version of an emitted RuntimeContainer struct:
361361
* *`contains $map $key`*: Returns `true` if `$map` contains `$key`. Takes maps from `string` to any type.
362362
* *`dir $path`*: Returns an array of filenames in the specified `$path`.
363363
* *`exists $path`*: Returns `true` if `$path` refers to an existing file or directory. Takes a string.
364+
* *`eval $templateName [$data]`*: Evaluates the named template like Go's built-in `template` action, but instead of writing out the result it returns the result as a string so that it can be post-processed. The `$data` argument may be omitted, which is equivalent to passing `nil`.
364365
* *`groupBy $containers $fieldPath`*: Groups an array of `RuntimeContainer` instances based on the values of a field path expression `$fieldPath`. A field path expression is a dot-delimited list of map keys or struct member names specifying the path from container to a nested value, which must be a string. Returns a map from the value of the field path expression to an array of containers having that value. Containers that do not have a value for the field path in question are omitted.
365366
* *`groupByKeys $containers $fieldPath`*: Returns the same as `groupBy` but only returns the keys of the map.
366367
* *`groupByMulti $containers $fieldPath $sep`*: Like `groupBy`, but the string value specified by `$fieldPath` is first split by `$sep` into a list of strings. A container whose `$fieldPath` value contains a list of strings will show up in the map output under each of those strings.

internal/template/template.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package template
33
import (
44
"bufio"
55
"bytes"
6+
"errors"
67
"fmt"
78
"io"
89
"io/ioutil"
@@ -43,11 +44,27 @@ func getArrayValues(funcName string, entries interface{}) (*reflect.Value, error
4344
}
4445

4546
func newTemplate(name string) *template.Template {
46-
tmpl := template.New(name).Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap{
47+
tmpl := template.New(name)
48+
// The eval function is defined here because it must be a closure around tmpl.
49+
eval := func(name string, args ...any) (string, error) {
50+
buf := bytes.NewBuffer(nil)
51+
data := any(nil)
52+
if len(args) == 1 {
53+
data = args[0]
54+
} else if len(args) > 1 {
55+
return "", errors.New("too many arguments")
56+
}
57+
if err := tmpl.ExecuteTemplate(buf, name, data); err != nil {
58+
return "", err
59+
}
60+
return buf.String(), nil
61+
}
62+
tmpl.Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap{
4763
"closest": arrayClosest,
4864
"coalesce": coalesce,
4965
"contains": contains,
5066
"dir": dirList,
67+
"eval": eval,
5168
"exists": utils.PathExists,
5269
"groupBy": groupBy,
5370
"groupByKeys": groupByKeys,

internal/template/template_test.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package template
22

33
import (
44
"bytes"
5+
"errors"
56
"reflect"
67
"strconv"
78
"strings"
@@ -13,28 +14,37 @@ import (
1314
type templateTestList []struct {
1415
tmpl string
1516
context interface{}
16-
expected string
17+
expected interface{}
1718
}
1819

1920
func (tests templateTestList) run(t *testing.T) {
2021
for n, test := range tests {
2122
test := test
2223
t.Run(strconv.Itoa(n), func(t *testing.T) {
2324
t.Parallel()
25+
wantErr, _ := test.expected.(error)
26+
want, ok := test.expected.(string)
27+
if !ok && wantErr == nil {
28+
t.Fatalf("test bug: want a string or error for .expected, got %v", test.expected)
29+
}
2430
tmpl, err := newTemplate("testTemplate").Parse(test.tmpl)
2531
if err != nil {
2632
t.Fatalf("Template parse failed: %v", err)
2733
}
2834

2935
var b bytes.Buffer
3036
err = tmpl.ExecuteTemplate(&b, "testTemplate", test.context)
37+
got := b.String()
3138
if err != nil {
39+
if wantErr != nil {
40+
return
41+
}
3242
t.Fatalf("Error executing template: %v", err)
43+
} else if wantErr != nil {
44+
t.Fatalf("Expected error, got %v", got)
3345
}
34-
35-
got := b.String()
36-
if test.expected != got {
37-
t.Fatalf("Incorrect output found; expected %s, got %s", test.expected, got)
46+
if want != got {
47+
t.Fatalf("Incorrect output found; want %#v, got %#v", want, got)
3848
}
3949
})
4050
}
@@ -151,3 +161,35 @@ func TestSprig(t *testing.T) {
151161
})
152162
}
153163
}
164+
165+
func TestEval(t *testing.T) {
166+
for _, tc := range []struct {
167+
desc string
168+
tts templateTestList
169+
}{
170+
{"undefined", templateTestList{
171+
{`{{eval "missing"}}`, nil, errors.New("")},
172+
{`{{eval "missing" nil}}`, nil, errors.New("")},
173+
{`{{eval "missing" "abc"}}`, nil, errors.New("")},
174+
{`{{eval "missing" "abc" "def"}}`, nil, errors.New("")},
175+
}},
176+
// The purpose of the "ctx" context is to assert that $ and . inside the template is the
177+
// eval argument, not the global context.
178+
{"noArg", templateTestList{
179+
{`{{define "T"}}{{$}}{{.}}{{end}}{{eval "T"}}`, "ctx", "<no value><no value>"},
180+
}},
181+
{"nilArg", templateTestList{
182+
{`{{define "T"}}{{$}}{{.}}{{end}}{{eval "T" nil}}`, "ctx", "<no value><no value>"},
183+
}},
184+
{"oneArg", templateTestList{
185+
{`{{define "T"}}{{$}}{{.}}{{end}}{{eval "T" "arg"}}`, "ctx", "argarg"},
186+
}},
187+
{"moreThanOneArg", templateTestList{
188+
{`{{define "T"}}{{$}}{{.}}{{end}}{{eval "T" "a" "b"}}`, "ctx", errors.New("")},
189+
}},
190+
} {
191+
t.Run(tc.desc, func(t *testing.T) {
192+
tc.tts.run(t)
193+
})
194+
}
195+
}

0 commit comments

Comments
 (0)