Centralize trusted JSON value cloning
This commit is contained in:
83
convert.go
83
convert.go
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
97
internal/jsonvalue/trusted.go
Normal file
97
internal/jsonvalue/trusted.go
Normal 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
|
||||
}
|
||||
}
|
||||
27
internal/jsonvalue/trusted_test.go
Normal file
27
internal/jsonvalue/trusted_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user