165 lines
3.4 KiB
Go
165 lines
3.4 KiB
Go
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
|
|
}
|