Centralize trusted JSON value cloning

This commit is contained in:
2026-11-05 14:02:20 +00:00
parent 6112ee8fb4
commit a02561d40f
4 changed files with 130 additions and 81 deletions

View File

@@ -2,7 +2,6 @@ package promptkit
import (
"fmt"
"reflect"
"unicode/utf8"
"go.maximumdirect.net/promptkit/internal/domain"
@@ -395,88 +394,10 @@ func copyAnyMap(src map[string]any) map[string]any {
if src == nil {
return nil
}
out := make(map[string]any, len(src))
for k, v := range src {
out[k] = copyAny(v)
}
return out
return jsonvalue.CloneTrusted(src).(map[string]any)
}
func copyAny(value any) any {
if value == nil {
return nil
}
switch v := value.(type) {
case map[string]any:
return copyAnyMap(v)
case []any:
out := make([]any, len(v))
for i, item := range v {
out[i] = copyAny(item)
}
return out
case []string:
return copyStringSlice(v)
case []byte:
return copyBytes(v)
default:
return copyReflectValue(reflect.ValueOf(value)).Interface()
}
}
func copyReflectValue(value reflect.Value) reflect.Value {
if !value.IsValid() {
return value
}
switch value.Kind() {
case reflect.Interface:
if value.IsNil() {
return reflect.Zero(value.Type())
}
copied := copyReflectValue(value.Elem())
if copied.IsValid() && copied.Type().AssignableTo(value.Type()) {
return copied
}
out := reflect.New(value.Type()).Elem()
out.Set(copied)
return out
case reflect.Pointer:
if value.IsNil() {
return reflect.Zero(value.Type())
}
out := reflect.New(value.Type().Elem())
out.Elem().Set(copyReflectValue(value.Elem()))
return out
case reflect.Map:
if value.IsNil() {
return reflect.Zero(value.Type())
}
out := reflect.MakeMapWithSize(value.Type(), value.Len())
iter := value.MapRange()
for iter.Next() {
out.SetMapIndex(copyReflectValue(iter.Key()), copyReflectValue(iter.Value()))
}
return out
case reflect.Slice:
if value.IsNil() {
return reflect.Zero(value.Type())
}
out := reflect.MakeSlice(value.Type(), value.Len(), value.Cap())
for i := 0; i < value.Len(); i++ {
out.Index(i).Set(copyReflectValue(value.Index(i)))
}
return out
case reflect.Array:
out := reflect.New(value.Type()).Elem()
for i := 0; i < value.Len(); i++ {
out.Index(i).Set(copyReflectValue(value.Index(i)))
}
return out
default:
return value
}
}
func copyAny(value any) any { return jsonvalue.CloneTrusted(value) }
func copyStringSlice(src []string) []string {
if src == nil {

View File

@@ -423,6 +423,10 @@ not performance guarantees.
mutation tests use independently specified expectations. Include distinguishing
nil/type/schema-key cases; source conversion from Stage 2 must not reappear here.
Completion: trusted nested-value cloning is owned by internal/jsonvalue while
bounded input copying remains separate. Focused type, nil, schema-key, client,
prepared-state, and inspection tests pass on Go 1.27.3.
## Stage 16: Stop Recopying Growing Condition Evidence
**Catalog:** 10. **Files:** prompt requirements evaluation and focused tests.

View File

@@ -0,0 +1,97 @@
package jsonvalue
import "reflect"
func cloneTrustedMap(src map[string]any) map[string]any {
if src == nil {
return nil
}
out := make(map[string]any, len(src))
for k, v := range src {
out[k] = CloneTrusted(v)
}
return out
}
// CloneTrusted detaches a previously accepted value without revalidating it.
func CloneTrusted(value any) any {
if value == nil {
return nil
}
switch v := value.(type) {
case map[string]any:
return cloneTrustedMap(v)
case []any:
out := make([]any, len(v))
for i, item := range v {
out[i] = CloneTrusted(item)
}
return out
case []string:
if v == nil {
return []string(nil)
}
return append([]string{}, v...)
case []byte:
if v == nil {
return []byte(nil)
}
return append([]byte{}, v...)
default:
return cloneTrustedReflect(reflect.ValueOf(value)).Interface()
}
}
func cloneTrustedReflect(value reflect.Value) reflect.Value {
if !value.IsValid() {
return value
}
switch value.Kind() {
case reflect.Interface:
if value.IsNil() {
return reflect.Zero(value.Type())
}
copied := cloneTrustedReflect(value.Elem())
if copied.IsValid() && copied.Type().AssignableTo(value.Type()) {
return copied
}
out := reflect.New(value.Type()).Elem()
out.Set(copied)
return out
case reflect.Pointer:
if value.IsNil() {
return reflect.Zero(value.Type())
}
out := reflect.New(value.Type().Elem())
out.Elem().Set(cloneTrustedReflect(value.Elem()))
return out
case reflect.Map:
if value.IsNil() {
return reflect.Zero(value.Type())
}
out := reflect.MakeMapWithSize(value.Type(), value.Len())
iter := value.MapRange()
for iter.Next() {
out.SetMapIndex(cloneTrustedReflect(iter.Key()), cloneTrustedReflect(iter.Value()))
}
return out
case reflect.Slice:
if value.IsNil() {
return reflect.Zero(value.Type())
}
out := reflect.MakeSlice(value.Type(), value.Len(), value.Cap())
for i := 0; i < value.Len(); i++ {
out.Index(i).Set(cloneTrustedReflect(value.Index(i)))
}
return out
case reflect.Array:
out := reflect.New(value.Type()).Elem()
for i := 0; i < value.Len(); i++ {
out.Index(i).Set(cloneTrustedReflect(value.Index(i)))
}
return out
default:
return value
}
}

View File

@@ -0,0 +1,27 @@
package jsonvalue
import (
"encoding/json"
"reflect"
"testing"
)
type trustedNamed string
func TestCloneTrustedPreservesTypesAndDetaches(t *testing.T) {
named := trustedNamed("value")
original := map[string]any{"": map[string]any{"number": json.Number("9007199254740993")}, "typed": []string{"original"}, "pointer": &named, "nilmap": map[string]any(nil), "nilslice": []string(nil), "empty": []any{}}
copy := CloneTrusted(original).(map[string]any)
if !reflect.DeepEqual(copy, original) {
t.Fatalf("clone = %#v", copy)
}
copy[""].(map[string]any)["number"] = json.Number("1")
copy["typed"].([]string)[0] = "changed"
*copy["pointer"].(*trustedNamed) = "changed"
if original[""].(map[string]any)["number"] != json.Number("9007199254740993") || original["typed"].([]string)[0] != "original" || named != "value" {
t.Fatal("clone shares mutable storage")
}
if copy["nilmap"].(map[string]any) != nil || copy["nilslice"].([]string) != nil || copy["empty"].([]any) == nil {
t.Fatal("nil/empty shape changed")
}
}