Skip to content
This repository was archived by the owner on Jan 21, 2020. It is now read-only.

Commit 17f7130

Browse files
author
David Chung
authored
Template context (#373)
Signed-off-by: David Chung <[email protected]>
1 parent 9d6641d commit 17f7130

File tree

7 files changed

+358
-12
lines changed

7 files changed

+358
-12
lines changed

pkg/template/funcs.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package template
33
import (
44
"encoding/json"
55
"fmt"
6+
"reflect"
67
"strings"
78
"time"
89

@@ -75,6 +76,35 @@ func UnixTime() interface{} {
7576
return time.Now().Unix()
7677
}
7778

79+
// Index returns the index of search in array. -1 if not found or array is not iterable. An optional true will
80+
// turn on strict type check while by default string representations are used to compare values.
81+
func Index(srch interface{}, array interface{}, strictOptional ...bool) int {
82+
strict := false
83+
if len(strictOptional) > 0 {
84+
strict = strictOptional[0]
85+
}
86+
switch reflect.TypeOf(array).Kind() {
87+
case reflect.Slice:
88+
s := reflect.ValueOf(array)
89+
for i := 0; i < s.Len(); i++ {
90+
if reflect.DeepEqual(srch, s.Index(i).Interface()) {
91+
return i
92+
}
93+
if !strict {
94+
// by string value which is useful for text based compares
95+
search := reflect.Indirect(reflect.ValueOf(srch)).Interface()
96+
value := reflect.Indirect(s.Index(i)).Interface()
97+
searchStr := fmt.Sprintf("%v", search)
98+
check := fmt.Sprintf("%v", value)
99+
if searchStr == check {
100+
return i
101+
}
102+
}
103+
}
104+
}
105+
return -1
106+
}
107+
78108
// DefaultFuncs returns a list of default functions for binding in the template
79109
func (t *Template) DefaultFuncs() map[string]interface{} {
80110
return map[string]interface{}{
@@ -102,6 +132,10 @@ func (t *Template) DefaultFuncs() map[string]interface{} {
102132
return included.Render(o)
103133
},
104134

135+
"loop": func(c int) []struct{} {
136+
return make([]struct{}, c)
137+
},
138+
105139
"var": func(name, doc string, v ...interface{}) interface{} {
106140
if found, has := t.binds[name]; has {
107141
return found
@@ -119,5 +153,6 @@ func (t *Template) DefaultFuncs() map[string]interface{} {
119153
"lines": SplitLines,
120154
"to_json": ToJSON,
121155
"from_json": FromJSON,
156+
"index": Index,
122157
}
123158
}

pkg/template/funcs_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,30 @@ func TestMapEncodeDecode(t *testing.T) {
280280

281281
require.Equal(t, expect, actual)
282282
}
283+
284+
func TestIndex(t *testing.T) {
285+
require.Equal(t, -1, Index("a", []string{"x", "y", "z"}))
286+
require.Equal(t, 1, Index("y", []string{"x", "y", "z"}))
287+
require.Equal(t, -1, Index(25, []string{"x", "y", "z"}))
288+
require.Equal(t, -1, Index(25, 26))
289+
require.Equal(t, 1, Index("y", []string{"x", "y", "z"}))
290+
require.Equal(t, 1, Index("y", []interface{}{"x", "y", "z"}))
291+
require.Equal(t, 1, Index(1, []interface{}{0, 1, 2}))
292+
require.Equal(t, 1, Index("1", []interface{}{0, 1, 2}))
293+
require.Equal(t, 1, Index(1, []interface{}{0, "1", 2}))
294+
require.Equal(t, -1, Index("1", []interface{}{0, 1, 2}, true)) // strict case type must match
295+
require.Equal(t, 1, Index("1", []interface{}{0, "1", 2}, true)) // strict case type must match
296+
require.Equal(t, -1, Index(1, []interface{}{0, "1", 2}, true)) // strict case type must match
297+
298+
v := "1"
299+
require.Equal(t, 1, Index(&v, []interface{}{0, "1", 2}))
300+
require.Equal(t, 1, Index(&v, []interface{}{0, &v, 2}, true))
301+
require.Equal(t, 1, Index(&v, []interface{}{0, &v, 2}))
302+
303+
a := "0"
304+
c := "2"
305+
require.Equal(t, 1, Index("1", []*string{&a, &v, &c}))
306+
307+
// This doesn't work because the type information is gone and we have just an address
308+
require.Equal(t, -1, Index("1", []interface{}{0, &v, 2}))
309+
}

pkg/template/integration_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,41 @@ systemctl start docker
120120
`,
121121
}
122122
)
123+
124+
func TestTemplateContext(t *testing.T) {
125+
126+
s := `
127+
{{ inc }}
128+
129+
{{ setString "hello" }}
130+
131+
{{ setBool true }}
132+
133+
{{ range loop 10 }}
134+
{{ inc }}
135+
{{ end }}
136+
137+
The count is {{count}}
138+
The message is {{str}}
139+
140+
{{ dec }}
141+
{{ range loop 5 }}
142+
{{ dec }}
143+
{{ end }}
144+
145+
The count is {{count}}
146+
The message is {{str}}
147+
`
148+
149+
tt, err := NewTemplate("str://"+s, Options{})
150+
require.NoError(t, err)
151+
152+
context := &context{}
153+
154+
_, err = tt.Render(context)
155+
require.NoError(t, err)
156+
157+
require.Equal(t, 5, context.Count)
158+
require.True(t, context.Bool)
159+
require.Equal(t, 23, context.invokes) // note this is private state not accessible in template
160+
}

pkg/template/template.go

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

33
import (
44
"bytes"
5+
"fmt"
56
"io"
7+
"reflect"
68
"strings"
79
"sync"
810
"text/template"
@@ -11,6 +13,30 @@ import (
1113
log "github.com/Sirupsen/logrus"
1214
)
1315

16+
// Function contains the description of an exported template function
17+
type Function struct {
18+
19+
// Name is the function name to bind in the template
20+
Name string
21+
22+
// Description provides help for the function
23+
Description string
24+
25+
// Func is the reference to the actual function
26+
Func interface{}
27+
}
28+
29+
// Context is a marker interface for a user-defined struct that is passed into the template engine (as context)
30+
// and accessible in the exported template functions. Template functions can have the signature
31+
// func(template.Context, arg1, arg2 ...) (string, error) and when functions like this are registered, the template
32+
// engine will dynamically create and export a function of the form func(arg1, arg2...) (string, error) where
33+
// the context instance becomes an out-of-band struct that can be mutated by functions. This in essence allows
34+
// structured data as output of the template, in addition to a string from evaluating the template.
35+
type Context interface {
36+
// Funcs returns a list of special template functions of the form func(template.Context, arg1, arg2) interface{}
37+
Funcs() []Function
38+
}
39+
1440
// Options contains parameters for customizing the behavior of the engine
1541
type Options struct {
1642

@@ -87,10 +113,10 @@ func (t *Template) Validate() (*Template, error) {
87113
t.lock.Lock()
88114
t.parsed = nil
89115
t.lock.Unlock()
90-
return t, t.build()
116+
return t, t.build(nil)
91117
}
92118

93-
func (t *Template) build() error {
119+
func (t *Template) build(context Context) error {
94120
t.lock.Lock()
95121
defer t.lock.Unlock()
96122

@@ -105,7 +131,21 @@ func (t *Template) build() error {
105131
}
106132

107133
for k, v := range t.funcs {
108-
fm[k] = v
134+
if tf, err := makeTemplateFunc(context, v); err == nil {
135+
fm[k] = tf
136+
} else {
137+
return err
138+
}
139+
}
140+
141+
if context != nil {
142+
for _, f := range context.Funcs() {
143+
if tf, err := makeTemplateFunc(context, f.Func); err == nil {
144+
fm[f.Name] = tf
145+
} else {
146+
return err
147+
}
148+
}
109149
}
110150

111151
parsed, err := template.New(t.url).Funcs(fm).Parse(string(t.body))
@@ -119,18 +159,75 @@ func (t *Template) build() error {
119159

120160
// Execute is a drop-in replace of the execute method of template
121161
func (t *Template) Execute(output io.Writer, context interface{}) error {
122-
if err := t.build(); err != nil {
162+
if err := t.build(toContext(context)); err != nil {
123163
return err
124164
}
125165
return t.parsed.Execute(output, context)
126166
}
127167

168+
func toContext(in interface{}) Context {
169+
var context Context
170+
if in != nil {
171+
if s, is := in.(Context); is {
172+
context = s
173+
}
174+
}
175+
return context
176+
}
177+
128178
// Render renders the template given the context
129179
func (t *Template) Render(context interface{}) (string, error) {
130-
if err := t.build(); err != nil {
180+
if err := t.build(toContext(context)); err != nil {
131181
return "", err
132182
}
133183
var buff bytes.Buffer
134184
err := t.parsed.Execute(&buff, context)
135185
return buff.String(), err
136186
}
187+
188+
// converts a function of f(Context, ags...) to a regular template function
189+
func makeTemplateFunc(ctx Context, f interface{}) (interface{}, error) {
190+
191+
contextType := reflect.TypeOf((*Context)(nil)).Elem()
192+
193+
ff := reflect.Indirect(reflect.ValueOf(f))
194+
// first we check to see if f has the special signature where the first
195+
// parameter is the context parameter...
196+
if ff.Kind() != reflect.Func {
197+
return nil, fmt.Errorf("not a function:%v", f)
198+
}
199+
200+
if ff.Type().In(0).AssignableTo(contextType) {
201+
202+
in := make([]reflect.Type, ff.Type().NumIn()-1) // exclude the context param
203+
out := make([]reflect.Type, ff.Type().NumOut())
204+
205+
for i := 1; i < ff.Type().NumIn(); i++ {
206+
in[i-1] = ff.Type().In(i)
207+
}
208+
variadic := false
209+
if len(in) > 0 {
210+
variadic = in[len(in)-1].Kind() == reflect.Slice
211+
}
212+
for i := 0; i < ff.Type().NumOut(); i++ {
213+
out[i] = ff.Type().Out(i)
214+
}
215+
funcType := reflect.FuncOf(in, out, variadic)
216+
funcImpl := func(in []reflect.Value) []reflect.Value {
217+
if !variadic {
218+
return ff.Call(append([]reflect.Value{reflect.ValueOf(ctx)}, in...))
219+
}
220+
221+
variadicParam := in[len(in)-1]
222+
last := make([]reflect.Value, variadicParam.Len())
223+
for i := 0; i < variadicParam.Len(); i++ {
224+
last[i] = variadicParam.Index(i)
225+
}
226+
return ff.Call(append(append([]reflect.Value{reflect.ValueOf(ctx)}, in[0:len(in)-1]...), last...))
227+
}
228+
229+
newFunc := reflect.MakeFunc(funcType, funcImpl)
230+
return newFunc.Interface(), nil
231+
}
232+
return ff.Interface(), nil
233+
}

0 commit comments

Comments
 (0)