internal/mcp/jsonschema: check for structure separately

Redo iteration. It was too subtle.

Write a separate recursive check for whether the schemas form a tree and
have any invalid nils.

While doing that, assign paths to schemas for better error messages,
even as we check structure. This also lets us detect re-occurring
schemas without a separate map.

Everything else, including the iterators, can now assume valid structure
and populated paths.

Change-Id: I90ec93d79d1d95c92258ad7afc94ef068fdf123f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/676376
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
Jonathan Amsterdam 2025-05-26 13:12:35 -04:00 • committed by Gopher Robot
commit b0d437f1c5

View file

@ -17,7 +17,6 @@ import (
"reflect"
"regexp"
"slices"
"strconv"
)
// A Schema is a JSON schema object.
@ -361,27 +360,12 @@ func Ptr[T any](x T) *T { return &x }
// every applies f preorder to every schema under s including s.
// The second argument to f is the path to the schema appended to the argument path.
// It stops when f returns false.
func (s *Schema) every(f func(*Schema, []string) bool, path []string) bool {
return s == nil ||
f(s, path) && s.everyChild(func(s *Schema, p []string) bool { return s.every(f, p) }, path)
func (s *Schema) every(f func(*Schema) bool) bool {
return f(s) && s.everyChild(func(s *Schema) bool { return s.every(f) })
}
// everyChild reports whether f is true for every immediate child schema of s.
// The second argument to f is the path to the schema appended to the argument path.
//
// It does not call f on nil-valued fields holding individual schemas, like Contains,
// because a nil value indicates that the field is absent.
// It does call f on nils when they occur in slices and maps, so those invalid values
// can be detected when the schema is validated.
func (s *Schema) everyChild(f func(*Schema, []string) bool, path []string) bool {
if s == nil {
return false
}
var (
schemaType = reflect.TypeFor[*Schema]()
schemaSliceType = reflect.TypeFor[[]*Schema]()
schemaMapType = reflect.TypeFor[map[string]*Schema]()
)
func (s *Schema) everyChild(f func(*Schema) bool) bool {
v := reflect.ValueOf(s)
for _, info := range schemaFieldInfos {
fv := v.Elem().FieldByIndex(info.sf.Index)
@ -389,25 +373,23 @@ func (s *Schema) everyChild(f func(*Schema, []string) bool, path []string) bool
case schemaType:
// A field that contains an individual schema. A nil is valid: it just means the field isn't present.
c := fv.Interface().(*Schema)
if c != nil && !f(c, append(path, info.jsonName)) {
if c != nil && !f(c) {
return false
}
case schemaSliceType:
slice := fv.Interface().([]*Schema)
// A field that contains a slice of schemas. Yield nils so we can check for their presence.
for i, c := range slice {
if !f(c, append(path, info.jsonName, strconv.Itoa(i))) {
for _, c := range slice {
if !f(c) {
return false
}
}
case schemaMapType:
// A field that is a map of schemas. Ditto about nils.
// Sort keys for determinism.
m := fv.Interface().(map[string]*Schema)
for _, k := range slices.Sorted(maps.Keys(m)) {
if !f(m[k], append(path, info.jsonName, escapeJSONPointerSegment(k))) {
if !f(m[k]) {
return false
}
}
@ -417,16 +399,21 @@ func (s *Schema) everyChild(f func(*Schema, []string) bool, path []string) bool
}
// all wraps every in an iterator.
func (s *Schema) all() iter.Seq2[*Schema, []string] {
return func(yield func(*Schema, []string) bool) { s.every(yield, nil) }
func (s *Schema) all() iter.Seq[*Schema] {
return func(yield func(*Schema) bool) { s.every(yield) }
}
// children wraps everyChild in an iterator.
func (s *Schema) children() iter.Seq2[*Schema, []string] {
var pathBuffer [4]string
return func(yield func(*Schema, []string) bool) { s.everyChild(yield, pathBuffer[:0]) }
func (s *Schema) children() iter.Seq[*Schema] {
return func(yield func(*Schema) bool) { s.everyChild(yield) }
}
var (
schemaType = reflect.TypeFor[*Schema]()
schemaSliceType = reflect.TypeFor[[]*Schema]()
schemaMapType = reflect.TypeFor[map[string]*Schema]()
)
type structFieldInfo struct {
sf reflect.StructField
jsonName string