Skip to content

Commit

Permalink
minor fix to the 'set' helper so it allows map[interface{}]interface{…
Browse files Browse the repository at this point in the history
…}. 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
  • Loading branch information
jucardi committed Nov 15, 2022
1 parent 9ecd273 commit b233f42
Show file tree
Hide file tree
Showing 6 changed files with 395 additions and 10 deletions.
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion cmd/infuse/cli/parser/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
100 changes: 94 additions & 6 deletions templates/gotmpl/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{} {
Expand Down Expand Up @@ -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 ""
}

Expand Down
5 changes: 5 additions & 0 deletions templates/helpers/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {{ }}")
Expand Down Expand Up @@ -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)
}
Expand Down
188 changes: 188 additions & 0 deletions util/maps/maps.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit b233f42

Please sign in to comment.