helmfile/pkg/yaml/append_processor.go

175 lines
3.9 KiB
Go

package yaml
import (
"fmt"
"strings"
)
type AppendProcessor struct{}
func NewAppendProcessor() *AppendProcessor {
return &AppendProcessor{}
}
func (ap *AppendProcessor) ProcessMap(data map[string]any) (map[string]any, error) {
result := make(map[string]any)
// First pass: collect all append keys and their base keys
appendKeys := make(map[string][]any)
baseKeys := make(map[string]any)
for key, value := range data {
if IsAppendKey(key) {
baseKey := GetBaseKey(key)
appendKeys[baseKey] = append(appendKeys[baseKey], value)
} else {
baseKeys[key] = value
}
}
// Second pass: process all values recursively
for key, value := range baseKeys {
processedValue, err := ap.processValue(value)
if err != nil {
return nil, fmt.Errorf("failed to process value for key %s: %w", key, err)
}
result[key] = processedValue
}
// Third pass: merge append keys with their base keys
for baseKey, appendValues := range appendKeys {
for _, appendValue := range appendValues {
processedValue, err := ap.processValue(appendValue)
if err != nil {
return nil, fmt.Errorf("failed to process append value for key %s: %w", baseKey, err)
}
if existingValue, exists := result[baseKey]; exists {
if isSlice(processedValue) && isSlice(existingValue) {
// Always append to the base key's slice
result[baseKey] = append(existingValue.([]any), processedValue.([]any)...)
} else {
// If not both slices, overwrite (fallback)
result[baseKey] = processedValue
}
} else {
result[baseKey] = processedValue
}
}
}
return result, nil
}
func (ap *AppendProcessor) processValue(value any) (any, error) {
switch v := value.(type) {
case map[string]any:
return ap.ProcessMap(v)
case map[any]any:
converted := make(map[string]any)
for k, val := range v {
if strKey, ok := k.(string); ok {
converted[strKey] = val
} else {
return nil, fmt.Errorf("non-string key in map: %v", k)
}
}
return ap.ProcessMap(converted)
case []any:
result := make([]any, len(v))
for i, elem := range v {
processed, err := ap.processValue(elem)
if err != nil {
return nil, fmt.Errorf("failed to process slice element %d: %w", i, err)
}
result[i] = processed
}
return result, nil
default:
return value, nil
}
}
func (ap *AppendProcessor) MergeWithAppend(dest, src map[string]any) error {
convertToStringMapInPlace(dest)
convertToStringMapInPlace(src)
for key, srcValue := range src {
if IsAppendKey(key) {
baseKey := GetBaseKey(key)
destValue, exists := dest[baseKey]
if exists {
if isSlice(srcValue) && isSlice(destValue) {
destSlice := destValue.([]any)
srcSlice := srcValue.([]any)
dest[baseKey] = append(destSlice, srcSlice...)
} else {
dest[baseKey] = srcValue
}
} else {
dest[baseKey] = srcValue
}
delete(src, key)
}
}
for key, srcValue := range src {
if isMap(srcValue) {
srcMap := srcValue.(map[string]any)
if destMap, ok := dest[key].(map[string]any); ok {
if err := ap.MergeWithAppend(destMap, srcMap); err != nil {
return err
}
dest[key] = destMap
} else {
dest[key] = srcMap
}
} else {
dest[key] = srcValue
}
}
return nil
}
func convertToStringMapInPlace(v any) any {
switch t := v.(type) {
case map[string]any:
for k, v2 := range t {
t[k] = convertToStringMapInPlace(v2)
}
return t
case map[any]any:
m := make(map[string]any, len(t))
for k, v2 := range t {
if ks, ok := k.(string); ok {
m[ks] = convertToStringMapInPlace(v2)
}
}
return m
case []any:
for i, v2 := range t {
t[i] = convertToStringMapInPlace(v2)
}
return t
default:
return v
}
}
func isSlice(value any) bool {
_, ok := value.([]any)
return ok
}
func isMap(value any) bool {
_, ok := value.(map[string]any)
return ok
}
func IsAppendKey(key string) bool {
return strings.HasSuffix(key, "+")
}
func GetBaseKey(key string) string {
return strings.TrimSuffix(key, "+")
}