diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go index e3cd3da..96bfbe8 100644 --- a/jsonschema/resolve.go +++ b/jsonschema/resolve.go @@ -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 diff --git a/jsonschema/resolve_test.go b/jsonschema/resolve_test.go index b790799..c345ef0 100644 --- a/jsonschema/resolve_test.go +++ b/jsonschema/resolve_test.go @@ -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) + } } }) } diff --git a/jsonschema/schema.go b/jsonschema/schema.go index c07dd68..1d8d4b7 100644 --- a/jsonschema/schema.go +++ b/jsonschema/schema.go @@ -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 }