This repository has been archived by the owner on Jan 21, 2020. It is now read-only.
/
funcs.go
328 lines (311 loc) · 9.35 KB
/
funcs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
package template
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
"github.com/docker/infrakit/pkg/types"
"github.com/jmespath/go-jmespath"
)
// DeepCopyObject makes a deep copy of the argument, using encoding/gob encode/decode.
func DeepCopyObject(from interface{}) (interface{}, error) {
var mod bytes.Buffer
enc := json.NewEncoder(&mod)
dec := json.NewDecoder(&mod)
err := enc.Encode(from)
if err != nil {
return nil, err
}
copy := reflect.New(reflect.TypeOf(from))
err = dec.Decode(copy.Interface())
if err != nil {
return nil, err
}
return reflect.Indirect(copy).Interface(), nil
}
// QueryObject applies a JMESPath query specified by the expression, against the target object.
func QueryObject(exp string, target interface{}) (interface{}, error) {
query, err := jmespath.Compile(exp)
if err != nil {
return nil, err
}
return query.Search(target)
}
// SplitLines splits the input into a string slice.
func SplitLines(o interface{}) ([]string, error) {
ret := []string{}
switch o := o.(type) {
case string:
return strings.Split(o, "\n"), nil
case []byte:
return strings.Split(string(o), "\n"), nil
}
return ret, fmt.Errorf("not-supported-value-type")
}
// FromJSON decode the input JSON encoded as string or byte slice into a map.
func FromJSON(o interface{}) (interface{}, error) {
var ret interface{}
switch o := o.(type) {
case string:
err := json.Unmarshal([]byte(o), &ret)
return ret, err
case []byte:
err := json.Unmarshal(o, &ret)
return ret, err
case *types.Any:
err := json.Unmarshal(o.Bytes(), &ret)
return ret, err
}
return ret, fmt.Errorf("not-supported-value-type")
}
// ToJSON encodes the input struct into a JSON string.
func ToJSON(o interface{}) (string, error) {
buff, err := json.MarshalIndent(o, "", " ")
return string(buff), err
}
// ToJSONFormat encodes the input struct into a JSON string with format prefix, and indent.
func ToJSONFormat(prefix, indent string, o interface{}) (string, error) {
buff, err := json.MarshalIndent(o, prefix, indent)
return string(buff), err
}
// FromMap decodes map into raw struct
func FromMap(m map[string]interface{}, raw interface{}) error {
// The safest way, but the slowest, is to just marshal and unmarshal back
buff, err := ToJSON(m)
if err != nil {
return err
}
return json.Unmarshal([]byte(buff), raw)
}
// ToMap encodes the input as a map
func ToMap(raw interface{}) (map[string]interface{}, error) {
buff, err := ToJSON(raw)
if err != nil {
return nil, err
}
out, err := FromJSON(buff)
return out.(map[string]interface{}), err
}
// UnixTime returns a timestamp in unix time
func UnixTime() interface{} {
return time.Now().Unix()
}
// IndexOf returns the index of search in array. -1 if not found or array is not iterable. An optional true will
// turn on strict type check while by default string representations are used to compare values.
func IndexOf(srch interface{}, array interface{}, strictOptional ...bool) int {
strict := false
if len(strictOptional) > 0 {
strict = strictOptional[0]
}
switch reflect.TypeOf(array).Kind() {
case reflect.Slice:
s := reflect.ValueOf(array)
for i := 0; i < s.Len(); i++ {
if reflect.DeepEqual(srch, s.Index(i).Interface()) {
return i
}
if !strict {
// by string value which is useful for text based compares
search := reflect.Indirect(reflect.ValueOf(srch)).Interface()
value := reflect.Indirect(s.Index(i)).Interface()
searchStr := fmt.Sprintf("%v", search)
check := fmt.Sprintf("%v", value)
if searchStr == check {
return i
}
}
}
}
return -1
}
// DefaultFuncs returns a list of default functions for binding in the template
func (t *Template) DefaultFuncs() []Function {
return []Function{
{
Name: "source",
Description: []string{
"Source / evaluate the template at the input location (as URL).",
"This will make all of the global variables declared there visible in this template's context.",
"Similar to 'source' in bash, sourcing another template means applying it in the same context ",
"as the calling template. The context (e.g. variables) of the calling template as a result can be mutated.",
},
Func: func(p string, opt ...interface{}) (string, error) {
var o interface{}
if len(opt) > 0 {
o = opt[0]
}
loc := p
if strings.Index(loc, "str://") == -1 {
buff, err := getURL(t.url, p)
if err != nil {
return "", err
}
loc = buff
}
sourced, err := NewTemplate(loc, t.options)
if err != nil {
return "", err
}
// set this as the parent of the sourced template so its global can mutate the globals in this
sourced.parent = t
sourced.forkFrom(t)
sourced.context = t.context
if o == nil {
o = sourced.context
}
// TODO(chungers) -- let the sourced template define new functions that can be called in the parent.
return sourced.Render(o)
},
},
{
Name: "include",
Description: []string{
"Render content found at URL as template and include here.",
"The optional second parameter is the context to use when rendering the template.",
"Conceptually similar to exec in bash, where the template included is applied using a fork ",
"of current context in the calling template. Any mutations to the context via 'global' will not ",
"be visible in the calling template's context.",
},
Func: func(p string, opt ...interface{}) (string, error) {
var o interface{}
if len(opt) > 0 {
o = opt[0]
}
loc := p
if strings.Index(loc, "str://") == -1 {
buff, err := getURL(t.url, p)
if err != nil {
return "", err
}
loc = buff
}
included, err := NewTemplate(loc, t.options)
if err != nil {
return "", err
}
dotCopy, err := included.forkFrom(t)
if err != nil {
return "", err
}
included.context = dotCopy
if o == nil {
o = included.context
}
return included.Render(o)
},
},
{
Name: "loop",
Description: []string{
"Loop generates a slice of length specified by the input. For use like {{ range loop 5 }}...{{ end }}",
},
Func: func(c int) []struct{} {
return make([]struct{}, c)
},
},
{
Name: "global",
Description: []string{
"Sets a global variable named after the first argument, with the value as the second argument.",
"This is similar to def (which sets the default value).",
"Global variables are propagated to all templates that are rendered via the 'include' function.",
},
Func: func(n string, v interface{}) Void {
t.Global(n, v)
return voidValue
},
},
{
Name: "def",
Description: []string{
"Defines a variable with the first argument as name and last argument value as the default.",
"It's also ok to pass a third optional parameter, in the middle, as the documentation string.",
},
Func: func(name string, args ...interface{}) (Void, error) {
if _, has := t.defaults[name]; has {
// not sure if this is good, but should complain loudly
return voidValue, fmt.Errorf("already defined: %v", name)
}
var doc string
var value interface{}
switch len(args) {
case 1:
// just value, no docs
value = args[0]
case 2:
// docs and value
doc = fmt.Sprintf("%v", args[0])
value = args[1]
}
t.Def(name, value, doc)
return voidValue, nil
},
},
{
Name: "ref",
Description: []string{
"References / gets the variable named after the first argument.",
"The values must be set first by either def or global.",
},
Func: t.Ref,
},
{
Name: "q",
Description: []string{
"Runs a JMESPath (http://jmespath.org/) query (first arg) on the object (second arg).",
"The return value is an object which needs to be rendered properly for the format of the document.",
"Example: {{ include \"https://httpbin.org/get\" | from_json | q \"origin\" }} returns the origin of http request.",
},
Func: QueryObject,
},
{
Name: "to_json",
Description: []string{
"Encodes the input as a JSON string",
"This is useful for taking an object (interface{}) and render it inline as proper JSON.",
"Example: {{ include \"https://httpbin.org/get\" | from_json | to_json }}",
},
Func: ToJSON,
},
{
Name: "to_json_format",
Description: []string{
"Encodes the input as a JSON string with first arg as prefix, second arg the indentation, then the object",
},
Func: ToJSONFormat,
},
{
Name: "from_json",
Description: []string{
"Decodes the input (first arg) into a structure (a map[string]interface{} or []interface{}).",
"This is useful for parsing arbitrary resources in JSON format as object. The object is the queryable via 'q'",
"For example: {{ include \"https://httpbin.org/get\" | from_json | q \"origin\" }} returns the origin of request.",
},
Func: FromJSON,
},
{
Name: "unixtime",
Description: []string{
"Returns the unix timestamp as the number of seconds elapsed since January 1, 1970 UTC.",
},
Func: UnixTime,
},
{
Name: "lines",
Description: []string{
"Splits the input string (first arg) into a slice by '\n'",
},
Func: SplitLines,
},
{
Name: "index_of",
Description: []string{
"Returns the index of first argument in the second argument which is a slice.",
"Example: {{ index_of \"foo\" (from_json \"[\"bar\",\"foo\",\"baz\"]\") }} returns 1 (int).",
},
Func: IndexOf,
},
}
}