Skip to content

Commit

Permalink
do not use json. use reflect to convert structs to map
Browse files Browse the repository at this point in the history
  • Loading branch information
adityathebe committed Jul 21, 2023
1 parent 097c035 commit e587d0e
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 44 deletions.
78 changes: 66 additions & 12 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"os"
"reflect"
"strings"
gotemplate "text/template"

Expand Down Expand Up @@ -108,18 +109,7 @@ func RunTemplate(environment map[string]any, template Template) (string, error)
return "", err
}

// Convert environment to json otherwise structs will not be casted to cel-go's ref.Val
envJSONRaw, err := json.Marshal(environment)
if err != nil {
return "", fmt.Errorf("failed to marshal environment: %v", err)
}

var envJSON map[string]any
if err := json.Unmarshal(envJSONRaw, &envJSON); err != nil {
return "", fmt.Errorf("failed to unmarshal environment: %v", err)
}

out, _, err := prg.Eval(envJSON)
out, _, err := prg.Eval(structToMap(environment))
if err != nil {
return "", err
}
Expand All @@ -142,3 +132,67 @@ func LoadSharedLibrary(source string) error {
registry.Register(func() string { return string(data) })
return nil
}

// serialize iterates over each key-value pair in the input map
// serializes any struct value to map[string]any.
func serialize(in map[string]any) map[string]any {
if in == nil {
return nil
}

newMap := make(map[string]any, len(in))
for k, v := range in {
if reflect.ValueOf(v).Kind() == reflect.Struct {
newMap[k] = structToMap(v)
} else {
newMap[k] = v
}
}

return newMap
}

func structToMap(i any) map[string]any {
data := make(map[string]any)
value := reflect.ValueOf(i)
typeOf := value.Type()

if value.Kind() == reflect.Ptr {
value = value.Elem()
typeOf = value.Type()
}

for i := 0; i < value.NumField(); i++ {
field := value.Field(i)
switch field.Kind() {
case reflect.Struct:
data[typeOf.Field(i).Name] = structToMap(field.Interface())
case reflect.Slice:
sliceData := make([]any, field.Len())
for j := 0; j < field.Len(); j++ {
sliceValue := field.Index(j)
if sliceValue.Kind() == reflect.Struct {
sliceData[j] = structToMap(sliceValue.Interface())
} else {
sliceData[j] = sliceValue.Interface()
}
}
data[typeOf.Field(i).Name] = sliceData
case reflect.Map:
mapData := make(map[string]any)
for _, key := range field.MapKeys() {
mapValue := field.MapIndex(key)
if mapValue.Kind() == reflect.Struct {
mapData[key.Interface().(string)] = structToMap(mapValue.Interface())
} else {
mapData[key.Interface().(string)] = mapValue.Interface()
}
}
data[typeOf.Field(i).Name] = mapData
default:
data[typeOf.Field(i).Name] = field.Interface()
}
}

return data
}
97 changes: 65 additions & 32 deletions template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@ import (
"testing"
"time"

_ "github.com/flanksource/gomplate/v3/js"
"github.com/flanksource/gomplate/v3/k8s"
_ "github.com/robertkrimen/otto/underscore"
"github.com/stretchr/testify/assert"
)

func TestJavascript(t *testing.T) {
tests := []struct {
env map[string]interface{}
env map[string]any
js string
out string
}{
{map[string]interface{}{"x": 5, "y": 3}, "x + y", "8"},
{map[string]interface{}{"str": "Hello, World!"}, "str", "Hello, World!"},
{map[string]interface{}{"numbers": []int{1, 2, 3, 4, 5}}, "_.reduce(numbers, function(memo, num){ return memo + num; }, 0)", "15"},
{map[string]interface{}{"numbers": []int{4, 2, 55}}, `_.max(numbers)`, "55"},
{map[string]interface{}{"arr": []int{1, 2, 1, 4, 1, 2}}, "_.uniq(arr)", "1,2,4"},
{map[string]interface{}{"numbers": []int{4, 2, 55}}, `_.max(numbers)`, "55"},
{map[string]interface{}{"arr": []int{1, 2, 1, 4, 1, 2}}, "_.uniq(arr)", "1,2,4"},
{map[string]interface{}{"x": "1Ki"}, "fromSI(x)", "1024"},
{map[string]interface{}{"x": "2m"}, "fromMillicores(x)", "2"},
{map[string]interface{}{"name": "mission.compute.internal"}, "k8s.getNodeName(name)", "mission"},
{map[string]interface{}{"msg": map[string]any{"healthStatus": map[string]string{"status": "HEALTHY"}}}, "k8s.conditions.isReady(msg)", "true"},
{map[string]any{"x": 5, "y": 3}, "x + y", "8"},
{map[string]any{"str": "Hello, World!"}, "str", "Hello, World!"},
{map[string]any{"numbers": []int{1, 2, 3, 4, 5}}, "_.reduce(numbers, function(memo, num){ return memo + num; }, 0)", "15"},
{map[string]any{"numbers": []int{4, 2, 55}}, `_.max(numbers)`, "55"},
{map[string]any{"arr": []int{1, 2, 1, 4, 1, 2}}, "_.uniq(arr)", "1,2,4"},
{map[string]any{"numbers": []int{4, 2, 55}}, `_.max(numbers)`, "55"},
{map[string]any{"arr": []int{1, 2, 1, 4, 1, 2}}, "_.uniq(arr)", "1,2,4"},
{map[string]any{"x": "1Ki"}, "fromSI(x)", "1024"},
{map[string]any{"x": "2m"}, "fromMillicores(x)", "2"},
{map[string]any{"name": "mission.compute.internal"}, "k8s.getNodeName(name)", "mission"},
{map[string]any{"msg": map[string]any{"healthStatus": map[string]string{"status": "HEALTHY"}}}, "k8s.conditions.isReady(msg)", "true"},
}

for _, tc := range tests {
Expand All @@ -40,19 +42,19 @@ func TestJavascript(t *testing.T) {

func TestGomplate(t *testing.T) {
tests := []struct {
env map[string]interface{}
env map[string]any
template string
out string
}{
{map[string]interface{}{"hello": "world"}, "{{ .hello }}", "world"},
{map[string]interface{}{"age": 75 * time.Second}, "{{ .age | humanDuration }}", "1m15s"},
{map[string]interface{}{"healthySvc": k8s.GetUnstructured(k8s.TestHealthy)}, "{{ (.healthySvc | isHealthy) }}", "true"},
{map[string]interface{}{"healthySvc": k8s.GetUnstructured(k8s.TestLuaStatus)}, "{{ (.healthySvc | getStatus) }}", "Degraded: found less than two generators, Merge requires two or more"},
{map[string]interface{}{"healthySvc": k8s.GetUnstructured(k8s.TestHealthy)}, "{{ (.healthySvc | getHealth).Status }}", "Healthy"},
{map[string]interface{}{"size": 123456}, "{{ .size | humanSize }}", "120.6K"},
{map[string]interface{}{"v": "1.2.3-beta.1+c0ff33"}, "{{ (.v | semver).Prerelease }}", "beta.1"},
{map[string]interface{}{"old": "1.2.3", "new": "1.2.3"}, "{{ .old | semverCompare .new }}", "true"},
{map[string]interface{}{"old": "1.2.3", "new": "1.2.4"}, "{{ .old | semverCompare .new }}", "false"},
{map[string]any{"hello": "world"}, "{{ .hello }}", "world"},
{map[string]any{"age": 75 * time.Second}, "{{ .age | humanDuration }}", "1m15s"},
{map[string]any{"healthySvc": k8s.GetUnstructured(k8s.TestHealthy)}, "{{ (.healthySvc | isHealthy) }}", "true"},
{map[string]any{"healthySvc": k8s.GetUnstructured(k8s.TestLuaStatus)}, "{{ (.healthySvc | getStatus) }}", "Degraded: found less than two generators, Merge requires two or more"},
{map[string]any{"healthySvc": k8s.GetUnstructured(k8s.TestHealthy)}, "{{ (.healthySvc | getHealth).Status }}", "Healthy"},
{map[string]any{"size": 123456}, "{{ .size | humanSize }}", "120.6K"},
{map[string]any{"v": "1.2.3-beta.1+c0ff33"}, "{{ (.v | semver).Prerelease }}", "beta.1"},
{map[string]any{"old": "1.2.3", "new": "1.2.3"}, "{{ .old | semverCompare .new }}", "true"},
{map[string]any{"old": "1.2.3", "new": "1.2.4"}, "{{ .old | semverCompare .new }}", "false"},
}

for _, tc := range tests {
Expand All @@ -67,15 +69,6 @@ func TestGomplate(t *testing.T) {
}

func TestCel(t *testing.T) {
type Address struct {
City string `json:"city"`
}

type Person struct {
Name string `json:"name"`
Address Address `json:"address"`
}

tests := []struct {
env map[string]interface{}
expression string
Expand Down Expand Up @@ -121,7 +114,7 @@ func TestCel(t *testing.T) {
},
},
},
`results.address.city == "Kathmandu" && results.name == "Aditya"`,
`results.Address.City == "Kathmandu" && results.Name == "Aditya"`,
"true",
},
}
Expand All @@ -136,3 +129,43 @@ func TestCel(t *testing.T) {
})
}
}

type Address struct {
City string
}

type Person struct {
Name string
Address Address
MetaData map[string]any
Codes []string
}

func Test_structToMap(t *testing.T) {
sample := Person{
Name: "Aditya",
MetaData: map[string]any{
"foo": "bar",
},
Codes: []string{"1", "2"},
Address: Address{
City: "Kathmandu",
},
}

res := structToMap(sample)
assert.IsType(t, "", res["Name"])
assert.Equal(t, "Aditya", res["Name"])

assert.IsType(t, map[string]any{}, res["Address"])
if val, ok := res["Address"].(map[string]any); ok {
assert.IsType(t, "", val["City"])
assert.Equal(t, "Kathmandu", val["City"])
}

assert.IsType(t, map[string]any{}, res["MetaData"])
assert.Equal(t, map[string]any{"foo": "bar"}, res["MetaData"])

assert.IsType(t, []any{}, res["Codes"])
assert.Equal(t, []any{"1", "2"}, res["Codes"])
}

0 comments on commit e587d0e

Please sign in to comment.