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 }