Skip to content

Commit 1ed0076

Browse files
committed
remove base and uri
1 parent 5350ef0 commit 1ed0076

File tree

4 files changed

+52
-56
lines changed

4 files changed

+52
-56
lines changed

jsonschema/resolve.go

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ type Resolved struct {
3232
// resolvedInfo holds information specific to a schema that is computed by [Schema.Resolve].
3333
type resolvedInfo struct {
3434
s *Schema
35+
// The schema's base schema.
36+
// If the schema is the root or has an ID, its base is itself.
37+
// Otherwise, its base is the innermost enclosing schema whose base
38+
// is itself.
39+
// Intuitively, a base schema is one that can be referred to with a
40+
// fragmentless URI.
41+
base *Schema
42+
// The URI for the schema, if it is the root or has an ID.
43+
// Otherwise nil.
44+
// Invariants:
45+
// s.base.uri != nil.
46+
// s.base == s <=> s.uri != nil
47+
uri *url.URL
3548
// The schema to which Ref refers.
3649
resolvedRef *Schema
3750

@@ -140,17 +153,16 @@ func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
140153
return nil, err
141154
}
142155

143-
m, err := resolveURIs(s, baseURI)
144-
if err != nil {
156+
rs := &Resolved{root: s}
157+
if err := resolveURIs(rs, baseURI); err != nil {
145158
return nil, err
146159
}
147-
rs := &Resolved{root: s, resolvedURIs: m, resolvedInfo: map[*Schema]*resolvedInfo{}}
148160

149161
// Remember the schema by both the URI we loaded it from and its canonical name,
150162
// which may differ if the schema has an $id.
151163
// We must set the map before calling resolveRefs, or ref cycles will cause unbounded recursion.
152164
r.loaded[baseURI.String()] = rs
153-
r.loaded[s.uri.String()] = rs
165+
r.loaded[rs.resolvedInfo[s].uri.String()] = rs
154166

155167
if err := r.resolveRefs(rs); err != nil {
156168
return nil, err
@@ -195,10 +207,9 @@ func (root *Schema) checkStructure() error {
195207
}
196208
if s.path != "" {
197209
// We've seen s before.
198-
// The schema graph at root is not a tree, but it needs to
199-
// be because we assume a unique parent when we store a schema's base
200-
// in the Schema. A cycle would also put Schema.all into an infinite
201-
// recursion.
210+
// The schema graph at root is not a tree, but it needs to be because
211+
// a schema's base must be unique.
212+
// A cycle would also put Schema.all into an infinite recursion.
202213
return fmt.Errorf("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)",
203214
root, s.path, p)
204215
}
@@ -305,8 +316,6 @@ func (s *Schema) checkLocal(report func(error)) {
305316
// to baseURI.
306317
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
307318
// 8.2.1.
308-
309-
// TODO(jba): dynamicAnchors (§8.2.2)
310319
//
311320
// Every schema has a base URI and a parent base URI.
312321
//
@@ -336,11 +345,17 @@ func (s *Schema) checkLocal(report func(error)) {
336345
// allOf/1 http://b.com (absolute $id; doesn't matter that it's not under the loaded URI)
337346
// allOf/2 http://a.com/root.json (inherited from parent)
338347
// allOf/2/not http://a.com/root.json (inherited from parent)
339-
func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
340-
resolvedURIs := map[string]*Schema{}
341-
348+
func resolveURIs(rs *Resolved, baseURI *url.URL) error {
342349
var resolve func(s, base *Schema) error
343350
resolve = func(s, base *Schema) error {
351+
assert(rs.resolvedInfo[base] != nil, "base resolved info not set")
352+
info := rs.resolvedInfo[s]
353+
if info == nil {
354+
info = &resolvedInfo{s: s}
355+
rs.resolvedInfo[s] = info
356+
}
357+
baseURI := rs.resolvedInfo[base].uri
358+
344359
// ids are scoped to the root.
345360
if s.ID != "" {
346361
// A non-empty ID establishes a new base.
@@ -352,21 +367,21 @@ func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
352367
return fmt.Errorf("$id %s must not have a fragment", s.ID)
353368
}
354369
// The base URI for this schema is its $id resolved against the parent base.
355-
s.uri = base.uri.ResolveReference(idURI)
356-
if !s.uri.IsAbs() {
357-
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %s)", s.ID, s.base.uri)
370+
info.uri = baseURI.ResolveReference(idURI)
371+
if !info.uri.IsAbs() {
372+
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %s)", s.ID, baseURI)
358373
}
359-
resolvedURIs[s.uri.String()] = s
374+
rs.resolvedURIs[info.uri.String()] = s
360375
base = s // needed for anchors
361376
}
362-
s.base = base
377+
info.base = base
363378

364379
// Anchors and dynamic anchors are URI fragments that are scoped to their base.
365380
// We treat them as keys in a map stored within the schema.
366381
setAnchor := func(anchor string, dynamic bool) error {
367382
if anchor != "" {
368383
if _, ok := base.anchors[anchor]; ok {
369-
return fmt.Errorf("duplicate anchor %q in %s", anchor, base.uri)
384+
return fmt.Errorf("duplicate anchor %q in %s", anchor, baseURI)
370385
}
371386
if base.anchors == nil {
372387
base.anchors = map[string]anchorInfo{}
@@ -388,23 +403,21 @@ func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
388403
}
389404

390405
// Set the root URI to the base for now. If the root has an $id, this will change.
391-
root.uri = baseURI
392-
// The original base, even if changed, is still a valid way to refer to the root.
393-
resolvedURIs[baseURI.String()] = root
394-
if err := resolve(root, root); err != nil {
395-
return nil, err
406+
rs.resolvedInfo = map[*Schema]*resolvedInfo{
407+
rs.root: {s: rs.root, uri: baseURI},
396408
}
397-
return resolvedURIs, nil
409+
// The original base, even if changed, is still a valid way to refer to the root.
410+
rs.resolvedURIs = map[string]*Schema{baseURI.String(): rs.root}
411+
412+
return resolve(rs.root, rs.root)
398413
}
399414

400415
// resolveRefs replaces every ref in the schemas with the schema it refers to.
401416
// A reference that doesn't resolve within the schema may refer to some other schema
402417
// that needs to be loaded.
403418
func (r *resolver) resolveRefs(rs *Resolved) error {
404419
for s := range rs.root.all() {
405-
assert(rs.resolvedInfo[s] == nil, "schema resolved info already set")
406-
info := &resolvedInfo{s: s}
407-
rs.resolvedInfo[s] = info
420+
info := rs.resolvedInfo[s]
408421
if s.Ref != "" {
409422
refSchema, _, err := r.resolveRef(rs, s, s.Ref)
410423
if err != nil {
@@ -440,7 +453,8 @@ func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, d
440453
return nil, "", err
441454
}
442455
// URI-resolve the ref against the current base URI to get a complete URI.
443-
refURI = s.base.uri.ResolveReference(refURI)
456+
base := rs.resolvedInfo[s].base
457+
refURI = rs.resolvedInfo[base].uri.ResolveReference(refURI)
444458
// The non-fragment part of a ref URI refers to the base URI of some schema.
445459
// This part is the same for dynamic refs too: their non-fragment part resolves
446460
// lexically.

jsonschema/resolve_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@ func TestResolveURIs(t *testing.T) {
129129
if err != nil {
130130
t.Fatal(err)
131131
}
132-
got, err := resolveURIs(root, base)
133-
if err != nil {
132+
133+
rs := &Resolved{root: root}
134+
if err := resolveURIs(rs, base); err != nil {
134135
t.Fatal(err)
135136
}
136137

@@ -154,6 +155,7 @@ func TestResolveURIs(t *testing.T) {
154155
},
155156
}
156157

158+
got := rs.resolvedURIs
157159
gotKeys := slices.Sorted(maps.Keys(got))
158160
wantKeys := slices.Sorted(maps.Keys(wantIDs))
159161
if !slices.Equal(gotKeys, wantKeys) {

jsonschema/schema.go

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"iter"
1414
"maps"
1515
"math"
16-
"net/url"
1716
"reflect"
1817
"regexp"
1918
"slices"
@@ -130,22 +129,8 @@ type Schema struct {
130129
// Extra allows for additional keywords beyond those specified.
131130
Extra map[string]any `json:"-"`
132131

133-
// computed fields
134-
135-
// This schema's base schema.
136-
// If the schema is the root or has an ID, its base is itself.
137-
// Otherwise, its base is the innermost enclosing schema whose base
138-
// is itself.
139-
// Intuitively, a base schema is one that can be referred to with a
140-
// fragmentless URI.
141-
base *Schema
142-
143-
// The URI for the schema, if it is the root or has an ID.
144-
// Otherwise nil.
145-
// Invariants:
146-
// s.base.uri != nil.
147-
// s.base == s <=> s.uri != nil
148-
uri *url.URL
132+
// These fields are independent of arguments to Schema.Resolved,
133+
// though they are computed there.
149134

150135
// The JSON Pointer path from the root schema to here.
151136
// Used in errors.
@@ -176,13 +161,8 @@ type anchorInfo struct {
176161

177162
// String returns a short description of the schema.
178163
func (s *Schema) String() string {
179-
if s.uri != nil {
180-
if u := s.uri.String(); u != "" {
181-
return u
182-
}
183-
}
184164
if a := cmp.Or(s.Anchor, s.DynamicAnchor); a != "" {
185-
return fmt.Sprintf("%q, anchor %s", s.base.uri.String(), a)
165+
return fmt.Sprintf("anchor %s", a)
186166
}
187167
if s.path != "" {
188168
return s.path

jsonschema/validate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
214214
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
215215
var dynamicSchema *Schema
216216
for _, s := range st.stack {
217-
info, ok := s.base.anchors[info.dynamicRefAnchor]
217+
info, ok := st.rs.resolvedInfo[s].base.anchors[info.dynamicRefAnchor]
218218
if ok && info.dynamic {
219219
dynamicSchema = info.schema
220220
break
@@ -574,7 +574,7 @@ func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) {
574574
// on the stack.
575575
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
576576
for _, s := range st.stack {
577-
info, ok := s.base.anchors[info.dynamicRefAnchor]
577+
info, ok := st.rs.resolvedInfo[s].base.anchors[info.dynamicRefAnchor]
578578
if ok && info.dynamic {
579579
return info.schema, nil
580580
}

0 commit comments

Comments
 (0)