120 lines
3.6 KiB
Go
120 lines
3.6 KiB
Go
package source
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
)
|
|
|
|
// CloneMetadata returns an independently owned copy of JSON-shaped metadata.
|
|
// It preserves concrete map, slice, and array types while rejecting values that
|
|
// cannot be safely represented as JSON-shaped metadata.
|
|
func CloneMetadata(metadata map[string]any) (map[string]any, error) {
|
|
if len(metadata) == 0 {
|
|
return nil, nil
|
|
}
|
|
cloned, err := cloneMetadataValue(reflect.ValueOf(metadata), "metadata", make(map[metadataVisit]struct{}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cloned.Interface().(map[string]any), nil
|
|
}
|
|
|
|
type metadataVisit struct {
|
|
typ reflect.Type
|
|
ptr uintptr
|
|
}
|
|
|
|
func cloneMetadataValue(value reflect.Value, location string, active map[metadataVisit]struct{}) (reflect.Value, error) {
|
|
if !value.IsValid() {
|
|
return value, nil
|
|
}
|
|
|
|
switch value.Kind() {
|
|
case reflect.Interface:
|
|
if value.IsNil() {
|
|
return reflect.Zero(value.Type()), nil
|
|
}
|
|
cloned, err := cloneMetadataValue(value.Elem(), location, active)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
result := reflect.New(value.Type()).Elem()
|
|
result.Set(cloned)
|
|
return result, nil
|
|
case reflect.Map:
|
|
if value.IsNil() {
|
|
return reflect.Zero(value.Type()), nil
|
|
}
|
|
if value.Type().Key().Kind() != reflect.String {
|
|
return reflect.Value{}, fmt.Errorf("%s has unsupported map key type %s", location, value.Type().Key())
|
|
}
|
|
leave, err := enterMetadataValue(value, active, location)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
defer leave()
|
|
|
|
keys := value.MapKeys()
|
|
sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
|
|
result := reflect.MakeMapWithSize(value.Type(), value.Len())
|
|
for _, key := range keys {
|
|
cloned, err := cloneMetadataValue(value.MapIndex(key), location+"."+key.String(), active)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
result.SetMapIndex(key, cloned)
|
|
}
|
|
return result, nil
|
|
case reflect.Slice:
|
|
if value.IsNil() {
|
|
return reflect.Zero(value.Type()), nil
|
|
}
|
|
leave, err := enterMetadataValue(value, active, location)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
defer leave()
|
|
|
|
result := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
|
|
for i := 0; i < value.Len(); i++ {
|
|
cloned, err := cloneMetadataValue(value.Index(i), fmt.Sprintf("%s[%d]", location, i), active)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
result.Index(i).Set(cloned)
|
|
}
|
|
return result, nil
|
|
case reflect.Array:
|
|
result := reflect.New(value.Type()).Elem()
|
|
for i := 0; i < value.Len(); i++ {
|
|
cloned, err := cloneMetadataValue(value.Index(i), fmt.Sprintf("%s[%d]", location, i), active)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
result.Index(i).Set(cloned)
|
|
}
|
|
return result, nil
|
|
case reflect.Float32, reflect.Float64:
|
|
if math.IsNaN(value.Float()) || math.IsInf(value.Float(), 0) {
|
|
return reflect.Value{}, fmt.Errorf("%s has a non-finite number", location)
|
|
}
|
|
return value, nil
|
|
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.String:
|
|
return value, nil
|
|
default:
|
|
return reflect.Value{}, fmt.Errorf("%s has unsupported type %s", location, value.Type())
|
|
}
|
|
}
|
|
|
|
func enterMetadataValue(value reflect.Value, active map[metadataVisit]struct{}, location string) (func(), error) {
|
|
visit := metadataVisit{typ: value.Type(), ptr: value.Pointer()}
|
|
if _, exists := active[visit]; exists {
|
|
return nil, fmt.Errorf("%s contains a cycle", location)
|
|
}
|
|
active[visit] = struct{}{}
|
|
return func() { delete(active, visit) }, nil
|
|
}
|