Skip to content

Commit e074366

Browse files
committed
Fully address issue daveshanley/vacuum#798
$id is now handled correctly when resolving schemas.
1 parent 85c6253 commit e074366

10 files changed

Lines changed: 441 additions & 77 deletions

datamodel/low/base/schema_proxy.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ type SchemaProxy struct {
6868
func (sp *SchemaProxy) Build(ctx context.Context, key, value *yaml.Node, idx *index.SpecIndex) error {
6969
sp.kn = key
7070
sp.idx = idx
71-
sp.ctx = ctx
7271

7372
// transform sibling refs to allOf structure if enabled and applicable
7473
// this ensures sp.vn contains the pre-transformed YAML as the source of truth
@@ -87,6 +86,7 @@ func (sp *SchemaProxy) Build(ctx context.Context, key, value *yaml.Node, idx *in
8786
}
8887

8988
sp.vn = transformedValue
89+
sp.ctx = applySchemaIdScope(ctx, value, idx)
9090

9191
// handle reference detection
9292
if !wasTransformed {
@@ -102,6 +102,33 @@ func (sp *SchemaProxy) Build(ctx context.Context, key, value *yaml.Node, idx *in
102102
return nil
103103
}
104104

105+
func applySchemaIdScope(ctx context.Context, node *yaml.Node, idx *index.SpecIndex) context.Context {
106+
if node == nil {
107+
return ctx
108+
}
109+
scope := index.GetSchemaIdScope(ctx)
110+
idValue := index.FindSchemaIdInNode(node)
111+
if idValue == "" {
112+
return ctx
113+
}
114+
if scope == nil {
115+
base := ""
116+
if idx != nil {
117+
base = idx.GetSpecAbsolutePath()
118+
}
119+
scope = index.NewSchemaIdScope(base)
120+
ctx = index.WithSchemaIdScope(ctx, scope)
121+
}
122+
parentBase := scope.BaseUri
123+
resolved, err := index.ResolveSchemaId(idValue, parentBase)
124+
if err != nil || resolved == "" {
125+
resolved = idValue
126+
}
127+
updated := scope.Copy()
128+
updated.PushId(resolved)
129+
return index.WithSchemaIdScope(ctx, updated)
130+
}
131+
105132
// Schema will first check if this SchemaProxy has already rendered the schema, and return the pre-rendered version
106133
// first.
107134
//

datamodel/low/extraction_functions.go

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -119,43 +119,60 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S
119119
root.Line, root.Column), ctx
120120
}
121121

122+
origRef := rv
123+
resolvedRef := rv
124+
if scope := index.GetSchemaIdScope(ctx); scope != nil && scope.BaseUri != "" {
125+
if resolved, err := index.ResolveRefAgainstSchemaId(rv, scope); err == nil && resolved != "" {
126+
resolvedRef = resolved
127+
}
128+
}
129+
searchRefs := []string{origRef}
130+
if resolvedRef != origRef {
131+
searchRefs = append(searchRefs, resolvedRef)
132+
}
133+
122134
// run through everything and return as soon as we find a match.
123135
// this operates as fast as possible as ever
124136
collections := generateIndexCollection(idx)
125137
var found map[string]*index.Reference
126138
for _, collection := range collections {
127139
found = collection()
128-
if found != nil && found[rv] != nil {
129-
foundRef := found[rv]
130-
foundIndex := idx
131-
if foundRef.Index != nil {
132-
foundIndex = foundRef.Index
133-
}
134-
if foundIndex != nil && foundRef.RemoteLocation != "" &&
135-
foundIndex.GetSpecAbsolutePath() != foundRef.RemoteLocation {
136-
if rolo := foundIndex.GetRolodex(); rolo != nil {
137-
for _, candidate := range append(rolo.GetIndexes(), rolo.GetRootIndex()) {
138-
if candidate == nil {
139-
continue
140-
}
141-
if candidate.GetSpecAbsolutePath() == foundRef.RemoteLocation {
142-
foundIndex = candidate
143-
break
140+
if found != nil {
141+
for _, candidate := range searchRefs {
142+
if found[candidate] == nil {
143+
continue
144+
}
145+
foundRef := found[candidate]
146+
foundIndex := idx
147+
if foundRef.Index != nil {
148+
foundIndex = foundRef.Index
149+
}
150+
if foundIndex != nil && foundRef.RemoteLocation != "" &&
151+
foundIndex.GetSpecAbsolutePath() != foundRef.RemoteLocation {
152+
if rolo := foundIndex.GetRolodex(); rolo != nil {
153+
for _, candidateIdx := range append(rolo.GetIndexes(), rolo.GetRootIndex()) {
154+
if candidateIdx == nil {
155+
continue
156+
}
157+
if candidateIdx.GetSpecAbsolutePath() == foundRef.RemoteLocation {
158+
foundIndex = candidateIdx
159+
break
160+
}
144161
}
145162
}
146163
}
147-
}
148-
foundCtx := ctx
149-
if foundRef.RemoteLocation != "" {
150-
foundCtx = context.WithValue(foundCtx, index.CurrentPathKey, foundRef.RemoteLocation)
151-
}
152-
// if this is a ref node, we need to keep diving
153-
// until we hit something that isn't a ref.
154-
if jh, _, _ := utils.IsNodeRefValue(foundRef.Node); jh {
155-
// if this node is circular, stop drop and roll.
156-
if !IsCircular(foundRef.Node, foundIndex) && foundRef.Node != root {
157-
return LocateRefNodeWithContext(foundCtx, foundRef.Node, foundIndex)
158-
} else {
164+
foundCtx := ctx
165+
if foundRef.RemoteLocation != "" {
166+
foundCtx = context.WithValue(foundCtx, index.CurrentPathKey, foundRef.RemoteLocation)
167+
}
168+
foundCtx = applyResolvedSchemaIdScope(foundCtx, foundRef, foundIndex)
169+
// if this is a ref node, we need to keep diving
170+
// until we hit something that isn't a ref.
171+
if jh, _, _ := utils.IsNodeRefValue(foundRef.Node); jh {
172+
// if this node is circular, stop drop and roll.
173+
if !IsCircular(foundRef.Node, foundIndex) && foundRef.Node != root {
174+
return LocateRefNodeWithContext(foundCtx, foundRef.Node, foundIndex)
175+
}
159176

160177
crr := GetCircularReferenceResult(foundRef.Node, foundIndex)
161178
jp := ""
@@ -168,11 +185,13 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S
168185
foundRef.Node.Line,
169186
foundRef.Node.Column), foundCtx
170187
}
188+
return utils.NodeAlias(foundRef.Node), foundIndex, nil, foundCtx
171189
}
172-
return utils.NodeAlias(foundRef.Node), foundIndex, nil, foundCtx
173190
}
174191
}
175192

193+
rv = resolvedRef
194+
176195
// Obtain the absolute filepath/URL of the spec in which we are trying to
177196
// resolve the reference value [rv] from. It's either available from the
178197
// index or passed down through context.
@@ -281,6 +300,7 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S
281300

282301
foundRef, fIdx, newCtx := idx.SearchIndexForReferenceWithContext(ctx, rv)
283302
if foundRef != nil {
303+
newCtx = applyResolvedSchemaIdScope(newCtx, foundRef, fIdx)
284304
return utils.NodeAlias(foundRef.Node), fIdx, nil, newCtx
285305
}
286306

@@ -303,6 +323,40 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S
303323
return nil, idx, nil, ctx
304324
}
305325

326+
func applyResolvedSchemaIdScope(ctx context.Context, ref *index.Reference, idx *index.SpecIndex) context.Context {
327+
if ref == nil || ref.Node == nil {
328+
return ctx
329+
}
330+
idValue := index.FindSchemaIdInNode(ref.Node)
331+
if idValue == "" {
332+
return ctx
333+
}
334+
335+
scope := index.GetSchemaIdScope(ctx)
336+
base := ""
337+
if ref.RemoteLocation != "" {
338+
base = ref.RemoteLocation
339+
} else if idx != nil {
340+
base = idx.GetSpecAbsolutePath()
341+
}
342+
if scope == nil {
343+
scope = index.NewSchemaIdScope(base)
344+
ctx = index.WithSchemaIdScope(ctx, scope)
345+
}
346+
347+
parentBase := scope.BaseUri
348+
if parentBase == "" {
349+
parentBase = base
350+
}
351+
resolved, err := index.ResolveSchemaId(idValue, parentBase)
352+
if err != nil || resolved == "" {
353+
resolved = idValue
354+
}
355+
updated := scope.Copy()
356+
updated.PushId(resolved)
357+
return index.WithSchemaIdScope(ctx, updated)
358+
}
359+
306360
// LocateRefNode will perform a complete lookup for a $ref node. This function searches the entire index for
307361
// the reference being supplied. If there is a match found, the reference *yaml.Node is returned.
308362
func LocateRefNode(root *yaml.Node, idx *index.SpecIndex) (*yaml.Node, *index.SpecIndex, error) {

index/extract_refs.go

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,6 @@ import (
2626
// Set LIBOPENAPI_LEGACY_REF_ORDER=true to use the old non-deterministic ordering.
2727
var preserveLegacyRefOrder = os.Getenv("LIBOPENAPI_LEGACY_REF_ORDER") == "true"
2828

29-
// findSchemaIdInNode looks for a $id key in a mapping node and returns its value.
30-
// Returns empty string if not found or if the node is not a mapping.
31-
func findSchemaIdInNode(node *yaml.Node) string {
32-
if node == nil || node.Kind != yaml.MappingNode {
33-
return ""
34-
}
35-
for i := 0; i < len(node.Content)-1; i += 2 {
36-
if node.Content[i].Value == "$id" && utils.IsNodeStringValue(node.Content[i+1]) {
37-
return node.Content[i+1].Value
38-
}
39-
}
40-
return ""
41-
}
42-
4329
// indexedRef pairs a resolved reference with its original input position for deterministic ordering.
4430
type indexedRef struct {
4531
ref *Reference
@@ -67,7 +53,7 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node
6753
// Check if THIS node has a $id and update scope for processing children
6854
// This must happen before iterating children so they see the updated scope
6955
if node.Kind == yaml.MappingNode {
70-
if nodeId := findSchemaIdInNode(node); nodeId != "" {
56+
if nodeId := FindSchemaIdInNode(node); nodeId != "" {
7157
resolvedNodeId, _ := ResolveSchemaId(nodeId, parentBaseUri)
7258
if resolvedNodeId == "" {
7359
resolvedNodeId = nodeId
@@ -307,6 +293,10 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node
307293
if len(node.Content) > i+1 {
308294

309295
value := node.Content[i+1].Value
296+
schemaIdBase := ""
297+
if scope != nil && len(scope.Chain) > 0 {
298+
schemaIdBase = scope.BaseUri
299+
}
310300
// extract last path segment without allocating a full slice
311301
lastSlash := strings.LastIndexByte(value, '/')
312302
var name string
@@ -424,6 +414,13 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node
424414
}
425415
}
426416

417+
if fullDefinitionPath == "" && value != "" {
418+
fullDefinitionPath = value
419+
}
420+
if componentName == "" {
421+
componentName = value
422+
}
423+
427424
_, p := utils.ConvertComponentIdIntoFriendlyPathSearch(componentName)
428425

429426
// check for sibling properties
@@ -444,6 +441,8 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node
444441
ParentNode: parent,
445442
FullDefinition: fullDefinitionPath,
446443
Definition: componentName,
444+
RawRef: value,
445+
SchemaIdBase: schemaIdBase,
447446
Name: name,
448447
Node: node,
449448
KeyNode: node.Content[i+1],
@@ -489,6 +488,8 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node
489488
ParentNode: parent,
490489
FullDefinition: fullDefinitionPath,
491490
Definition: ref.Definition,
491+
RawRef: ref.RawRef,
492+
SchemaIdBase: ref.SchemaIdBase,
492493
Name: ref.Name,
493494
Node: &copiedNode,
494495
KeyNode: node.Content[i],
@@ -1019,7 +1020,16 @@ func (index *SpecIndex) locateRef(ctx context.Context, ref *Reference) *Referenc
10191020
}
10201021

10211022
if located == nil {
1022-
return nil
1023+
rawRef := ref.RawRef
1024+
if rawRef == "" {
1025+
rawRef = ref.FullDefinition
1026+
}
1027+
normalizedRef := resolveRefWithSchemaBase(rawRef, ref.SchemaIdBase)
1028+
if resolved := index.ResolveRefViaSchemaId(normalizedRef); resolved != nil {
1029+
located = resolved
1030+
} else {
1031+
return nil
1032+
}
10231033
}
10241034

10251035
// Extract KeyNode - yamlpath API returns subnodes only, so we need to

index/find_component.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ func (index *SpecIndex) FindComponent(ctx context.Context, componentId string) *
2626
return nil
2727
}
2828

29+
if strings.HasPrefix(componentId, "/") {
30+
baseUri, fragment := SplitRefFragment(componentId)
31+
if resolved := index.resolveRefViaSchemaIdPath(baseUri); resolved != nil {
32+
if fragment != "" && resolved.Node != nil {
33+
if fragmentNode := navigateToFragment(resolved.Node, fragment); fragmentNode != nil {
34+
resolved.Node = fragmentNode
35+
}
36+
}
37+
return resolved
38+
}
39+
}
40+
2941
uri := strings.Split(componentId, "#/")
3042
if len(uri) == 2 {
3143
if uri[0] != "" {

index/index_model.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,29 @@ import (
2121
// Reference is a wrapper around *yaml.Node results to make things more manageable when performing
2222
// algorithms on data models. the *yaml.Node def is just a bit too low level for tracking state.
2323
type Reference struct {
24-
FullDefinition string `json:"fullDefinition,omitempty"`
25-
Definition string `json:"definition,omitempty"`
26-
Name string `json:"name,omitempty"`
27-
Node *yaml.Node `json:"-"`
28-
KeyNode *yaml.Node `json:"-"`
29-
ParentNode *yaml.Node `json:"-"`
30-
ParentNodeSchemaType string `json:"-"` // used to determine if the parent node is an array or not.
31-
ParentNodeTypes []string `json:"-"` // used to capture deep journeys, if any item is an array, we need to know.
32-
Resolved bool `json:"-"`
33-
Circular bool `json:"-"`
34-
Seen bool `json:"-"`
35-
IsRemote bool `json:"isRemote,omitempty"`
36-
IsExtensionRef bool `json:"isExtensionRef,omitempty"` // true if ref is under an x-* extension path
37-
Index *SpecIndex `json:"-"` // index that contains this reference.
38-
RemoteLocation string `json:"remoteLocation,omitempty"`
39-
Path string `json:"path,omitempty"` // this won't always be available.
40-
RequiredRefProperties map[string][]string `json:"requiredProperties,omitempty"` // definition names (eg, #/definitions/One) to a list of required properties on this definition which reference that definition
41-
HasSiblingProperties bool `json:"-"` // indicates if ref has sibling properties
42-
SiblingProperties map[string]*yaml.Node `json:"-"` // stores sibling property nodes
43-
SiblingKeys []*yaml.Node `json:"-"` // stores sibling key nodes
44-
In string `json:"-"` // parameter location (path, query, header, cookie) - cached for performance
24+
FullDefinition string `json:"fullDefinition,omitempty"`
25+
Definition string `json:"definition,omitempty"`
26+
RawRef string `json:"-"`
27+
SchemaIdBase string `json:"-"`
28+
Name string `json:"name,omitempty"`
29+
Node *yaml.Node `json:"-"`
30+
KeyNode *yaml.Node `json:"-"`
31+
ParentNode *yaml.Node `json:"-"`
32+
ParentNodeSchemaType string `json:"-"` // used to determine if the parent node is an array or not.
33+
ParentNodeTypes []string `json:"-"` // used to capture deep journeys, if any item is an array, we need to know.
34+
Resolved bool `json:"-"`
35+
Circular bool `json:"-"`
36+
Seen bool `json:"-"`
37+
IsRemote bool `json:"isRemote,omitempty"`
38+
IsExtensionRef bool `json:"isExtensionRef,omitempty"` // true if ref is under an x-* extension path
39+
Index *SpecIndex `json:"-"` // index that contains this reference.
40+
RemoteLocation string `json:"remoteLocation,omitempty"`
41+
Path string `json:"path,omitempty"` // this won't always be available.
42+
RequiredRefProperties map[string][]string `json:"requiredProperties,omitempty"` // definition names (eg, #/definitions/One) to a list of required properties on this definition which reference that definition
43+
HasSiblingProperties bool `json:"-"` // indicates if ref has sibling properties
44+
SiblingProperties map[string]*yaml.Node `json:"-"` // stores sibling property nodes
45+
SiblingKeys []*yaml.Node `json:"-"` // stores sibling key nodes
46+
In string `json:"-"` // parameter location (path, query, header, cookie) - cached for performance
4547
}
4648

4749
// ReferenceMapped is a helper struct for mapped references put into sequence (we lose the key)

0 commit comments

Comments
 (0)