s.every now visits the entire subtree of schemas at s, not only the immediate children of s. This re-introduces a tower: now of closures instead of iterators. We could remove it having everyChild call every recursively instead of returning, just passing f down all the way unwrapped. But this is going to complicate the recursive function inside resolveURIs, which needs to pass parent context down. It would have the same problem that ast.Inspect has, and we'd have to adopt the same solution, maintaining a stack explicitly. I feel it's cleaner to keep the recursion simple at the cost of building several closures. To do otherwise would be premature optimization. Change-Id: Ic453cdaac482da8147dafc34b0117d25458d1c9d Reviewed-on: https://go-review.googlesource.com/c/tools/+/670675 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Alan Donovan <adonovan@google.com>
128 lines
3.7 KiB
Go
128 lines
3.7 KiB
Go
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
|
// Use of this source code is governed by an MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package jsonschema
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"testing"
|
|
)
|
|
|
|
func TestGoRoundTrip(t *testing.T) {
|
|
// Verify that Go representations round-trip.
|
|
for _, s := range []*Schema{
|
|
{Type: "null"},
|
|
{Types: []string{"null", "number"}},
|
|
{Type: "string", MinLength: Ptr(20)},
|
|
{Minimum: Ptr(20.0)},
|
|
{Items: &Schema{Type: "integer"}},
|
|
{Const: Ptr(any(0))},
|
|
{Const: Ptr(any(nil))},
|
|
{Const: Ptr(any([]int{}))},
|
|
{Const: Ptr(any(map[string]any{}))},
|
|
} {
|
|
data, err := json.Marshal(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Logf("marshal: %s", data)
|
|
var got *Schema
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !Equal(got, s) {
|
|
t.Errorf("got %+v, want %+v", got, s)
|
|
if got.Const != nil && s.Const != nil {
|
|
t.Logf("Consts: got %#v (%[1]T), want %#v (%[2]T)", *got.Const, *s.Const)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestJSONRoundTrip(t *testing.T) {
|
|
// Verify that JSON texts for schemas marshal into equivalent forms.
|
|
// We don't expect everything to round-trip perfectly. For example, "true" and "false"
|
|
// will turn into their object equivalents.
|
|
// But most things should.
|
|
// Some of these cases test Schema.{UnM,M}arshalJSON.
|
|
// Most of others follow from the behavior of encoding/json, but they are still
|
|
// valuable as regression tests of this package's behavior.
|
|
for _, tt := range []struct {
|
|
in, want string
|
|
}{
|
|
{`true`, `{}`}, // boolean schemas become object schemas
|
|
{`false`, `{"not":{}}`},
|
|
{`{"type":"", "enum":null}`, `{}`}, // empty fields are omitted
|
|
{`{"minimum":1}`, `{"minimum":1}`},
|
|
{`{"minimum":1.0}`, `{"minimum":1}`}, // floating-point integers lose their fractional part
|
|
{`{"minLength":1.0}`, `{"minLength":1}`}, // some floats are unmarshaled into ints, but you can't tell
|
|
{
|
|
// map keys are sorted
|
|
`{"$vocabulary":{"b":true, "a":false}}`,
|
|
`{"$vocabulary":{"a":false,"b":true}}`,
|
|
},
|
|
{`{"unk":0}`, `{}`}, // unknown fields are dropped, unfortunately
|
|
} {
|
|
var s Schema
|
|
if err := json.Unmarshal([]byte(tt.in), &s); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := json.Marshal(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := string(data); got != tt.want {
|
|
t.Errorf("%s:\ngot %s\nwant %s", tt.in, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalErrors(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
in string
|
|
want string // error must match this regexp
|
|
}{
|
|
{`1`, "cannot unmarshal number"},
|
|
{`{"type":1}`, `invalid value for "type"`},
|
|
{`{"minLength":1.5}`, `not an integer value`},
|
|
{`{"maxLength":1.5}`, `not an integer value`},
|
|
{`{"minItems":1.5}`, `not an integer value`},
|
|
{`{"maxItems":1.5}`, `not an integer value`},
|
|
{`{"minProperties":1.5}`, `not an integer value`},
|
|
{`{"maxProperties":1.5}`, `not an integer value`},
|
|
{`{"minContains":1.5}`, `not an integer value`},
|
|
{`{"maxContains":1.5}`, `not an integer value`},
|
|
{fmt.Sprintf(`{"maxContains":%d}`, int64(math.MaxInt32+1)), `out of range`},
|
|
{`{"minLength":9e99}`, `cannot be unmarshaled`},
|
|
{`{"minLength":"1.5"}`, `not a number`},
|
|
} {
|
|
var s Schema
|
|
err := json.Unmarshal([]byte(tt.in), &s)
|
|
if err == nil {
|
|
t.Fatalf("%s: no error but expected one", tt.in)
|
|
}
|
|
if !regexp.MustCompile(tt.want).MatchString(err.Error()) {
|
|
t.Errorf("%s: error %q does not match %q", tt.in, err, tt.want)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func TestEvery(t *testing.T) {
|
|
// Schema.every should visit all descendants of a schema, not just the immediate ones.
|
|
s := &Schema{
|
|
Items: &Schema{
|
|
Items: &Schema{},
|
|
},
|
|
}
|
|
want := 3
|
|
got := 0
|
|
s.every(func(*Schema) bool { got++; return true })
|
|
if got != want {
|
|
t.Errorf("got %d, want %d", got, want)
|
|
}
|
|
}
|