Skip to content

Commit b233f42

Browse files
committed
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
1 parent 9ecd273 commit b233f42

6 files changed

Lines changed: 395 additions & 10 deletions

File tree

Makefile

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,24 @@ compile-all: deps
2222
@echo "compiling..."
2323
@rm -rf build
2424
@mkdir build
25-
@echo "building linux binary..."
25+
@echo "building x86_64 linux binary..."
2626
@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
2727
@shasum -a 256 build/infuse-Linux-x86_64 >> build/infuse-Linux-x86_64.sha256
28-
@echo "building macosx binary..."
28+
@echo "building arm64 linux binary..."
29+
@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
30+
@shasum -a 256 build/infuse-Linux-arm64 >> build/infuse-Linux-arm64.sha256
31+
@echo "building x86_64 macosx binary..."
2932
@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
3033
@shasum -a 256 build/infuse-Darwin-x86_64 >> build/infuse-Darwin-x86_64.sha256
31-
@echo "building windows binary..."
34+
@echo "building arm64 macosx binary..."
35+
@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
36+
@shasum -a 256 build/infuse-Darwin-arm64 >> build/infuse-Darwin-arm64.sha256
37+
@echo "building x86_64 windows binary..."
3238
@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
3339
@shasum -a 256 build/infuse-Windows-x86_64.exe >> build/infuse-Windows-x86_64.exe.sha256
40+
@echo "building arm64 windows binary..."
41+
@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
42+
@shasum -a 256 build/infuse-Windows-arm64.exe >> build/infuse-Windows-arm64.exe.sha256
3443

3544
install:
3645
@go install -mod=vendor -ldflags "-X $(CMDROOT)/version.Version=$(VERSION) -X $(CMDROOT)/version.Built=$(BUILD_TIME)" ./cmd/infuse

cmd/infuse/cli/parser/loader.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7-
"gopkg.in/yaml.v2"
87
"io/ioutil"
98
"net/http"
109
"path/filepath"
1110
"reflect"
1211
"strings"
12+
13+
"gopkg.in/yaml.v2"
1314
)
1415

1516
var zeroVal = reflect.Value{}

templates/gotmpl/helpers.go

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package gotmpl
33
import (
44
"encoding/json"
55
"fmt"
6-
"github.com/jucardi/go-streams/streams"
7-
"github.com/jucardi/infuse/templates/helpers"
8-
"github.com/jucardi/infuse/util/log"
96
"io/ioutil"
107
"reflect"
118
"text/template"
9+
10+
"github.com/jucardi/go-streams/streams"
11+
"github.com/jucardi/infuse/templates/helpers"
12+
"github.com/jucardi/infuse/util/log"
13+
"github.com/jucardi/infuse/util/maps"
1214
)
1315

1416
var instance *helperContext
@@ -46,10 +48,91 @@ func (h *helperContext) init() {
4648
_ = h.Register("map", h.mapFn, "Creates a new map[string]interface{}, the provided arguments should be key, value, key, value...")
4749
_ = h.Register("dict", h.mapFn, "Creates a new map[string]interface{}, the provided arguments should be key, value, key, value...")
4850
_ = h.Register("include", h.includeFile, "Includes a template file as an internal template reference by the provided name")
49-
_ = h.Register("set", h.setFn, "Allows to set a value to a map[string]interface{}")
51+
_ = h.Register("set", h.setFn, "Allows to set a value to a map[string]interface{} or map[interface{}]interface{}")
5052
_ = h.Register("append", h.append, "Appends a value into an existing array")
5153
_ = 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.")
5254
_ = h.Register("loadJson", h.loadJson, "Unmarshals a JSON string into a map[string]interface{}")
55+
_ = 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 }}`)
56+
_ = 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 }}`)
57+
_ = h.Register("mapContains", h.mapContainsFn, `Indicates whether a value at the provided XPATH representation of the key exists in the provided map`)
58+
_ = 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{}`)
59+
}
60+
61+
func (h *helperContext) mapSetFn(obj interface{}, key string, value interface{}, makeEmpty ...bool) string {
62+
var inMap map[string]interface{}
63+
64+
switch m := obj.(type) {
65+
case map[string]interface{}:
66+
inMap = m
67+
case map[interface{}]interface{}:
68+
if converted, err := maps.ConvertMap(obj); err != nil {
69+
panic(fmt.Sprintf("failed to convert map[interface{}]interface{} to map[string]interface{}, %s", err.Error()))
70+
} else {
71+
inMap = converted
72+
}
73+
}
74+
75+
if inMap == nil {
76+
panic(fmt.Sprintf("type not supported for map operations %T", obj))
77+
}
78+
79+
if err := maps.SetValue(inMap, key, value, len(makeEmpty) > 0 && makeEmpty[0]); err != nil {
80+
panic(fmt.Sprintf("failed to set value to map using key '%s' > %s", key, err.Error()))
81+
}
82+
83+
return ""
84+
}
85+
86+
func (h *helperContext) mapGetFn(obj interface{}, key string, defaultValue ...interface{}) interface{} {
87+
var (
88+
inMap map[string]interface{}
89+
ret interface{}
90+
)
91+
92+
switch m := obj.(type) {
93+
case map[string]interface{}:
94+
inMap = m
95+
case map[interface{}]interface{}:
96+
if converted, err := maps.ConvertMap(obj); err != nil {
97+
panic(fmt.Sprintf("failed to convert map[interface{}]interface{} to map[string]interface{}, %s", err.Error()))
98+
} else {
99+
inMap = converted
100+
}
101+
}
102+
103+
if len(defaultValue) > 0 {
104+
ret = defaultValue[0]
105+
}
106+
107+
if inMap == nil {
108+
return ret
109+
}
110+
111+
return maps.GetOrDefault(inMap, key, ret)
112+
}
113+
114+
func (h *helperContext) mapContainsFn(obj interface{}, key string) bool {
115+
var inMap map[string]interface{}
116+
117+
switch m := obj.(type) {
118+
case map[string]interface{}:
119+
inMap = m
120+
case map[interface{}]interface{}:
121+
if converted, err := maps.ConvertMap(obj); err != nil {
122+
panic(fmt.Sprintf("failed to convert map[interface{}]interface{} to map[string]interface{}, %s", err.Error()))
123+
} else {
124+
inMap = converted
125+
}
126+
}
127+
return maps.Contains(inMap, key)
128+
}
129+
130+
func (h *helperContext) mapConvertFn(obj interface{}) map[string]interface{} {
131+
ret, err := maps.ConvertMap(obj)
132+
if err != nil {
133+
panic(fmt.Sprintf("failed to convert to map[string]interface{}, %s", err.Error()))
134+
}
135+
return ret
53136
}
54137

55138
func (h *helperContext) defaultFn(val ...interface{}) interface{} {
@@ -106,8 +189,13 @@ func (h *helperContext) includeFile(name, file string) (string, error) {
106189
}
107190

108191
func (h *helperContext) setFn(obj interface{}, key string, value interface{}) string {
109-
m := obj.(map[string]interface{})
110-
m[key] = value
192+
switch m := obj.(type) {
193+
case map[string]interface{}:
194+
m[key] = value
195+
case map[interface{}]interface{}:
196+
m[key] = value
197+
}
198+
111199
return ""
112200
}
113201

templates/helpers/common.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func RegisterCommon(manager IHelpersManager) {
3333
_ = manager.Register("stringsTrimSpace", strings.TrimSpace, "Returns a slice of the string s, with all leading and trailing white space removed, as defined by Unicode.")
3434
_ = manager.Register("stringsContains", strings.Contains, "Returns a boolean indicating whether the string s contains substr.")
3535
_ = manager.Register("stringsCompare", strings.Compare, "Returns an integer comparing two strings lexicographically.")
36+
_ = manager.Register("stringsSub", stringsSub, "Returns a substring of the specified string. E.g: {{stringsSub $sourceStr, startIndex, endIndex}}")
3637
_ = manager.Register("startsWith", strings.HasPrefix, "Returns a boolean indicating whether the string s begins with prefix.")
3738
_ = manager.Register("endsWith", strings.HasSuffix, "Returns a boolean indicating whether the string s ends with suffix.")
3839
_ = manager.Register("br", bracketsFn, "Wraps the contents into double brackets {{ }}")
@@ -77,6 +78,10 @@ func stringFn(arg interface{}) string {
7778
return fmt.Sprintf("%+v", arg)
7879
}
7980

81+
func stringsSub(sourceStr string, start, end int) string {
82+
return sourceStr[start:end]
83+
}
84+
8085
func bracketsFn(arg interface{}) string {
8186
return fmt.Sprintf("{{%+v}}", arg)
8287
}

util/maps/maps.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package maps
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"regexp"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/jucardi/infuse/util/reflectx"
11+
)
12+
13+
var (
14+
regex = regexp.MustCompile(`.*\[\d*\]$`)
15+
)
16+
17+
// Contains indicates if the given map contains an entry by the given key
18+
func Contains(c map[string]interface{}, key string) bool {
19+
if !strings.Contains(key, ".") {
20+
if _, ok := c[key]; ok {
21+
return ok
22+
}
23+
24+
return false
25+
}
26+
27+
if _, err := GetValue(c, key); err != nil {
28+
return false
29+
}
30+
31+
return true
32+
}
33+
34+
// GetValue If a map represents a JSON with nested objects. GetValue retrieves the value by the given path. Eg. 'info.database.port'
35+
func GetValue(data map[string]interface{}, key string) (interface{}, error) {
36+
split := strings.Split(key, ".")
37+
v := reflect.ValueOf(data)
38+
for i, s := range split {
39+
isArray := regex.MatchString(s)
40+
index := 0
41+
42+
if isArray {
43+
split := strings.Split(s, "[")
44+
s = split[0]
45+
index, _ = strconv.Atoi(split[1][:len(split[1])-1])
46+
}
47+
48+
current := v.MapIndex(reflect.ValueOf(s))
49+
50+
if !current.IsValid() {
51+
return nil, fmt.Errorf("unable to get value by the key '%s'. The value for '%s' is not present", key, s)
52+
}
53+
54+
if reflectx.IsNil(current) {
55+
if i < len(split)-1 {
56+
return nil, fmt.Errorf("unable to get value by the key '%s'. The value for '%s' is null", key, s)
57+
} else {
58+
return nil, nil
59+
}
60+
}
61+
62+
if isArray {
63+
for current = current.Elem(); current.IsValid() && current.Kind() != reflect.Slice && current.Kind() != reflect.Array; {
64+
}
65+
if current.Len() <= index {
66+
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())
67+
}
68+
current = current.Index(index)
69+
}
70+
71+
if i < len(split)-1 {
72+
if v.Kind() != reflect.Map {
73+
return nil, fmt.Errorf("unable to get value by path: '%s' | The piece '%s' does not represent an object", key, s)
74+
}
75+
76+
// m, err := ConvertMap(current.Interface())
77+
m, err := reflectx.GetNonPointerValue(current)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
v = m
83+
} else {
84+
v = current
85+
}
86+
}
87+
88+
if !v.IsValid() {
89+
return nil, fmt.Errorf("value by the key '%s' is not present", key)
90+
}
91+
92+
if reflectx.IsNil(v) {
93+
return nil, nil
94+
}
95+
96+
return v.Interface(), nil
97+
}
98+
99+
// 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`
100+
func GetOrDefault(data map[string]interface{}, key string, defaultVal interface{}) interface{} {
101+
if v, _ := GetValue(data, key); v == nil {
102+
return defaultVal
103+
} else {
104+
return v
105+
}
106+
}
107+
108+
// SetValue if a map represents a JSON with nested objects. SetValue assigns a value to the given path. Eg. 'info.database.port'
109+
// '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.
110+
func SetValue(data map[string]interface{}, key string, value interface{}, makeEmpty bool) error {
111+
split := strings.Split(key, ".")
112+
v := reflect.ValueOf(data)
113+
for i, s := range split {
114+
if i == len(split)-1 {
115+
v.SetMapIndex(reflect.ValueOf(s), reflect.ValueOf(value))
116+
} else {
117+
if v.Kind() != reflect.Map {
118+
return fmt.Errorf("unable to get value by path: '%s' | The piece '%s' does not represent an object", key, s)
119+
}
120+
121+
val := v.MapIndex(reflect.ValueOf(s))
122+
123+
if !val.IsValid() {
124+
if makeEmpty {
125+
val = reflect.ValueOf(make(map[string]interface{}))
126+
v.SetMapIndex(reflect.ValueOf(s), val)
127+
} else {
128+
return fmt.Errorf("unable to get value by path: '%s' | The piece '%s' does not represent an object", key, s)
129+
}
130+
}
131+
v = reflect.ValueOf(val.Interface().(map[string]interface{}))
132+
}
133+
}
134+
return nil
135+
}
136+
137+
func ConvertMap(val interface{}) (map[string]interface{}, error) {
138+
if m, ok := val.(map[string]interface{}); ok {
139+
for k, v := range m {
140+
if mapValue, ok := v.(map[interface{}]interface{}); ok {
141+
if newVal, err := ConvertMap(mapValue); err != nil {
142+
return nil, fmt.Errorf("failed to convert '%s', %s ", k, err.Error())
143+
} else {
144+
m[k] = newVal
145+
}
146+
}
147+
}
148+
return m, nil
149+
}
150+
151+
if m, ok := val.(map[interface{}]interface{}); ok {
152+
ret := map[string]interface{}{}
153+
for k, v := range m {
154+
key, ok := k.(string)
155+
if !ok {
156+
return nil, fmt.Errorf("all keys must be strings when mapping to a struct, detected key: '%+v'", k)
157+
}
158+
if mapValue, ok := v.(map[interface{}]interface{}); ok {
159+
if newVal, err := ConvertMap(mapValue); err != nil {
160+
return nil, fmt.Errorf("failed to convert '%s', %s ", key, err.Error())
161+
} else {
162+
ret[key] = newVal
163+
}
164+
} else {
165+
ret[key] = v
166+
}
167+
}
168+
return ret, nil
169+
}
170+
171+
return nil, fmt.Errorf("unexpected object type: %+v", val)
172+
}
173+
174+
func StringMapEqual(m1 map[string]string, m2 map[string]string) bool {
175+
if len(m1) != len(m2) {
176+
return false
177+
}
178+
for k1, v1 := range m1 {
179+
if v2, ok := m2[k1]; ok {
180+
if v1 != v2 {
181+
return false
182+
}
183+
} else {
184+
return false
185+
}
186+
}
187+
return true
188+
}

0 commit comments

Comments
 (0)