Skip to content

Commit dd0a5b1

Browse files
committed
New Helpers:
- `includeAsString`: Similar to `include`, allows to register a template, but reads the template contents from a string instead of a file. E.g: `{{ includeAsString “templateName” .var_with_contents }}` - `invoke`: Similar to `{{ template [name] [data] }}, invokes a template by a given name with a data object. The difference with `template` is that 'invoke` can be used with a string variable as the name instead of a hardcoded string. - `parse`: Attempts to parse the provided template contents using the provided data object and returns the parsed value. Usage `{{ parse [obj] [template contents] }}`. - `parseXpath`: Attempts to parse a value inside a data object as a template and returns the parsed value using the same entry object to parse the template. Usage `{{ parse [obj] [xpath to value with template] }}` - `in`: Indicates whether a value is contained in an array. Usage: `{{ in [array] [value] }}` Minor Fixes: - The helper `yaml` now properly allows an optional argument to indicate the indentation of the resulting YAML. Usage `{{ yaml [obj] [int] }}` where the `int` number indicates the amount of spaces for indentation. - Map functions now allow XPATH to be provided with or without a `.` as a prefix
1 parent b233f42 commit dd0a5b1

File tree

4 files changed

+95
-4
lines changed

4 files changed

+95
-4
lines changed

templates/gotmpl/helpers.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package gotmpl
22

33
import (
4+
"bytes"
45
"encoding/json"
6+
"errors"
57
"fmt"
68
"io/ioutil"
79
"reflect"
@@ -11,6 +13,7 @@ import (
1113
"github.com/jucardi/infuse/templates/helpers"
1214
"github.com/jucardi/infuse/util/log"
1315
"github.com/jucardi/infuse/util/maps"
16+
"github.com/jucardi/infuse/util/reflectx"
1417
)
1518

1619
var instance *helperContext
@@ -28,6 +31,7 @@ func getHelpers() *helperContext {
2831
type helperContext struct {
2932
helpers.IHelpersManager
3033
*template.Template
34+
loaded map[string]*template.Template
3135
}
3236

3337
func (h *helperContext) setTemplate(tmpl *template.Template) {
@@ -48,6 +52,7 @@ func (h *helperContext) init() {
4852
_ = h.Register("map", h.mapFn, "Creates a new map[string]interface{}, the provided arguments should be key, value, key, value...")
4953
_ = h.Register("dict", h.mapFn, "Creates a new map[string]interface{}, the provided arguments should be key, value, key, value...")
5054
_ = h.Register("include", h.includeFile, "Includes a template file as an internal template reference by the provided name")
55+
_ = h.Register("includeAsString", h.includeTemplate, "Includes a provided template string as an internal template reference by the provided name. Eg: {{ include [name] [contents] }}")
5156
_ = h.Register("set", h.setFn, "Allows to set a value to a map[string]interface{} or map[interface{}]interface{}")
5257
_ = h.Register("append", h.append, "Appends a value into an existing array")
5358
_ = h.Register("iterate", h.iterate, "Creates an iteration array of the provided length, so it can be used as {{ range $val := iterate N }} where N is the length of the iteration. Created due to the lack of `for` loops.")
@@ -56,6 +61,10 @@ func (h *helperContext) init() {
5661
_ = h.Register("mapGet", h.mapGetFn, `Allows to get a value from a map using an XPATH representation of the key. Accepts optional argument for a default value to return if the value is not found". E.g: {{mapGet $map ".some.key.path" $someDefaultValue }}`)
5762
_ = h.Register("mapContains", h.mapContainsFn, `Indicates whether a value at the provided XPATH representation of the key exists in the provided map`)
5863
_ = h.Register("mapConvert", h.mapConvertFn, `Ensures the provided map is map[string]interface{}. Useful when loading values from a YAML where the deserialization is map[interface{}]interface{}`)
64+
_ = h.Register("invoke", h.invoke, `Similar to {{ template [name] [data] }}, invokes a name by the given name with the given data. The difference with 'template' is that 'invoke' can be used with a string value as the name instead of a hardcoded string`)
65+
_ = h.Register("parse", h.parse, `Attempts to parse the provided template contents using the provided data object and returns the parsed value. Usage {{ parse [obj] [template contents] }}`)
66+
_ = h.Register("parseXpath", h.parseXpath, `Attempts to parse a value inside a data object as a template and returns the parsed value using the same entry object to parse the template. Usage {{ parse [obj] [xpath to value with template] }}`)
67+
_ = h.Register("in", h.in, `Indicates whether a value is contained in an array. Usage: {{ in [array] [value] }}`)
5968
}
6069

6170
func (h *helperContext) mapSetFn(obj interface{}, key string, value interface{}, makeEmpty ...bool) string {
@@ -188,6 +197,15 @@ func (h *helperContext) includeFile(name, file string) (string, error) {
188197
return "", nil
189198
}
190199

200+
func (h *helperContext) includeTemplate(name, contents string) (string, error) {
201+
tmpl, err := h.Template.New(name).Parse(contents)
202+
if err != nil {
203+
return "", fmt.Errorf("error parsing template by name %s, %s", name, err.Error())
204+
}
205+
h.Template = tmpl
206+
return "", nil
207+
}
208+
191209
func (h *helperContext) setFn(obj interface{}, key string, value interface{}) string {
192210
switch m := obj.(type) {
193211
case map[string]interface{}:
@@ -223,3 +241,56 @@ func (h *helperContext) loadJson(str string) map[string]interface{} {
223241
}
224242
return ret
225243
}
244+
245+
func (h *helperContext) invoke(name string, data interface{}) (string, error) {
246+
tmpl := h.Template.Lookup(name)
247+
if tmpl == nil {
248+
return "", fmt.Errorf("failed to invoke template '%s', not found", name)
249+
}
250+
buf := &bytes.Buffer{}
251+
err := tmpl.Execute(buf, data)
252+
return buf.String(), err
253+
}
254+
255+
func (h *helperContext) parse(data interface{}, templateData string, failOnEmptyResult ...bool) (string, error) {
256+
if templateData == "" {
257+
if len(failOnEmptyResult) > 0 && failOnEmptyResult[0] {
258+
return "", errors.New("template produced empty result")
259+
}
260+
return "", nil
261+
}
262+
name := "__internal/parse/" + templateData
263+
tmpl := h.Template.Lookup(name)
264+
if tmpl == nil {
265+
if _, err := h.includeTemplate(name, templateData); err != nil {
266+
return "", err
267+
}
268+
tmpl = h.Template.Lookup(name)
269+
}
270+
buf := &bytes.Buffer{}
271+
err := tmpl.Execute(buf, data)
272+
ret := buf.String()
273+
if len(failOnEmptyResult) > 0 && failOnEmptyResult[0] && ret == "" && err == nil {
274+
return "", errors.New("template produced empty result")
275+
}
276+
return ret, err
277+
}
278+
279+
func (h *helperContext) parseXpath(data interface{}, xpath string, failOnEmptyResult ...bool) (string, error) {
280+
templateData, ok := h.mapGetFn(data, xpath).(string)
281+
if !ok {
282+
return "", fmt.Errorf("failed to obtain template data, the provided object does not contain a string at the provided key '%s'", xpath)
283+
}
284+
return h.parse(data, templateData, failOnEmptyResult...)
285+
}
286+
287+
func (h *helperContext) in(array interface{}, value interface{}) bool {
288+
arrVal := reflect.ValueOf(array)
289+
if kind := arrVal.Kind(); kind != reflect.Slice && kind != reflect.Array {
290+
panic("attempting to use 'in' with a non-array type")
291+
}
292+
if reflectx.IsNil(arrVal) || arrVal.Len() == 0 {
293+
return false
294+
}
295+
return streams.From(array).Contains(value)
296+
}

templates/gotmpl/template.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package gotmpl
22

33
import (
44
"fmt"
5+
"io"
6+
"text/template"
7+
58
"github.com/jucardi/go-strings/stringx"
69
"github.com/jucardi/infuse/templates"
710
"github.com/jucardi/infuse/templates/base"
811
"github.com/jucardi/infuse/templates/helpers"
9-
"io"
10-
"text/template"
1112
)
1213

1314
// TypeGo is the type for Go templates.

templates/helpers/common.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,24 @@ func bracketsFn(arg interface{}) string {
8686
return fmt.Sprintf("{{%+v}}", arg)
8787
}
8888

89-
func toYMLString(arg interface{}) string {
89+
func toYMLString(arg interface{}, indent ...int) string {
9090
result, err := yaml.Marshal(arg)
9191
if err != nil {
9292
log.Panicf("error occurred while converting object to YAML. %+v", err)
9393
}
94-
return string(result)
94+
ret := string(result)
95+
if len(indent) > 0 {
96+
ind := ""
97+
for i := 0; i < indent[0]; i++ {
98+
ind = ind + " "
99+
}
100+
split := strings.Split(ret, "\n")
101+
for i := 0; i < len(split); i++ {
102+
split[i] = ind + split[i]
103+
}
104+
ret = strings.Join(split, "\n")
105+
}
106+
return ret
95107
}
96108

97109
func toJSONString(arg interface{}) string {

util/maps/maps.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ func GetValue(data map[string]interface{}, key string) (interface{}, error) {
3636
split := strings.Split(key, ".")
3737
v := reflect.ValueOf(data)
3838
for i, s := range split {
39+
if s == "" {
40+
continue
41+
}
42+
3943
isArray := regex.MatchString(s)
4044
index := 0
4145

@@ -111,6 +115,9 @@ func SetValue(data map[string]interface{}, key string, value interface{}, makeEm
111115
split := strings.Split(key, ".")
112116
v := reflect.ValueOf(data)
113117
for i, s := range split {
118+
if s == "" {
119+
continue
120+
}
114121
if i == len(split)-1 {
115122
v.SetMapIndex(reflect.ValueOf(s), reflect.ValueOf(value))
116123
} else {

0 commit comments

Comments
 (0)