From 5400c06426ec4e3afe58cc73bd9ee6ac0eeb28d4 Mon Sep 17 00:00:00 2001 From: incubator4 Date: Mon, 7 Nov 2022 14:55:22 +0800 Subject: [PATCH 1/3] Implement `gohcl.EvalContext` function and add simple test case. --- gohcl/eval_context.go | 94 ++++++++++++++++++++++++++++++++++++++ gohcl/eval_context_test.go | 91 ++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 gohcl/eval_context.go create mode 100644 gohcl/eval_context_test.go diff --git a/gohcl/eval_context.go b/gohcl/eval_context.go new file mode 100644 index 00000000..98150423 --- /dev/null +++ b/gohcl/eval_context.go @@ -0,0 +1,94 @@ +package gohcl + +import ( + "bytes" + "fmt" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "reflect" +) + +func EvalContext(v interface{}) *hcl.EvalContext { + return &hcl.EvalContext{ + Variables: structMapVal(v), + } +} + +func structMapVal(v interface{}) map[string]cty.Value { + rt := reflect.TypeOf(v) + rv := reflect.ValueOf(v) + + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + + var variables = make(map[string]cty.Value) + + for index := 0; index < rt.NumField(); index++ { + key := rt.Field(index) + value := rv.Field(index) + + if !value.IsZero() { + k := marshalKey(key.Name) + //k := key.Name + variables[k] = reflectVal(value) + } + + } + return variables + +} + +func reflectVal(v reflect.Value) cty.Value { + switch v.Kind() { + case reflect.Int: + return cty.NumberIntVal(v.Int()) + case reflect.String: + return cty.StringVal(v.String()) + case reflect.Struct: + return structVal(v) + case reflect.Slice: + return sliceVal(v) + default: + panic(fmt.Sprintf("target value must be pointer to int, string, slice, struct or map, not %s", v.String())) + } +} + +func sliceVal(v reflect.Value) cty.Value { + elems := []cty.Value{} + for i := 0; i < v.Len(); i++ { + elems = append(elems, reflectVal(v.Index(i))) + } + return cty.TupleVal(elems) +} + +func structVal(v reflect.Value) cty.Value { + var ctyVals = make(map[string]cty.Value) + for index := 0; index < v.Type().NumField(); index++ { + key := v.Type().Field(index) + value := v.Field(index) + ctyVals[marshalKey(key.Name)] = reflectVal(value) + } + return cty.MapVal(ctyVals) +} + +func marshalKey(input string) string { + if input == "" { + return "" + } + var output bytes.Buffer + for index, letter := range input { + if letter < 96 { + letter = letter + 32 + if index > 0 { + output.WriteString("_") + } + + } + output.WriteRune(letter) + } + return output.String() +} diff --git a/gohcl/eval_context_test.go b/gohcl/eval_context_test.go new file mode 100644 index 00000000..df480b78 --- /dev/null +++ b/gohcl/eval_context_test.go @@ -0,0 +1,91 @@ +package gohcl + +import ( + "bytes" + "fmt" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "testing" +) + +var ( + valueComparer = cmp.Comparer(cty.Value.RawEquals) +) + +func TestEvalContext(t *testing.T) { + + type ServiceConfig struct { + Type string `hcl:"type,label"` + Name string `hcl:"name,label"` + ListenAddr string `hcl:"listen_addr"` + } + type Config struct { + IOMode string `hcl:"io_mode"` + Services []ServiceConfig `hcl:"service,block"` + } + + type Context struct { + Pid string + } + + tests := []struct { + Input interface{} + Output hcl.EvalContext + }{ + { + Input: &Context{ + Pid: "fake-pid", + }, + Output: hcl.EvalContext{ + Variables: map[string]cty.Value{ + "pid": cty.StringVal("fake-pid"), + }, + }, + }, + { + Input: &Config{ + IOMode: "fake-mode", + Services: []ServiceConfig{ + { + Type: "t", + Name: "n", + ListenAddr: "addr", + }, + }, + }, + Output: hcl.EvalContext{ + Variables: map[string]cty.Value{ + "i_o_mode": cty.StringVal("fake-mode"), + "services": cty.TupleVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "type": cty.StringVal("t"), + "name": cty.StringVal("n"), + "listen_addr": cty.StringVal("addr"), + }), + }), + }, + }, + }, + } + + for index, test := range tests { + t.Run(fmt.Sprintf("test-%d", index), func(t *testing.T) { + realOutput := EvalContext(test.Input) + + gotVal := realOutput.Variables + wantVal := test.Output.Variables + + if !cmp.Equal(gotVal, wantVal, valueComparer) { + diff := cmp.Diff(gotVal, wantVal, cmp.Comparer(func(a, b []byte) bool { + return bytes.Equal(a, b) + })) + t.Errorf( + "wrong result\nvalue: %#v\ngot: %#v\nwant: %#v\ndiff: %s", + test.Input, gotVal, wantVal, diff, + ) + } + + }) + } +} From cc9da1c29e60b673a41788ad0ea491e4f815a604 Mon Sep 17 00:00:00 2001 From: incubator4 Date: Wed, 9 Nov 2022 01:52:45 +0800 Subject: [PATCH 2/3] Annotate functions and replace cty.ListVal with cty.TupleVal for Slice var. --- gohcl/eval_context.go | 30 +++++++++++++++++++++++++++--- gohcl/eval_context_test.go | 2 +- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/gohcl/eval_context.go b/gohcl/eval_context.go index 98150423..49a4de41 100644 --- a/gohcl/eval_context.go +++ b/gohcl/eval_context.go @@ -8,12 +8,22 @@ import ( "reflect" ) +// EvalContext constructs an expression evaluation context from a Go struct value, +// making the fields available as variables and the methods available as functions, +// after transforming the field and method names such that each word (starting with +// an uppercase letter) is all lowercase and separated by underscores. +// +// Cause of Functions variable are implemented by special stdlib functions, +// this function could not evaluation golang native function variable func EvalContext(v interface{}) *hcl.EvalContext { return &hcl.EvalContext{ Variables: structMapVal(v), } } +// structMapVal use reflect to traverse the struct, +// input could be a pointer,it would check the source +// struct, and return a map of cty.Value. func structMapVal(v interface{}) map[string]cty.Value { rt := reflect.TypeOf(v) rv := reflect.ValueOf(v) @@ -33,7 +43,6 @@ func structMapVal(v interface{}) map[string]cty.Value { if !value.IsZero() { k := marshalKey(key.Name) - //k := key.Name variables[k] = reflectVal(value) } @@ -42,10 +51,17 @@ func structMapVal(v interface{}) map[string]cty.Value { } +// reflectVal receive a reflect.Value and according to the kind implemented, +// return a cty.Value. The value kind that have been implemented so far are +// Int/Uint, Float, String, and nest Struct and Slice func reflectVal(v reflect.Value) cty.Value { switch v.Kind() { - case reflect.Int: + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return cty.NumberIntVal(v.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return cty.NumberUIntVal(v.Uint()) + case reflect.Float32, reflect.Float64: + return cty.NumberFloatVal(v.Float()) case reflect.String: return cty.StringVal(v.String()) case reflect.Struct: @@ -57,14 +73,21 @@ func reflectVal(v reflect.Value) cty.Value { } } +// sliceVal receive a reflect.Value which should be asserted as Slice type. +// In the for loop, each var would be called by func reflectVal to return +// a cty.Value and add into a slice.Finally return cty.ListVal func sliceVal(v reflect.Value) cty.Value { elems := []cty.Value{} for i := 0; i < v.Len(); i++ { elems = append(elems, reflectVal(v.Index(i))) } - return cty.TupleVal(elems) + return cty.ListVal(elems) } +// structVal received a reflect.Value which should be asserted as Struct type. +// It uses the NumFiled() of reflect type to loop all struct fields, +// and return cty.MapVal + func structVal(v reflect.Value) cty.Value { var ctyVals = make(map[string]cty.Value) for index := 0; index < v.Type().NumField(); index++ { @@ -75,6 +98,7 @@ func structVal(v reflect.Value) cty.Value { return cty.MapVal(ctyVals) } +// marshalKey trans camelcase to lowercase with separated by underscores func marshalKey(input string) string { if input == "" { return "" diff --git a/gohcl/eval_context_test.go b/gohcl/eval_context_test.go index df480b78..f1f3b782 100644 --- a/gohcl/eval_context_test.go +++ b/gohcl/eval_context_test.go @@ -57,7 +57,7 @@ func TestEvalContext(t *testing.T) { Output: hcl.EvalContext{ Variables: map[string]cty.Value{ "i_o_mode": cty.StringVal("fake-mode"), - "services": cty.TupleVal([]cty.Value{ + "services": cty.ListVal([]cty.Value{ cty.MapVal(map[string]cty.Value{ "type": cty.StringVal("t"), "name": cty.StringVal("n"), From e2ee2e14943880f90baf0adc63d0a71e96f619df Mon Sep 17 00:00:00 2001 From: incubator4 Date: Wed, 9 Nov 2022 02:38:52 +0800 Subject: [PATCH 3/3] Add map var case, and ignore empty object(map/slice/struct), which might cause cty return error. --- gohcl/eval_context.go | 66 +++++++++++++++++++++++++++++--------- gohcl/eval_context_test.go | 46 ++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/gohcl/eval_context.go b/gohcl/eval_context.go index 49a4de41..92e7c4fc 100644 --- a/gohcl/eval_context.go +++ b/gohcl/eval_context.go @@ -43,7 +43,10 @@ func structMapVal(v interface{}) map[string]cty.Value { if !value.IsZero() { k := marshalKey(key.Name) - variables[k] = reflectVal(value) + refVal, err := reflectVal(value) + if err == nil { + variables[k] = refVal + } } } @@ -54,48 +57,81 @@ func structMapVal(v interface{}) map[string]cty.Value { // reflectVal receive a reflect.Value and according to the kind implemented, // return a cty.Value. The value kind that have been implemented so far are // Int/Uint, Float, String, and nest Struct and Slice -func reflectVal(v reflect.Value) cty.Value { +func reflectVal(v reflect.Value) (cty.Value, error) { switch v.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return cty.NumberIntVal(v.Int()) + return cty.NumberIntVal(v.Int()), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return cty.NumberUIntVal(v.Uint()) + return cty.NumberUIntVal(v.Uint()), nil case reflect.Float32, reflect.Float64: - return cty.NumberFloatVal(v.Float()) + return cty.NumberFloatVal(v.Float()), nil case reflect.String: - return cty.StringVal(v.String()) + return cty.StringVal(v.String()), nil case reflect.Struct: return structVal(v) case reflect.Slice: - return sliceVal(v) + return listVal(v) + case reflect.Map: + return mapVal(v) default: - panic(fmt.Sprintf("target value must be pointer to int, string, slice, struct or map, not %s", v.String())) + return cty.EmptyObjectVal, fmt.Errorf("target value must be pointer to number, string, slice, struct or map, not %s", v.String()) } } -// sliceVal receive a reflect.Value which should be asserted as Slice type. +func mapVal(v reflect.Value) (cty.Value, error) { + var mapVar = make(map[string]cty.Value) + kind := v.Type().Key().Kind() + if kind == reflect.String { + iter := v.MapRange() + for iter.Next() { + refVal, err := reflectVal(iter.Value()) + if err == nil { + mapVar[marshalKey(iter.Key().String())] = refVal + } + } + if len(mapVar) > 0 { + return cty.MapVal(mapVar), nil + } + return cty.EmptyObjectVal, fmt.Errorf("target map error or is empty") + } + return cty.EmptyObjectVal, fmt.Errorf("target key should be string, %s is not support", kind.String()) +} + +// listVal receive a reflect.Value which should be asserted as Slice type. // In the for loop, each var would be called by func reflectVal to return // a cty.Value and add into a slice.Finally return cty.ListVal -func sliceVal(v reflect.Value) cty.Value { +func listVal(v reflect.Value) (cty.Value, error) { elems := []cty.Value{} for i := 0; i < v.Len(); i++ { - elems = append(elems, reflectVal(v.Index(i))) + refVal, err := reflectVal(v.Index(i)) + if err == nil { + elems = append(elems, refVal) + } + } + if len(elems) > 0 { + return cty.ListVal(elems), nil } - return cty.ListVal(elems) + return cty.EmptyTupleVal, fmt.Errorf(" slice is empty, cty.ListVal must not call ListVal with empty slice") } // structVal received a reflect.Value which should be asserted as Struct type. // It uses the NumFiled() of reflect type to loop all struct fields, // and return cty.MapVal -func structVal(v reflect.Value) cty.Value { +func structVal(v reflect.Value) (cty.Value, error) { var ctyVals = make(map[string]cty.Value) for index := 0; index < v.Type().NumField(); index++ { key := v.Type().Field(index) value := v.Field(index) - ctyVals[marshalKey(key.Name)] = reflectVal(value) + refVal, err := reflectVal(value) + if err == nil { + ctyVals[marshalKey(key.Name)] = refVal + } + } + if len(ctyVals) > 0 { + return cty.MapVal(ctyVals), nil } - return cty.MapVal(ctyVals) + return cty.EmptyObjectVal, fmt.Errorf(" slice is empty, cty.ListVal must not call ListVal with empty map") } // marshalKey trans camelcase to lowercase with separated by underscores diff --git a/gohcl/eval_context_test.go b/gohcl/eval_context_test.go index f1f3b782..a06e8625 100644 --- a/gohcl/eval_context_test.go +++ b/gohcl/eval_context_test.go @@ -67,6 +67,52 @@ func TestEvalContext(t *testing.T) { }, }, }, + { + Input: struct { + HashMap map[string]string + }{ + HashMap: map[string]string{ + "a": "b", + "c": "d", + }, + }, + Output: hcl.EvalContext{Variables: map[string]cty.Value{ + "hash_map": cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + "c": cty.StringVal("d"), + }), + }}, + }, + { + Input: struct { + HashMap map[string]string + }{ + HashMap: map[string]string{}, + }, + Output: hcl.EvalContext{Variables: map[string]cty.Value{}}, + }, + { + Input: struct { + Array []string + }{ + Array: []string{"elem-1", "elem-2"}, + }, + Output: hcl.EvalContext{Variables: map[string]cty.Value{ + "array": cty.ListVal( + []cty.Value{ + cty.StringVal("elem-1"), + cty.StringVal("elem-2"), + }), + }}, + }, + { + Input: struct { + Array []string + }{ + Array: []string{}, + }, + Output: hcl.EvalContext{Variables: map[string]cty.Value{}}, + }, } for index, test := range tests {