blob: 61a727686d6d27c168280e2dedebc354b306b1af [file] [log] [blame]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mcp
import (
"encoding/json"
"fmt"
"reflect"
"sync"
"golang.org/x/tools/internal/mcp/internal/util"
)
func assert(cond bool, msg string) {
if !cond {
panic(msg)
}
}
// marshalStructWithMap marshals its first argument to JSON, treating the field named
// mapField as an embedded map. The first argument must be a pointer to
// a struct. The underlying type of mapField must be a map[string]any, and it must have
// an "omitempty" json tag.
//
// For example, given this struct:
//
// type S struct {
// A int
// Extra map[string] any `json:,omitempty`
// }
//
// and this value:
//
// s := S{A: 1, Extra: map[string]any{"B": 2}}
//
// the call marshalJSONWithMap(s, "Extra") would return
//
// {"A": 1, "B": 2}
//
// It is an error if the map contains the same key as another struct field's
// JSON name.
//
// marshalStructWithMap calls json.Marshal on a value of type T, so T must not
// have a MarshalJSON method that calls this function, on pain of infinite regress.
//
// TODO: avoid this restriction on T by forcing it to marshal in a default way.
// See https://go.dev/play/p/EgXKJHxEx_R.
func marshalStructWithMap[T any](s *T, mapField string) ([]byte, error) {
// Marshal the struct and the map separately, and concatenate the bytes.
// This strategy is dramatically less complicated than
// constructing a synthetic struct or map with the combined keys.
if s == nil {
return []byte("null"), nil
}
s2 := *s
vMapField := reflect.ValueOf(&s2).Elem().FieldByName(mapField)
mapVal := vMapField.Interface().(map[string]any)
// Check for duplicates.
names := jsonNames(reflect.TypeFor[T]())
for key := range mapVal {
if names[key] {
return nil, fmt.Errorf("map key %q duplicates struct field", key)
}
}
// Clear the map field, relying on the omitempty tag to omit it.
vMapField.Set(reflect.Zero(vMapField.Type()))
structBytes, err := json.Marshal(s2)
if err != nil {
return nil, fmt.Errorf("marshalStructWithMap(%+v): %w", s, err)
}
if len(mapVal) == 0 {
return structBytes, nil
}
mapBytes, err := json.Marshal(mapVal)
if err != nil {
return nil, err
}
if len(structBytes) == 2 { // must be "{}"
return mapBytes, nil
}
// "{X}" + "{Y}" => "{X,Y}"
res := append(structBytes[:len(structBytes)-1], ',')
res = append(res, mapBytes[1:]...)
return res, nil
}
// unmarshalStructWithMap is the inverse of marshalStructWithMap.
// T has the same restrictions as in that function.
func unmarshalStructWithMap[T any](data []byte, v *T, mapField string) error {
// Unmarshal into the struct, ignoring unknown fields.
if err := json.Unmarshal(data, v); err != nil {
return err
}
// Unmarshal into the map.
m := map[string]any{}
if err := json.Unmarshal(data, &m); err != nil {
return err
}
// Delete from the map the fields of the struct.
for n := range jsonNames(reflect.TypeFor[T]()) {
delete(m, n)
}
if len(m) != 0 {
reflect.ValueOf(v).Elem().FieldByName(mapField).Set(reflect.ValueOf(m))
}
return nil
}
var jsonNamesMap sync.Map // from reflect.Type to map[string]bool
// jsonNames returns the set of JSON object keys that t will marshal into.
// t must be a struct type.
func jsonNames(t reflect.Type) map[string]bool {
// Lock not necessary: at worst we'll duplicate work.
if val, ok := jsonNamesMap.Load(t); ok {
return val.(map[string]bool)
}
m := map[string]bool{}
for i := range t.NumField() {
info := util.FieldJSONInfo(t.Field(i))
if !info.Omit {
m[info.Name] = true
}
}
jsonNamesMap.Store(t, m)
return m
}