From b233f427721bba0c9a780f67d91a3ad901b13e3e Mon Sep 17 00:00:00 2001 From: "Juan C. Diaz" Date: Mon, 14 Nov 2022 20:11:18 -0800 Subject: [PATCH] minor fix to the 'set' helper so it allows map[interface{}]interface{}. Added support for arm64 targets. Added stringsSub helper to obtain the substring of a string. Added new map helpers to allow XPATH notation to be used to get or set values --- Makefile | 15 ++- cmd/infuse/cli/parser/loader.go | 3 +- templates/gotmpl/helpers.go | 100 ++++++++++++++++- templates/helpers/common.go | 5 + util/maps/maps.go | 188 ++++++++++++++++++++++++++++++++ util/reflectx/reflect.go | 94 ++++++++++++++++ 6 files changed, 395 insertions(+), 10 deletions(-) create mode 100644 util/maps/maps.go create mode 100644 util/reflectx/reflect.go diff --git a/Makefile b/Makefile index bbb3153..be5e37f 100644 --- a/Makefile +++ b/Makefile @@ -22,15 +22,24 @@ compile-all: deps @echo "compiling..." @rm -rf build @mkdir build - @echo "building linux binary..." + @echo "building x86_64 linux binary..." @GOOS=linux GOARCH=amd64 go build -mod=vendor -ldflags "-X $(CMDROOT)/version.Version=$(VERSION) -X $(CMDROOT)/version.Built=$(BUILD_TIME)" -o build/infuse-Linux-x86_64 ./cmd/infuse @shasum -a 256 build/infuse-Linux-x86_64 >> build/infuse-Linux-x86_64.sha256 - @echo "building macosx binary..." + @echo "building arm64 linux binary..." + @GOOS=linux GOARCH=arm64 go build -mod=vendor -ldflags "-X $(CMDROOT)/version.Version=$(VERSION) -X $(CMDROOT)/version.Built=$(BUILD_TIME)" -o build/infuse-Linux-arm64 ./cmd/infuse + @shasum -a 256 build/infuse-Linux-arm64 >> build/infuse-Linux-arm64.sha256 + @echo "building x86_64 macosx binary..." @GOOS=darwin GOARCH=amd64 go build -mod=vendor -ldflags "-X $(CMDROOT)/version.Version=$(VERSION) -X $(CMDROOT)/version.Built=$(BUILD_TIME)" -o build/infuse-Darwin-x86_64 ./cmd/infuse @shasum -a 256 build/infuse-Darwin-x86_64 >> build/infuse-Darwin-x86_64.sha256 - @echo "building windows binary..." + @echo "building arm64 macosx binary..." + @GOOS=darwin GOARCH=arm64 go build -mod=vendor -ldflags "-X $(CMDROOT)/version.Version=$(VERSION) -X $(CMDROOT)/version.Built=$(BUILD_TIME)" -o build/infuse-Darwin-arm64 ./cmd/infuse + @shasum -a 256 build/infuse-Darwin-arm64 >> build/infuse-Darwin-arm64.sha256 + @echo "building x86_64 windows binary..." @GOOS=windows GOARCH=amd64 go build -mod=vendor -ldflags "-X $(CMDROOT)/version.Version=$(VERSION) -X $(CMDROOT)/version.Built=$(BUILD_TIME)" -o build/infuse-Windows-x86_64.exe ./cmd/infuse @shasum -a 256 build/infuse-Windows-x86_64.exe >> build/infuse-Windows-x86_64.exe.sha256 + @echo "building arm64 windows binary..." + @GOOS=windows GOARCH=arm64 go build -mod=vendor -ldflags "-X $(CMDROOT)/version.Version=$(VERSION) -X $(CMDROOT)/version.Built=$(BUILD_TIME)" -o build/infuse-Windows-arm64.exe ./cmd/infuse + @shasum -a 256 build/infuse-Windows-arm64.exe >> build/infuse-Windows-arm64.exe.sha256 install: @go install -mod=vendor -ldflags "-X $(CMDROOT)/version.Version=$(VERSION) -X $(CMDROOT)/version.Built=$(BUILD_TIME)" ./cmd/infuse diff --git a/cmd/infuse/cli/parser/loader.go b/cmd/infuse/cli/parser/loader.go index d3fc9f2..83cc2fe 100644 --- a/cmd/infuse/cli/parser/loader.go +++ b/cmd/infuse/cli/parser/loader.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/json" "fmt" - "gopkg.in/yaml.v2" "io/ioutil" "net/http" "path/filepath" "reflect" "strings" + + "gopkg.in/yaml.v2" ) var zeroVal = reflect.Value{} diff --git a/templates/gotmpl/helpers.go b/templates/gotmpl/helpers.go index dda3792..ecfd2b8 100644 --- a/templates/gotmpl/helpers.go +++ b/templates/gotmpl/helpers.go @@ -3,12 +3,14 @@ package gotmpl import ( "encoding/json" "fmt" - "github.com/jucardi/go-streams/streams" - "github.com/jucardi/infuse/templates/helpers" - "github.com/jucardi/infuse/util/log" "io/ioutil" "reflect" "text/template" + + "github.com/jucardi/go-streams/streams" + "github.com/jucardi/infuse/templates/helpers" + "github.com/jucardi/infuse/util/log" + "github.com/jucardi/infuse/util/maps" ) var instance *helperContext @@ -46,10 +48,91 @@ func (h *helperContext) init() { _ = h.Register("map", h.mapFn, "Creates a new map[string]interface{}, the provided arguments should be key, value, key, value...") _ = h.Register("dict", h.mapFn, "Creates a new map[string]interface{}, the provided arguments should be key, value, key, value...") _ = h.Register("include", h.includeFile, "Includes a template file as an internal template reference by the provided name") - _ = h.Register("set", h.setFn, "Allows to set a value to a map[string]interface{}") + _ = h.Register("set", h.setFn, "Allows to set a value to a map[string]interface{} or map[interface{}]interface{}") _ = h.Register("append", h.append, "Appends a value into an existing array") _ = 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.") _ = h.Register("loadJson", h.loadJson, "Unmarshals a JSON string into a map[string]interface{}") + _ = h.Register("mapSet", h.mapSetFn, `Allows to set a value using an XPATH representation of the key. Accepts an optional argument to indicate if the parents should be created if they don't exist'. E.g: {{mapSet $map ".some.key.path" $value $makeEmpty }}`) + _ = 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 }}`) + _ = h.Register("mapContains", h.mapContainsFn, `Indicates whether a value at the provided XPATH representation of the key exists in the provided map`) + _ = 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{}`) +} + +func (h *helperContext) mapSetFn(obj interface{}, key string, value interface{}, makeEmpty ...bool) string { + var inMap map[string]interface{} + + switch m := obj.(type) { + case map[string]interface{}: + inMap = m + case map[interface{}]interface{}: + if converted, err := maps.ConvertMap(obj); err != nil { + panic(fmt.Sprintf("failed to convert map[interface{}]interface{} to map[string]interface{}, %s", err.Error())) + } else { + inMap = converted + } + } + + if inMap == nil { + panic(fmt.Sprintf("type not supported for map operations %T", obj)) + } + + if err := maps.SetValue(inMap, key, value, len(makeEmpty) > 0 && makeEmpty[0]); err != nil { + panic(fmt.Sprintf("failed to set value to map using key '%s' > %s", key, err.Error())) + } + + return "" +} + +func (h *helperContext) mapGetFn(obj interface{}, key string, defaultValue ...interface{}) interface{} { + var ( + inMap map[string]interface{} + ret interface{} + ) + + switch m := obj.(type) { + case map[string]interface{}: + inMap = m + case map[interface{}]interface{}: + if converted, err := maps.ConvertMap(obj); err != nil { + panic(fmt.Sprintf("failed to convert map[interface{}]interface{} to map[string]interface{}, %s", err.Error())) + } else { + inMap = converted + } + } + + if len(defaultValue) > 0 { + ret = defaultValue[0] + } + + if inMap == nil { + return ret + } + + return maps.GetOrDefault(inMap, key, ret) +} + +func (h *helperContext) mapContainsFn(obj interface{}, key string) bool { + var inMap map[string]interface{} + + switch m := obj.(type) { + case map[string]interface{}: + inMap = m + case map[interface{}]interface{}: + if converted, err := maps.ConvertMap(obj); err != nil { + panic(fmt.Sprintf("failed to convert map[interface{}]interface{} to map[string]interface{}, %s", err.Error())) + } else { + inMap = converted + } + } + return maps.Contains(inMap, key) +} + +func (h *helperContext) mapConvertFn(obj interface{}) map[string]interface{} { + ret, err := maps.ConvertMap(obj) + if err != nil { + panic(fmt.Sprintf("failed to convert to map[string]interface{}, %s", err.Error())) + } + return ret } func (h *helperContext) defaultFn(val ...interface{}) interface{} { @@ -106,8 +189,13 @@ func (h *helperContext) includeFile(name, file string) (string, error) { } func (h *helperContext) setFn(obj interface{}, key string, value interface{}) string { - m := obj.(map[string]interface{}) - m[key] = value + switch m := obj.(type) { + case map[string]interface{}: + m[key] = value + case map[interface{}]interface{}: + m[key] = value + } + return "" } diff --git a/templates/helpers/common.go b/templates/helpers/common.go index d5008cb..675b2a3 100644 --- a/templates/helpers/common.go +++ b/templates/helpers/common.go @@ -33,6 +33,7 @@ func RegisterCommon(manager IHelpersManager) { _ = manager.Register("stringsTrimSpace", strings.TrimSpace, "Returns a slice of the string s, with all leading and trailing white space removed, as defined by Unicode.") _ = manager.Register("stringsContains", strings.Contains, "Returns a boolean indicating whether the string s contains substr.") _ = manager.Register("stringsCompare", strings.Compare, "Returns an integer comparing two strings lexicographically.") + _ = manager.Register("stringsSub", stringsSub, "Returns a substring of the specified string. E.g: {{stringsSub $sourceStr, startIndex, endIndex}}") _ = manager.Register("startsWith", strings.HasPrefix, "Returns a boolean indicating whether the string s begins with prefix.") _ = manager.Register("endsWith", strings.HasSuffix, "Returns a boolean indicating whether the string s ends with suffix.") _ = manager.Register("br", bracketsFn, "Wraps the contents into double brackets {{ }}") @@ -77,6 +78,10 @@ func stringFn(arg interface{}) string { return fmt.Sprintf("%+v", arg) } +func stringsSub(sourceStr string, start, end int) string { + return sourceStr[start:end] +} + func bracketsFn(arg interface{}) string { return fmt.Sprintf("{{%+v}}", arg) } diff --git a/util/maps/maps.go b/util/maps/maps.go new file mode 100644 index 0000000..b0346c9 --- /dev/null +++ b/util/maps/maps.go @@ -0,0 +1,188 @@ +package maps + +import ( + "fmt" + "reflect" + "regexp" + "strconv" + "strings" + + "github.com/jucardi/infuse/util/reflectx" +) + +var ( + regex = regexp.MustCompile(`.*\[\d*\]$`) +) + +// Contains indicates if the given map contains an entry by the given key +func Contains(c map[string]interface{}, key string) bool { + if !strings.Contains(key, ".") { + if _, ok := c[key]; ok { + return ok + } + + return false + } + + if _, err := GetValue(c, key); err != nil { + return false + } + + return true +} + +// GetValue If a map represents a JSON with nested objects. GetValue retrieves the value by the given path. Eg. 'info.database.port' +func GetValue(data map[string]interface{}, key string) (interface{}, error) { + split := strings.Split(key, ".") + v := reflect.ValueOf(data) + for i, s := range split { + isArray := regex.MatchString(s) + index := 0 + + if isArray { + split := strings.Split(s, "[") + s = split[0] + index, _ = strconv.Atoi(split[1][:len(split[1])-1]) + } + + current := v.MapIndex(reflect.ValueOf(s)) + + if !current.IsValid() { + return nil, fmt.Errorf("unable to get value by the key '%s'. The value for '%s' is not present", key, s) + } + + if reflectx.IsNil(current) { + if i < len(split)-1 { + return nil, fmt.Errorf("unable to get value by the key '%s'. The value for '%s' is null", key, s) + } else { + return nil, nil + } + } + + if isArray { + for current = current.Elem(); current.IsValid() && current.Kind() != reflect.Slice && current.Kind() != reflect.Array; { + } + if current.Len() <= index { + return nil, fmt.Errorf("failed to retrieve value at key '%s'. Index out of range for field '%s' (index: %d | length: %d)", key, s, index, current.Len()) + } + current = current.Index(index) + } + + if i < len(split)-1 { + if v.Kind() != reflect.Map { + return nil, fmt.Errorf("unable to get value by path: '%s' | The piece '%s' does not represent an object", key, s) + } + + // m, err := ConvertMap(current.Interface()) + m, err := reflectx.GetNonPointerValue(current) + if err != nil { + return nil, err + } + + v = m + } else { + v = current + } + } + + if !v.IsValid() { + return nil, fmt.Errorf("value by the key '%s' is not present", key) + } + + if reflectx.IsNil(v) { + return nil, nil + } + + return v.Interface(), nil +} + +// GetOrDefault gets the value by the given key, if the value is not present, or an error occurs while retrieving the value, returns what was specified as `defaultVal` +func GetOrDefault(data map[string]interface{}, key string, defaultVal interface{}) interface{} { + if v, _ := GetValue(data, key); v == nil { + return defaultVal + } else { + return v + } +} + +// SetValue if a map represents a JSON with nested objects. SetValue assigns a value to the given path. Eg. 'info.database.port' +// 'makeEmpty' indicates that if a piece of the path is missing (Eg. 'info.database' is nil) an empty object should be created to continue the assignment. +func SetValue(data map[string]interface{}, key string, value interface{}, makeEmpty bool) error { + split := strings.Split(key, ".") + v := reflect.ValueOf(data) + for i, s := range split { + if i == len(split)-1 { + v.SetMapIndex(reflect.ValueOf(s), reflect.ValueOf(value)) + } else { + if v.Kind() != reflect.Map { + return fmt.Errorf("unable to get value by path: '%s' | The piece '%s' does not represent an object", key, s) + } + + val := v.MapIndex(reflect.ValueOf(s)) + + if !val.IsValid() { + if makeEmpty { + val = reflect.ValueOf(make(map[string]interface{})) + v.SetMapIndex(reflect.ValueOf(s), val) + } else { + return fmt.Errorf("unable to get value by path: '%s' | The piece '%s' does not represent an object", key, s) + } + } + v = reflect.ValueOf(val.Interface().(map[string]interface{})) + } + } + return nil +} + +func ConvertMap(val interface{}) (map[string]interface{}, error) { + if m, ok := val.(map[string]interface{}); ok { + for k, v := range m { + if mapValue, ok := v.(map[interface{}]interface{}); ok { + if newVal, err := ConvertMap(mapValue); err != nil { + return nil, fmt.Errorf("failed to convert '%s', %s ", k, err.Error()) + } else { + m[k] = newVal + } + } + } + return m, nil + } + + if m, ok := val.(map[interface{}]interface{}); ok { + ret := map[string]interface{}{} + for k, v := range m { + key, ok := k.(string) + if !ok { + return nil, fmt.Errorf("all keys must be strings when mapping to a struct, detected key: '%+v'", k) + } + if mapValue, ok := v.(map[interface{}]interface{}); ok { + if newVal, err := ConvertMap(mapValue); err != nil { + return nil, fmt.Errorf("failed to convert '%s', %s ", key, err.Error()) + } else { + ret[key] = newVal + } + } else { + ret[key] = v + } + } + return ret, nil + } + + return nil, fmt.Errorf("unexpected object type: %+v", val) +} + +func StringMapEqual(m1 map[string]string, m2 map[string]string) bool { + if len(m1) != len(m2) { + return false + } + for k1, v1 := range m1 { + if v2, ok := m2[k1]; ok { + if v1 != v2 { + return false + } + } else { + return false + } + } + return true +} diff --git a/util/reflectx/reflect.go b/util/reflectx/reflect.go new file mode 100644 index 0000000..6da70ea --- /dev/null +++ b/util/reflectx/reflect.go @@ -0,0 +1,94 @@ +package reflectx + +import ( + "errors" + "reflect" +) + +func IsNil(obj interface{}) bool { + val := getReflectValue(obj) + return obj == nil || !val.IsValid() || ((val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface) && val.IsNil()) +} + +func IsZero(obj interface{}) bool { + v := getReflectValue(obj) + if IsNil(v) { + return true + } + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func IsNillable(obj interface{}) bool { + switch getReflectValue(obj).Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.Chan, reflect.Ptr, reflect.Interface, reflect.Func, reflect.UnsafePointer: + return true + } + return false +} + +// GetValue dynamically retrieves the value of a field by name. Returns the zero Value if the field is not found +func GetValue(obj interface{}, field string) reflect.Value { + if field == "" { + return reflect.Value{} + } + val := getReflectValue(obj) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + return val.FieldByName(field) +} + +// SetValue dynamically sets a value to a field by the given name +func SetValue(obj interface{}, field string, value interface{}) { + if field == "" { + return + } + val := reflect.ValueOf(obj) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + val.FieldByName(field).Set(reflect.ValueOf(value)) +} + +// GetNonPointerValue iterates over the V.Elem() of the object until the kind is not a pointer +// or an interface and returns its value. +func GetNonPointerValue(obj interface{}) (reflect.Value, error) { + ret, _, err := getNonPointerValue(obj) + return ret, err +} + +func getReflectValue(obj interface{}) reflect.Value { + if v, ok := obj.(reflect.Value); ok { + return v + } + return reflect.ValueOf(obj) +} +func getNonPointerValue(obj interface{}) (reflect.Value, []reflect.Kind, error) { + val := getReflectValue(obj) + var typeTracker []reflect.Kind + for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + typeTracker = append(typeTracker, val.Kind()) + val = val.Elem() + } + if !val.IsValid() { + return reflect.Value{}, typeTracker, errors.New("value is invalid") + } + if IsNillable(val) && val.IsNil() { + return reflect.Value{}, typeTracker, errors.New("value is nil") + } + return val, typeTracker, nil +}