jsonschema: resolve anchors

Change-Id: Ibe7d8df0a46d3e3a8b5d0651b1113a54e756c7df
Reviewed-on: https://go-review.googlesource.com/c/tools/+/669998
Reviewed-by: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Jonathan Amsterdam 2025-04-26 11:24:37 -04:00
commit 5cc4e6ea54
3 changed files with 34 additions and 8 deletions

View file

@ -122,8 +122,7 @@ func (s *Schema) checkLocal(report func(error)) {
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
// 8.2.1.
// TODO(jba): anchors (section 8.2.2)
// TODO(jba): dynamicAnchors (ditto)
// TODO(jba): dynamicAnchors (§8.2.2)
//
// Every schema has a base URI and a parent base URI.
//
@ -180,6 +179,18 @@ func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
base = s // needed for anchors
}
// Anchors are URI fragments that are scoped to their base.
// We treat them as keys in a map stored within the schema.
if s.Anchor != "" {
if base.anchors[s.Anchor] != nil {
return fmt.Errorf("duplicate anchor %q in %s", s.Anchor, base.baseURI)
}
if base.anchors == nil {
base.anchors = map[string]*Schema{}
}
base.anchors[s.Anchor] = s
}
for c := range s.children() {
if err := resolve(c, base); err != nil {
return err

View file

@ -70,23 +70,36 @@ func TestResolveURIs(t *testing.T) {
t.Fatal(err)
}
want := map[string]*Schema{
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 {
want[root.ID] = root
wantIDs[root.ID] = root
}
wantAnchors := map[*Schema]map[string]*Schema{
root.Contains: {"a": root.Contains, "b": root.Contains.Items},
root.Contains.Items.Items: {"c": root.Contains.Items.Items},
}
gotKeys := slices.Sorted(maps.Keys(got))
wantKeys := slices.Sorted(maps.Keys(want))
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, want) {
t.Errorf("IDs:\ngot %+v\n\nwant %+v", got, want)
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)
}
}
})
}

View file

@ -114,7 +114,9 @@ type Schema struct {
// Otherwise, the base URI is the ID resolved against the parent's baseURI.
// The parent base URI at top level is where the schema was loaded from, or
// if not loaded, then it should be provided to Schema.Resolve.
baseURI *url.URL
baseURI *url.URL
// map from anchors to subschemas
anchors map[string]*Schema
pattern *regexp.Regexp
patternProperties map[*regexp.Regexp]*Schema
}