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>
138 lines
3.7 KiB
Go
138 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"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The test for validation uses the official test suite, expressed as a set of JSON files.
|
|
// Each file is an array of group objects.
|
|
|
|
// A testGroup consists of a schema and some tests on it.
|
|
type testGroup struct {
|
|
Description string
|
|
Schema *Schema
|
|
Tests []test
|
|
}
|
|
|
|
// A test consists of a JSON instance to be validated and the expected result.
|
|
type test struct {
|
|
Description string
|
|
Data any
|
|
Valid bool
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
files, err := filepath.Glob(filepath.FromSlash("testdata/draft2020-12/*.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(files) == 0 {
|
|
t.Fatal("no files")
|
|
}
|
|
for _, file := range files {
|
|
base := filepath.Base(file)
|
|
t.Run(base, func(t *testing.T) {
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
dec := json.NewDecoder(f)
|
|
var groups []testGroup
|
|
if err := dec.Decode(&groups); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, g := range groups {
|
|
t.Run(g.Description, func(t *testing.T) {
|
|
rs, err := g.Schema.Resolve("", loadRemote)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range g.Tests {
|
|
t.Run(test.Description, func(t *testing.T) {
|
|
err = rs.Validate(test.Data)
|
|
if err != nil && test.Valid {
|
|
t.Errorf("wanted success, but failed with: %v", err)
|
|
}
|
|
if err == nil && !test.Valid {
|
|
t.Error("succeeded but wanted failure")
|
|
}
|
|
if t.Failed() {
|
|
t.Errorf("schema: %s", g.Schema.json())
|
|
t.Fatalf("instance: %v (%[1]T)", test.Data)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStructInstance(t *testing.T) {
|
|
instance := struct {
|
|
I int
|
|
B bool `json:"b"`
|
|
u int
|
|
}{1, true, 0}
|
|
|
|
// The instance fails for all of these schemas, demonstrating that it
|
|
// was processed correctly.
|
|
for _, schema := range []*Schema{
|
|
{MinProperties: Ptr(3)},
|
|
{MaxProperties: Ptr(1)},
|
|
{Required: []string{"i"}}, // the name is "I"
|
|
{Required: []string{"B"}}, // the name is "b"
|
|
{PropertyNames: &Schema{MinLength: Ptr(2)}},
|
|
{Properties: map[string]*Schema{"b": {Type: "number"}}},
|
|
{Required: []string{"I"}, AdditionalProperties: falseSchema()},
|
|
{DependentRequired: map[string][]string{"b": {"u"}}},
|
|
{DependentSchemas: map[string]*Schema{"b": falseSchema()}},
|
|
{UnevaluatedProperties: falseSchema()},
|
|
} {
|
|
res, err := schema.Resolve("", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = res.Validate(instance)
|
|
if err == nil {
|
|
t.Errorf("succeeded but wanted failure; schema = %s", schema.json())
|
|
}
|
|
}
|
|
}
|
|
|
|
// loadRemote loads a remote reference used in the test suite.
|
|
func loadRemote(uri *url.URL) (*Schema, error) {
|
|
// Anything with localhost:1234 refers to the remotes directory in the test suite repo.
|
|
if uri.Host == "localhost:1234" {
|
|
return loadSchemaFromFile(filepath.FromSlash(filepath.Join("testdata/remotes", uri.Path)))
|
|
}
|
|
// One test needs the meta-schema files.
|
|
const metaPrefix = "https://json-schema.org/draft/2020-12/"
|
|
if after, ok := strings.CutPrefix(uri.String(), metaPrefix); ok {
|
|
return loadSchemaFromFile(filepath.FromSlash("meta-schemas/draft2020-12/" + after + ".json"))
|
|
}
|
|
return nil, fmt.Errorf("don't know how to load %s", uri)
|
|
}
|
|
|
|
func loadSchemaFromFile(filename string) (*Schema, error) {
|
|
data, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var s Schema
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return nil, fmt.Errorf("unmarshaling JSON at %s: %w", filename, err)
|
|
}
|
|
return &s, nil
|
|
}
|