diff --git a/cmd/gencel_test.go b/cmd/gencel_test.go index c2be71544..b67f7958c 100644 --- a/cmd/gencel_test.go +++ b/cmd/gencel_test.go @@ -9,7 +9,7 @@ import ( ) // Not really a test but just a runner so it's easier to attach a debugger. -func TestGencel(t *testing.T) { +func testGencel(t *testing.T) { wd, _ := os.Getwd() fmt.Printf("WD: %s", wd) diff --git a/go.mod b/go.mod index 526d36f66..612d1c99b 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ require ( github.com/Masterminds/goutils v1.1.1 github.com/Masterminds/semver/v3 v3.2.1 github.com/flanksource/is-healthy v0.0.0-20230705092916-3b4cf510c5fc + github.com/flanksource/mapstructure v1.6.0 github.com/google/cel-go v0.17.1 github.com/google/uuid v1.3.0 github.com/gosimple/slug v1.13.1 github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf github.com/itchyny/gojq v0.12.13 - github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 github.com/robertkrimen/otto v0.2.1 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index e87744a24..c28883dba 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/flanksource/is-healthy v0.0.0-20230705092916-3b4cf510c5fc h1:CPUNUw2pHnlF4ucBHx44vLTcCa4FlEEu6PkNo5rCvD4= github.com/flanksource/is-healthy v0.0.0-20230705092916-3b4cf510c5fc/go.mod h1:4pQhmF+TnVqJroQKY8wSnSp+T18oLson6YQ2M0qPHfQ= +github.com/flanksource/mapstructure v1.6.0 h1:+1kJ+QsO1SxjAgktfLlpZXetsVSJ0uCLhGKrA4BtwTE= +github.com/flanksource/mapstructure v1.6.0/go.mod h1:dttg5+FFE2sp4D/CrcPCVqufNDrBggDaM+08nk5S8Ps= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -44,8 +46,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/template.go b/template.go index 5b2e01980..3a900cdce 100644 --- a/template.go +++ b/template.go @@ -12,9 +12,9 @@ import ( "github.com/flanksource/gomplate/v3/funcs" _ "github.com/flanksource/gomplate/v3/js" pkgStrings "github.com/flanksource/gomplate/v3/strings" + "github.com/flanksource/mapstructure" "github.com/google/cel-go/cel" "github.com/google/cel-go/ext" - "github.com/mitchellh/mapstructure" "github.com/robertkrimen/otto" "github.com/robertkrimen/otto/registry" _ "github.com/robertkrimen/otto/underscore" @@ -114,7 +114,7 @@ func RunTemplate(environment map[string]any, template Template) (string, error) out, _, err := prg.Eval(data) if err != nil { - return "", err + return "", fmt.Errorf("error evaluating expression %s: %v", template.Expression, err) } return fmt.Sprintf("%v", out.Value()), nil @@ -145,22 +145,44 @@ func serialize(in map[string]any) (map[string]any, error) { newMap := make(map[string]any, len(in)) for k, v := range in { - if reflect.ValueOf(v).Kind() != reflect.Struct { - newMap[k] = v - continue - } + var dec *mapstructure.Decoder + var err error + + vt := reflect.TypeOf(v) + switch vt.Kind() { + case reflect.Struct: + var result map[string]any + dec, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &result, Squash: true, Deep: true}) + if err != nil { + return nil, fmt.Errorf("error creating new mapstructure decoder: %w", err) + } - var vMap map[string]any - dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &vMap, Squash: true}) - if err != nil { - return nil, fmt.Errorf("error creating new mapstructure decoder: %w", err) - } + if err := dec.Decode(v); err != nil { + return nil, fmt.Errorf("error decoding %T to map[string]any: %w", v, err) + } - if err := dec.Decode(v); err != nil { - return nil, fmt.Errorf("error decoding %T to map[string]any: %w", v, err) - } + newMap[k] = result - newMap[k] = vMap + case reflect.Slice: + var result any + if vt.Elem().Kind() == reflect.Struct { + result = make([]map[string]any, 0) + } + + dec, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &result, Squash: true, Deep: true}) + if err != nil { + return nil, fmt.Errorf("error creating new mapstructure decoder: %w", err) + } + if err := dec.Decode(v); err != nil { + return nil, fmt.Errorf("error decoding %T to map[string]any: %w", v, err) + } + + newMap[k] = result + + default: + newMap[k] = v + continue + } } return newMap, nil diff --git a/template_test.go b/template_test.go index 8d28fad11..57111930a 100644 --- a/template_test.go +++ b/template_test.go @@ -1,6 +1,7 @@ package gomplate import ( + "reflect" "testing" "time" @@ -10,22 +11,28 @@ import ( "github.com/stretchr/testify/assert" ) +type NoStructTag struct { + Name string + UPPER string +} + type Address struct { City string `json:"city_name"` } type Person struct { - Name string `json:"name"` - Address Address - MetaData map[string]any - Codes []string + Name string `json:"name"` + Address *Address `json:",omitempty"` + MetaData map[string]any `json:",omitempty"` + Codes []string `json:",omitempty"` + Addresses []Address `json:"addresses,omitempty"` } // A shared test data for all template test var structEnv = map[string]any{ "results": Person{ Name: "Aditya", - Address: Address{ + Address: &Address{ City: "Kathmandu", }, }, @@ -58,6 +65,7 @@ var junitEnv = JunitTestSuites{ Totals: Totals{ Passed: 1, }, + Suites: []JunitTestSuite{{Name: "hi", Totals: Totals{Failed: 2}}}, } type SQLDetails struct { @@ -112,7 +120,11 @@ func TestGomplate(t *testing.T) { {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"}, {structEnv, `{{.results.name}} {{.results.Address.city_name}}`, "Aditya Kathmandu"}, - {map[string]any{"results": junitEnv}, `{{.results.passed}}`, "1"}, + { + map[string]any{"results": junitEnv}, + `{{.results.passed}}{{ range $r := .results.suites}}{{$r.name}} ✅ {{$r.passed}} ❌ {{$r.failed}} in 🕑 {{$r.duration}}{{end}}`, + "1hi ✅ 0 ❌ 2 in 🕑 0", + }, { map[string]any{ "results": SQLDetails{ @@ -184,3 +196,79 @@ func TestCel(t *testing.T) { }) } } + +func Test_serialize(t *testing.T) { + tests := []struct { + name string + in map[string]any + want map[string]any + wantErr bool + }{ + {name: "nil", in: nil, want: nil, wantErr: false}, + {name: "empty", in: map[string]any{}, want: map[string]any{}, wantErr: false}, + { + name: "simple - no struct tags", + in: map[string]any{"r": NoStructTag{Name: "Kathmandu", UPPER: "u"}}, + want: map[string]any{"r": map[string]any{"Name": "Kathmandu", "UPPER": "u"}}, + wantErr: false, + }, + {name: "simple - struct tags", in: map[string]any{"r": Address{City: "Kathmandu"}}, want: map[string]any{"r": map[string]any{"city_name": "Kathmandu"}}, wantErr: false}, + { + name: "nested struct", + in: map[string]any{"r": Person{Name: "Aditya", Address: &Address{City: "Kathmandu"}}}, + want: map[string]any{"r": map[string]any{"name": "Aditya", "Address": map[string]any{"city_name": "Kathmandu"}}}, + wantErr: false, + }, + { + name: "slice of struct", + in: map[string]any{ + "r": []Address{ + {City: "Kathmandu"}, + {City: "Lalitpur"}, + }, + }, + want: map[string]any{ + "r": []map[string]any{ + {"city_name": "Kathmandu"}, + {"city_name": "Lalitpur"}, + }, + }, + wantErr: false, + }, + { + name: "nested slice of struct", + in: map[string]any{ + "r": Person{ + Name: "Aditya", + Addresses: []Address{ + {City: "Kathmandu"}, + {City: "Lalitpur"}, + }, + }, + }, + want: map[string]any{ + "r": map[string]any{ + "name": "Aditya", + "addresses": []map[string]any{ + {"city_name": "Kathmandu"}, + {"city_name": "Lalitpur"}, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := serialize(tt.in) + if (err != nil) != tt.wantErr { + t.Errorf("serialize() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("serialize() = %v, want %v", got, tt.want) + } + }) + } +}