package util import ( "encoding/json" "fmt" "strings" ) // GetByPath retrieves a value from a nested map using dot-notation path. // e.g. GetByPath(node, "ws-opts.headers.Host") func GetByPath(input map[string]any, path string) any { keys := strings.Split(path, ".") var current any = input for _, key := range keys { if current == nil { return nil } m, ok := current.(map[string]any) if !ok { return nil } current = m[key] } return current } // SetByPath sets a value in a nested map using dot-notation path, // creating intermediate maps as needed. func SetByPath(input map[string]any, path string, value any) map[string]any { keys := strings.Split(path, ".") current := input for i := 0; i < len(keys)-1; i++ { key := keys[i] next, ok := current[key].(map[string]any) if !ok { next = map[string]any{} current[key] = next } current = next } current[keys[len(keys)-1]] = value return input } func GetString(input map[string]any, path string) string { v := GetByPath(input, path) if s, ok := v.(string); ok { return s } return "" } func GetInt(input map[string]any, path string) int { v := GetByPath(input, path) switch n := v.(type) { case int: return n case int64: return int(n) case float64: return int(n) case string: var i int fmt.Sscanf(n, "%d", &i) return i } return 0 } func GetBool(input map[string]any, path string) bool { v := GetByPath(input, path) if b, ok := v.(bool); ok { return b } return false } // DeepCopy produces a deep copy of a ProxyNode via JSON round-trip. func DeepCopy(node map[string]any) map[string]any { data, err := json.Marshal(node) if err != nil { return map[string]any{} } var copy map[string]any if err := json.Unmarshal(data, ©); err != nil { return map[string]any{} } return copy } // StripUndefined removes keys with nil or empty-string values from a map. func StripUndefined(input map[string]any) map[string]any { result := make(map[string]any, len(input)) for k, v := range input { if v == nil { continue } if s, ok := v.(string); ok && s == "" { continue } result[k] = v } return result } // MergeDeep recursively merges `next` into `base`, returning a new map. func MergeDeep(base, next map[string]any) map[string]any { result := make(map[string]any, len(base)) for k, v := range base { result[k] = v } for k, v := range next { if v == nil { continue } if isPlainObject(v) && isPlainObject(result[k]) { result[k] = MergeDeep(result[k].(map[string]any), v.(map[string]any)) } else { result[k] = v } } return result } func isPlainObject(v any) bool { _, ok := v.(map[string]any) return ok } // ToId normalizes a string into a valid record ID: lowercase, non-alphanum → dash, max 64. func ToId(input string) string { s := strings.TrimSpace(input) b := make([]byte, 0, len(s)) prevDash := false for i := 0; i < len(s); i++ { c := s[i] if c >= 'A' && c <= 'Z' { c += 32 } if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' { b = append(b, c) prevDash = false } else if c == '-' { if !prevDash { b = append(b, '-') prevDash = true } } else { if !prevDash && len(b) > 0 { b = append(b, '-') prevDash = true } } } // trim leading/trailing dashes result := strings.Trim(string(b), "-") if len(result) > 64 { result = result[:64] } if result == "" { return "item" } return result }