Repository: honeydipper/honeydipper
Pull request #514 (2023-02-21 17:36:35)

feat(core): allow return typed data from go template
#### Description

It is useful to return typed or structured data from templates. We can avoid parsing yaml in the string to get typed data, such as boolean, int, map or list.

#### This PR fixes the following issues
N/A


Patch:
diff --git a/internal/config/repo.go b/internal/config/repo.go
index 626fd02..dc24cf9 100644
--- a/internal/config/repo.go
+++ b/internal/config/repo.go
@@ -7,6 +7,7 @@
 package config
 
 import (
+	"bytes"
 	"fmt"
 	"os"
 	"path"
@@ -75,7 +76,12 @@ func (c *Repo) loadFile(filename string) {
 
 	var content DataSet
 	yamlFile := dipper.Must(os.ReadFile(path.Join(c.root, filename[1:]))).([]byte)
-	yamlFile = []byte(dipper.InterpolateGoTemplate(true, "filename", string(yamlFile), map[string]interface{}{"env": dipper.Getenv()}))
+	switch ret := dipper.InterpolateGoTemplate(true, "filename", string(yamlFile), map[string]interface{}{"env": dipper.Getenv()}).(type) {
+	case *bytes.Buffer:
+		yamlFile = ret.Bytes()
+	case string:
+		yamlFile = []byte(ret)
+	}
 	dipper.Must(yaml.Unmarshal(yamlFile, &content))
 
 	if content.Repos != nil {
diff --git a/pkg/dipper/interpolation.go b/pkg/dipper/interpolation.go
index 98beca1..07c6dc9 100644
--- a/pkg/dipper/interpolation.go
+++ b/pkg/dipper/interpolation.go
@@ -48,7 +48,7 @@ func InterpolateStr(pattern string, data interface{}) string {
 }
 
 // InterpolateGoTemplate : parse the string as go template.
-func InterpolateGoTemplate(isLoading bool, title string, pattern string, data interface{}) string {
+func InterpolateGoTemplate(isLoading bool, title string, pattern string, data interface{}) interface{} {
 	ldelim := "{{"
 	rdelim := "}}"
 	if isLoading {
@@ -56,14 +56,35 @@ func InterpolateGoTemplate(isLoading bool, title string, pattern string, data in
 		rdelim = "%}"
 	}
 	if strings.Contains(pattern, ldelim) {
-		tmpl := template.Must(template.New(title).Funcs(FuncMap).Funcs(sprig.TxtFuncMap()).Delims(ldelim, rdelim).Parse(pattern))
+		var ret interface{}
+		useRet := false
+		returnFuncMap := template.FuncMap{
+			"return": func(v interface{}) string {
+				useRet = true
+				ret = v
+
+				return ""
+			},
+		}
+
+		tmpl := template.New(title)
+		tmpl = tmpl.Funcs(FuncMap)
+		tmpl = tmpl.Funcs(sprig.TxtFuncMap())
+		tmpl = tmpl.Funcs(returnFuncMap)
+		tmpl = tmpl.Delims(ldelim, rdelim)
+		parsed := template.Must(tmpl.Parse(pattern))
+
 		buf := new(bytes.Buffer)
-		if err := tmpl.Execute(buf, data); err != nil {
+		if err := parsed.Execute(buf, data); err != nil {
 			Logger.Warningf("interpolation pattern failed: %+v", pattern)
 			Logger.Panicf("failed to interpolate: %+v", err)
 		}
 
-		return buf.String()
+		if useRet {
+			return ret
+		}
+
+		return buf
 	}
 
 	return pattern
@@ -138,7 +159,16 @@ func Interpolate(source interface{}, data interface{}) interface{} {
 			return InterpolateDollarStr(v, data)
 		}
 
-		ret := InterpolateGoTemplate(false, "go", v, data)
+		var ret string
+
+		switch retAnything := InterpolateGoTemplate(false, "go", v, data).(type) {
+		case *bytes.Buffer:
+			ret = retAnything.String()
+		case string:
+			ret = retAnything
+		default:
+			return retAnything
+		}
 
 		if strings.HasPrefix(ret, ":yaml:") {
 			defer func() {
@@ -150,9 +180,8 @@ func Interpolate(source interface{}, data interface{}) interface{} {
 
 			return ParseYaml(ret[6:])
 		}
-		ret = strings.TrimPrefix(ret, "\\")
 
-		return ret
+		return strings.TrimPrefix(ret, "\\")
 	case map[string]interface{}:
 		ret := map[string]interface{}{}
 		for k, val := range v {


Test patch:
diff --git a/pkg/dipper/interpolation_test.go b/pkg/dipper/interpolation_test.go
index c5c1779..5048a76 100644
--- a/pkg/dipper/interpolation_test.go
+++ b/pkg/dipper/interpolation_test.go
@@ -10,6 +10,7 @@
 package dipper
 
 import (
+	"bytes"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -76,6 +77,8 @@ test:
 func TestInterpolateGoTemplate(t *testing.T) {
 	assert.Equal(t, "{% not interpolated %}", InterpolateGoTemplate(false, "go", "{% not interpolated %}", map[string]interface{}{}), "should not interpolate {%%} in non-loading time")
 	assert.Equal(t, "{{ not interpolated }}", InterpolateGoTemplate(true, "test.yml", "{{ not interpolated }}", map[string]interface{}{}), "should not interpolate {{}} in loading time")
-	assert.Equal(t, "test", InterpolateGoTemplate(false, "go", "{{ .env.TEST_ENV }}", map[string]interface{}{"env": map[string]interface{}{"TEST_ENV": "test"}}), "should interpolate {{}} in non-loading time")
-	assert.Equal(t, "test", InterpolateGoTemplate(true, "test.yml", "{% .env.TEST_ENV %}", map[string]interface{}{"env": map[string]interface{}{"TEST_ENV": "test"}}), "should interpolate {%%} in loading time")
+	assert.Equal(t, "test", InterpolateGoTemplate(false, "go", "{{ .env.TEST_ENV }}", map[string]interface{}{"env": map[string]interface{}{"TEST_ENV": "test"}}).(*bytes.Buffer).String(), "should interpolate {{}} in non-loading time")
+	assert.Equal(t, "test", InterpolateGoTemplate(true, "test.yml", "{% .env.TEST_ENV %}", map[string]interface{}{"env": map[string]interface{}{"TEST_ENV": "test"}}).(*bytes.Buffer).String(), "should interpolate {%%} in loading time")
+	assert.Equal(t, true, InterpolateGoTemplate(false, "go", "{{ return true }}", map[string]interface{}{}), "should return a boolean type")
+	assert.Equal(t, map[string]interface{}{"foo": "bar"}, InterpolateGoTemplate(false, "go", "{{ dict \"foo\" \"bar\" | return }}", map[string]interface{}{}), "should return a map type")
 }