A dynamic reference is like a dynamic variable in a programming language, with two twists: - There must be a dynamic anchor in lexical scope for the ref to resolve. (Think of it as declaring the dynamic variable.) - The ref resolves to the schema highest on the stack, not lowest. That feels odd, but it turns out to work for the intended purpose, which is to provide extension points for additional features. There is some distracting code motion and refactoring in this CL: resolveRefs was moved down in the file, and resolution of a single ref was split off. Change-Id: I17da6052e0782fc540d14b8a41b06a2645923872 Reviewed-on: https://go-review.googlesource.com/c/tools/+/674155 Auto-Submit: Jonathan Amsterdam <jba@google.com> Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
165 lines
3.7 KiB
Go
165 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 (
|
|
"errors"
|
|
"maps"
|
|
"net/url"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCheckLocal(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
s *Schema
|
|
want string // error must be non-nil and match this regexp
|
|
}{
|
|
{nil, "nil"},
|
|
{
|
|
&Schema{Pattern: "]["},
|
|
"regexp",
|
|
},
|
|
{
|
|
&Schema{PatternProperties: map[string]*Schema{"*": nil}},
|
|
"regexp",
|
|
},
|
|
} {
|
|
_, err := tt.s.Resolve("", nil)
|
|
if err == nil {
|
|
t.Errorf("%s: unexpectedly passed", tt.s.json())
|
|
continue
|
|
}
|
|
if !regexp.MustCompile(tt.want).MatchString(err.Error()) {
|
|
t.Errorf("%s: did not match\nerror: %s\nregexp: %s",
|
|
tt.s.json(), err, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSchemaNonTree(t *testing.T) {
|
|
run := func(s *Schema, kind string) {
|
|
err := s.check()
|
|
if err == nil || !strings.Contains(err.Error(), "tree") {
|
|
t.Fatalf("did not detect %s", kind)
|
|
}
|
|
}
|
|
|
|
s := &Schema{Type: "number"}
|
|
run(&Schema{Items: s, Contains: s}, "DAG")
|
|
|
|
root := &Schema{Items: s}
|
|
s.Items = root
|
|
run(root, "cycle")
|
|
}
|
|
|
|
func TestResolveURIs(t *testing.T) {
|
|
for _, baseURI := range []string{"", "http://a.com"} {
|
|
t.Run(baseURI, func(t *testing.T) {
|
|
root := &Schema{
|
|
ID: "http://b.com",
|
|
Items: &Schema{
|
|
ID: "/foo.json",
|
|
},
|
|
Contains: &Schema{
|
|
ID: "/bar.json",
|
|
Anchor: "a",
|
|
DynamicAnchor: "da",
|
|
Items: &Schema{
|
|
Anchor: "b",
|
|
Items: &Schema{
|
|
// An ID shouldn't be a query param, but this tests
|
|
// resolving an ID with its parent.
|
|
ID: "?items",
|
|
Anchor: "c",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
base, err := url.Parse(baseURI)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := resolveURIs(root, base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wantIDs := map[string]*Schema{
|
|
baseURI: root,
|
|
"http://b.com/foo.json": root.Items,
|
|
"http://b.com/bar.json": root.Contains,
|
|
"http://b.com/bar.json?items": root.Contains.Items.Items,
|
|
}
|
|
if baseURI != root.ID {
|
|
wantIDs[root.ID] = root
|
|
}
|
|
wantAnchors := map[*Schema]map[string]anchorInfo{
|
|
root.Contains: {
|
|
"a": anchorInfo{root.Contains, false},
|
|
"da": anchorInfo{root.Contains, true},
|
|
"b": anchorInfo{root.Contains.Items, false},
|
|
},
|
|
root.Contains.Items.Items: {
|
|
"c": anchorInfo{root.Contains.Items.Items, false},
|
|
},
|
|
}
|
|
|
|
gotKeys := slices.Sorted(maps.Keys(got))
|
|
wantKeys := slices.Sorted(maps.Keys(wantIDs))
|
|
if !slices.Equal(gotKeys, wantKeys) {
|
|
t.Errorf("ID keys:\ngot %q\nwant %q", gotKeys, wantKeys)
|
|
}
|
|
if !maps.Equal(got, wantIDs) {
|
|
t.Errorf("IDs:\ngot %+v\n\nwant %+v", got, wantIDs)
|
|
}
|
|
for s := range root.all() {
|
|
if want := wantAnchors[s]; want != nil {
|
|
if got := s.anchors; !maps.Equal(got, want) {
|
|
t.Errorf("anchors:\ngot %+v\n\nwant %+v", got, want)
|
|
}
|
|
} else if s.anchors != nil {
|
|
t.Errorf("non-nil anchors for %s", s)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRefCycle(t *testing.T) {
|
|
// Verify that cycles of refs are OK.
|
|
// The test suite doesn't check this, suprisingly.
|
|
schemas := map[string]*Schema{
|
|
"root": {Ref: "a"},
|
|
"a": {Ref: "b"},
|
|
"b": {Ref: "a"},
|
|
}
|
|
|
|
loader := func(uri *url.URL) (*Schema, error) {
|
|
s, ok := schemas[uri.Path[1:]]
|
|
if !ok {
|
|
return nil, errors.New("not found")
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
rs, err := schemas["root"].Resolve("", loader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := func(s *Schema, key string) {
|
|
t.Helper()
|
|
if s.resolvedRef != schemas[key] {
|
|
t.Errorf("%s resolvedRef != schemas[%q]", s.json(), key)
|
|
}
|
|
}
|
|
|
|
check(rs.root, "a")
|
|
check(schemas["a"], "b")
|
|
check(schemas["b"], "a")
|
|
}
|