358 lines
7.5 KiB
Go
358 lines
7.5 KiB
Go
package maputil
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func CastKeysToStrings(s any) (map[string]any, error) {
|
|
new := map[string]any{}
|
|
switch src := s.(type) {
|
|
case map[any]any:
|
|
for k, v := range src {
|
|
var strK string
|
|
switch typedK := k.(type) {
|
|
case string:
|
|
strK = typedK
|
|
default:
|
|
return nil, fmt.Errorf("unexpected type of key in map: expected string, got %T: value=%v, map=%v", typedK, typedK, src)
|
|
}
|
|
|
|
castedV, err := RecursivelyStringifyMapKey(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
new[strK] = castedV
|
|
}
|
|
case map[string]any:
|
|
for k, v := range src {
|
|
castedV, err := RecursivelyStringifyMapKey(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
new[k] = castedV
|
|
}
|
|
}
|
|
return new, nil
|
|
}
|
|
|
|
func RecursivelyStringifyMapKey(v any) (any, error) {
|
|
var castedV any
|
|
switch typedV := v.(type) {
|
|
case map[any]any, map[string]any:
|
|
tmp, err := CastKeysToStrings(typedV)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
castedV = tmp
|
|
case []any:
|
|
a := []any{}
|
|
for i := range typedV {
|
|
res, err := RecursivelyStringifyMapKey(typedV[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a = append(a, res)
|
|
}
|
|
castedV = a
|
|
default:
|
|
castedV = typedV
|
|
}
|
|
return castedV, nil
|
|
}
|
|
|
|
type arg interface {
|
|
getMap(map[string]any) map[string]any
|
|
set(map[string]any, any)
|
|
}
|
|
|
|
type keyArg struct {
|
|
key string
|
|
}
|
|
|
|
func (a keyArg) getMap(m map[string]any) map[string]any {
|
|
_, ok := m[a.key]
|
|
if !ok {
|
|
m[a.key] = map[string]any{}
|
|
}
|
|
switch t := m[a.key].(type) {
|
|
case map[string]any:
|
|
return t
|
|
default:
|
|
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
|
|
}
|
|
}
|
|
|
|
func (a keyArg) set(m map[string]any, value any) {
|
|
m[a.key] = value
|
|
}
|
|
|
|
type indexedKeyArg struct {
|
|
key string
|
|
index int
|
|
}
|
|
|
|
func (a indexedKeyArg) getArray(m map[string]any) []any {
|
|
_, ok := m[a.key]
|
|
if !ok {
|
|
m[a.key] = make([]any, a.index+1)
|
|
}
|
|
switch t := m[a.key].(type) {
|
|
case []any:
|
|
if len(t) <= a.index {
|
|
t2 := make([]any, a.index+1)
|
|
copy(t2, t)
|
|
t = t2
|
|
}
|
|
return t
|
|
default:
|
|
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
|
|
}
|
|
}
|
|
|
|
func (a indexedKeyArg) getMap(m map[string]any) map[string]any {
|
|
t := a.getArray(m)
|
|
if t[a.index] == nil {
|
|
t[a.index] = map[string]any{}
|
|
}
|
|
switch t := t[a.index].(type) {
|
|
case map[string]any:
|
|
return t
|
|
default:
|
|
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
|
|
}
|
|
}
|
|
|
|
func (a indexedKeyArg) set(m map[string]any, value any) {
|
|
t := a.getArray(m)
|
|
t[a.index] = value
|
|
m[a.key] = t
|
|
}
|
|
|
|
func getCursor(key string) arg {
|
|
key = strings.ReplaceAll(key, "[", " ")
|
|
key = strings.ReplaceAll(key, "]", " ")
|
|
k := key
|
|
idx := 0
|
|
|
|
n, err := fmt.Sscanf(key, "%s %d", &k, &idx)
|
|
|
|
if n == 2 && err == nil {
|
|
return indexedKeyArg{
|
|
key: k,
|
|
index: idx,
|
|
}
|
|
}
|
|
|
|
return keyArg{
|
|
key: key,
|
|
}
|
|
}
|
|
|
|
func ParseKey(key string) []string {
|
|
r := []string{}
|
|
part := ""
|
|
escaped := false
|
|
for _, rune := range key {
|
|
split := false
|
|
switch {
|
|
case !escaped && rune == '\\':
|
|
escaped = true
|
|
continue
|
|
case rune == '.':
|
|
split = !escaped
|
|
}
|
|
escaped = false
|
|
if split {
|
|
r = append(r, part)
|
|
part = ""
|
|
} else {
|
|
part += string(rune)
|
|
}
|
|
}
|
|
if len(part) > 0 {
|
|
r = append(r, part)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func Set(m map[string]any, key []string, value string, stringBool bool) {
|
|
if len(key) == 0 {
|
|
panic(fmt.Errorf("bug: unexpected length of key: %d", len(key)))
|
|
}
|
|
|
|
for len(key) > 1 {
|
|
m, key = getCursor(key[0]).getMap(m), key[1:]
|
|
}
|
|
|
|
getCursor(key[0]).set(m, typedVal(value, stringBool))
|
|
}
|
|
|
|
func typedVal(val string, st bool) any {
|
|
// if st is true, directly return it without casting it
|
|
if st {
|
|
return val
|
|
}
|
|
|
|
if strings.EqualFold(val, "true") {
|
|
return true
|
|
}
|
|
|
|
if strings.EqualFold(val, "false") {
|
|
return false
|
|
}
|
|
|
|
if strings.EqualFold(val, "null") {
|
|
return nil
|
|
}
|
|
|
|
// handling of only zero, if val has zero prefix, it will be considered as string
|
|
if strings.EqualFold(val, "0") {
|
|
return int64(0)
|
|
}
|
|
|
|
// If this value does not start with zero, try parsing it to an int
|
|
if len(val) != 0 && val[0] != '0' {
|
|
if iv, err := strconv.ParseInt(val, 10, 64); err == nil {
|
|
return iv
|
|
}
|
|
}
|
|
|
|
return val
|
|
}
|
|
|
|
// MergeMaps merges two maps with special handling for nested maps and arrays.
|
|
func MergeMaps(a, b map[string]interface{}, opts ...MergeOptions) map[string]interface{} {
|
|
arrayStrategy := ArrayMergeStrategySparse
|
|
if len(opts) > 0 {
|
|
arrayStrategy = opts[0].ArrayStrategy
|
|
}
|
|
|
|
out := make(map[string]interface{}, len(a))
|
|
for k, v := range a {
|
|
out[k] = v
|
|
}
|
|
for k, v := range b {
|
|
if v == nil {
|
|
// If key doesn't exist in base, add nil (issue #1154).
|
|
// If key exists in base, don't overwrite with nil.
|
|
if _, exists := out[k]; !exists {
|
|
out[k] = nil
|
|
}
|
|
continue
|
|
}
|
|
if v, ok := v.(map[string]interface{}); ok {
|
|
if bv, ok := out[k]; ok {
|
|
if bv, ok := bv.(map[string]interface{}); ok {
|
|
out[k] = MergeMaps(bv, v, opts...)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
vSlice := toInterfaceSlice(v)
|
|
if vSlice != nil {
|
|
if outVal, exists := out[k]; exists {
|
|
outSlice := toInterfaceSlice(outVal)
|
|
if outSlice != nil {
|
|
out[k] = mergeSlices(outSlice, vSlice, arrayStrategy)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func toInterfaceSlice(v any) []any {
|
|
if slice, ok := v.([]any); ok {
|
|
return slice
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ArrayMergeStrategy int
|
|
|
|
const (
|
|
// ArrayMergeStrategySparse uses auto-detection: sparse arrays (with nils) merge
|
|
// element-by-element, complete arrays (no nils) replace entirely.
|
|
ArrayMergeStrategySparse ArrayMergeStrategy = iota
|
|
// ArrayMergeStrategyReplace always replaces arrays entirely.
|
|
ArrayMergeStrategyReplace
|
|
// ArrayMergeStrategyMerge always merges arrays element-by-element (for CLI overrides).
|
|
ArrayMergeStrategyMerge
|
|
)
|
|
|
|
type MergeOptions struct {
|
|
ArrayStrategy ArrayMergeStrategy
|
|
}
|
|
|
|
// mergeSlices merges two slices based on the strategy.
|
|
//
|
|
// Behavior by strategy:
|
|
// - ArrayMergeStrategyReplace: always returns override as-is
|
|
// - ArrayMergeStrategySparse: auto-detects based on nil values (see below)
|
|
// - ArrayMergeStrategyMerge: always merges element-by-element
|
|
//
|
|
// For Sparse strategy, auto-detection logic:
|
|
// - If override contains ANY nil values → treated as sparse → merge element-by-element
|
|
// - If override contains NO nil values → treated as complete → replace entirely
|
|
//
|
|
// Edge cases:
|
|
// - Empty array `[]` has no nils, so it replaces entirely. This is intentional:
|
|
// explicitly setting an empty array should clear the base array.
|
|
// - Explicit `[null, value]` in YAML is treated as sparse (rare but correct).
|
|
//
|
|
// Recursive behavior: When merging maps within array elements, the same strategy
|
|
// is propagated to nested MergeMaps calls, maintaining consistent merge semantics.
|
|
func mergeSlices(base, override []any, strategy ArrayMergeStrategy) []any {
|
|
if strategy == ArrayMergeStrategyReplace {
|
|
return override
|
|
}
|
|
|
|
if strategy == ArrayMergeStrategySparse {
|
|
isSparse := false
|
|
for _, v := range override {
|
|
if v == nil {
|
|
isSparse = true
|
|
break
|
|
}
|
|
}
|
|
if !isSparse {
|
|
return override
|
|
}
|
|
}
|
|
|
|
// Merge element-by-element (for ArrayMergeStrategyMerge or sparse arrays)
|
|
maxLen := len(base)
|
|
if len(override) > maxLen {
|
|
maxLen = len(override)
|
|
}
|
|
|
|
result := make([]interface{}, maxLen)
|
|
copy(result, base)
|
|
|
|
for i := 0; i < len(override); i++ {
|
|
overrideVal := override[i]
|
|
if overrideVal == nil {
|
|
continue
|
|
}
|
|
|
|
if overrideMap, ok := overrideVal.(map[string]any); ok {
|
|
if i < len(base) {
|
|
if baseMap, ok := base[i].(map[string]any); ok {
|
|
result[i] = MergeMaps(baseMap, overrideMap, MergeOptions{ArrayStrategy: strategy})
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
result[i] = overrideVal
|
|
}
|
|
|
|
return result
|
|
}
|