-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathfuncs.go
550 lines (514 loc) · 15.1 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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
package template // import "github.com/orkestr8/template"
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strings"
"time"
"github.com/ghodss/yaml"
"github.com/hashicorp/hcl"
"github.com/jmespath/go-jmespath"
"github.com/vaughan0/go-ini"
)
// Unescape replaces all the \{\{ and \}\} back to the normal unescaped {{ and }}.
func Unescape(source []byte) []byte {
if source == nil {
return source
}
// Apply the template but we need escape the \{\{ if any
buff := source
buff = bytes.Replace(buff, []byte("\\{\\{"), []byte("{{"), -1)
buff = bytes.Replace(buff, []byte("\\}\\}"), []byte("}}"), -1)
// YAML will escape the escapes... so twice
buff = bytes.Replace(buff, []byte("\\\\{\\\\{"), []byte("{{"), -1)
buff = bytes.Replace(buff, []byte("\\\\}\\\\}"), []byte("}}"), -1)
return buff
}
// EscapeString returns an escaped string
func EscapeString(source string) string {
return string(Escape([]byte(source)))
}
// Escape replaces all the {{ and }} with \{\{ and \}\} to escape template content.
func Escape(source []byte) []byte {
if source == nil {
return source
}
buff := source
buff = bytes.Replace(buff, []byte("{{"), []byte("\\{\\{"), -1)
buff = bytes.Replace(buff, []byte("}}"), []byte("\\}\\}"), -1)
return buff
}
// 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 Go value.
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 *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
}
// FromYAML decode the input YAML encoded as string or byte slice into a Go value.
func FromYAML(o interface{}) (interface{}, error) {
var ret interface{}
switch o := o.(type) {
case string:
err := yaml.Unmarshal([]byte(o), &ret)
return ret, err
case []byte:
err := yaml.Unmarshal(o, &ret)
return ret, err
case *Any:
err := yaml.Unmarshal(o.Bytes(), &ret)
return ret, err
}
return ret, fmt.Errorf("not-supported-value-type")
}
// ToYAML encodes the input struct into a YAML string.
func ToYAML(o interface{}) (string, error) {
buff, err := yaml.Marshal(o)
return string(buff), err
}
// FromINI decodes content formatted in INI format at path
func FromINI(v string) (map[string]interface{}, error) {
buff := bytes.NewBufferString(v)
file, err := ini.Load(buff)
if err != nil {
return nil, err
}
out := map[string]interface{}{}
for n, section := range file {
m := map[string]interface{}{}
for k, v := range section {
m[k] = v
}
out[n] = m
}
return out, nil
}
// FromHCL decode the input HCL encoded as string or byte slice into a Go value
func FromHCL(o interface{}) (interface{}, error) {
var ret interface{}
switch o := o.(type) {
case string:
err := hcl.Unmarshal([]byte(o), &ret)
return ret, err
case []byte:
err := hcl.Unmarshal(o, &ret)
return ret, err
case *Any:
err := hcl.Unmarshal(o.Bytes(), &ret)
return ret, err
}
return ret, fmt.Errorf("not-supported-value-type")
}
// 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
}
// given optional args in a template function call, extra headers and the context
func headersAndContext(opt ...interface{}) (headers map[string][]string, context interface{}) {
if len(opt) == 0 {
return
}
// scan through all the args and if it's a string of the form x=y, then use as header
// the element that doesn't follow the form is the context
headers = map[string][]string{}
for _, v := range opt {
if vv, is := v.(string); is && strings.Index(vv, "=") > 0 {
kv := strings.Split(vv, "=")
key := kv[0]
value := ""
if len(kv) == 2 {
value = kv[1]
}
if _, has := headers[key]; !has {
headers[key] = []string{value}
} else {
headers[key] = append(headers[key], value)
}
} else {
context = v
}
}
return
}
func setHeaders(req *http.Request, headers map[string][]string) {
for k, vv := range headers {
for _, v := range vv {
req.Header.Add(k, v)
}
}
}
// Fetch opens and reads a url and returns its content
func (t *Template) Fetch(p string, opt ...interface{}) (string, error) {
headers, _ := headersAndContext(opt...)
loc := p
if strings.Index(loc, "str://") == -1 {
u, err := GetURL(t.url, p)
if err != nil {
return "", err
}
loc = u.String()
}
prev := t.options.CustomizeFetch
t.options.CustomizeFetch = func(req *http.Request) {
setHeaders(req, headers)
if prev != nil {
prev(req)
}
}
buff, err := checkCache(loc, t.options, func() ([]byte, error) {
return Fetch(loc, t.options)
})
return string(buff), err
}
// Source 'sources' the input file at url, also inherits all the variables.
func (t *Template) Source(p string, opt ...interface{}) (string, error) {
_, context, sourced, err := t.raw(p, opt...)
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 context == nil {
context = sourced.context
}
// TODO(chungers) -- let the sourced template define new functions that can be called in the parent.
return sourced.Render(context)
}
// Include includes the template at the url inline.
func (t *Template) Include(p string, opt ...interface{}) (string, error) {
_, context, included, err := t.raw(p, opt...)
if err != nil {
return "", err
}
dotCopy, err := included.forkFrom(t)
if err != nil {
return "", err
}
included.context = dotCopy
if context == nil {
context = included.context
}
return included.Render(context)
}
// Raw includes the raw bytes fetched from the url inline.
func (t *Template) Raw(p string, opt ...interface{}) ([]byte, error) {
_, _, included, err := t.raw(p, opt...)
if err != nil {
return []byte{}, err
}
return included.body, nil
}
func (t *Template) raw(p string, opt ...interface{}) (map[string][]string, interface{}, *Template, error) {
headers, context := headersAndContext(opt...)
loc := p
if strings.Index(loc, "str://") == -1 {
u, err := GetURL(t.url, p)
if err != nil {
return nil, nil, nil, err
}
loc = u.String()
}
prev := t.options.CustomizeFetch
t.options.CustomizeFetch = func(req *http.Request) {
setHeaders(req, headers)
if prev != nil {
prev(req)
}
}
tt, err := NewTemplate(loc, t.options)
return headers, context, tt, err
}
// DefaultFuncs returns a list of default functions for binding in the template
func (t *Template) DefaultFuncs() []Function {
return []Function{
{
Name: "fetch",
Description: []string{
"Fetches a resource without evaluation as template. The results are cached locally by the url fetched from.",
},
Func: t.Fetch,
},
{
Name: "raw",
Description: []string{
"Get a resource and returns the raw bytes. The results are not cached.",
},
Func: t.Fetch,
},
{
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: t.Source,
},
{
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: t.Include,
},
{
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: "var",
Description: []string{
"References or sets a variable. If single argument, returns the value.",
"If second argument is provided, sets the variable. The scope is global.",
},
Func: t.Var,
},
{
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\" | jsonDecode | q \"origin\" }}",
"returns the origin of http request.",
},
Func: QueryObject,
},
{
Name: "yamlEncode",
Description: []string{
"Encodes the input as a YAML string",
"This is useful for taking an object (interface{}) and render it inline as proper YAML.",
"Example: {{ include \"https://httpbin.org/get\" | jsonDecode | yamlEncode }}",
},
Func: ToYAML,
},
{
Name: "yamlDecode",
Description: []string{
"Decodes the input YAML (first arg) into a structure (a map[string]interface{} or []interface{}).",
"This is useful for parsing arbitrary resources in YAML format as object. The object is the queryable via 'q'",
},
Func: FromYAML,
},
{
Name: "jsonEncode",
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\" | jsonDecode | jsonEncode }}",
},
Func: ToJSON,
},
{
Name: "jsonEncodeIndent",
Description: []string{
"Encodes the input as a JSON string with first arg as prefix, second arg the indentation, then the object",
},
Func: ToJSONFormat,
},
{
Name: "jsonDecode",
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\" | jsonDecode | 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,
},
{
Name: "indexOf",
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,
},
{
Name: "iniDecode",
Description: []string{
"Decodes the input INI into a structure (a map[string]interface{}).",
"This is useful for parsing arbitrary resources in INI format as object. The object is the queryable via 'q'",
},
Func: FromINI,
},
{
Name: "hclDecode",
Description: []string{
"Decodes the input HCL (Hashicorp Terraform) into a Go value.",
"This is useful for working with HCL formatted output.",
},
Func: FromHCL,
},
{
Name: "escape",
Description: []string{
"Escapes the content as template so it can be inlined another template",
"and not be evaluated all at the same time.",
},
Func: Escape,
},
{
Name: "unescape",
Description: []string{
"Unescapes the content back into a template so that the content",
"can be evaluated.",
},
Func: Escape,
},
{
Name: "echo",
Description: []string{
"Print the args to stderr. This does not affect the evaluation of the template and result is not in the template.",
},
Func: // echo out to stderr
func(args ...interface{}) string {
var out io.Writer
if t.options.Stderr != nil {
out = t.options.Stderr()
}
if out != nil {
fmt.Fprintln(out, args...)
}
return ""
},
},
}
}