Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions datamodel/low/extraction_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,27 +126,50 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S
for _, collection := range collections {
found = collection()
if found != nil && found[rv] != nil {
foundRef := found[rv]
foundIndex := idx
if foundRef.Index != nil {
foundIndex = foundRef.Index
}
if foundIndex != nil && foundRef.RemoteLocation != "" &&
foundIndex.GetSpecAbsolutePath() != foundRef.RemoteLocation {
if rolo := foundIndex.GetRolodex(); rolo != nil {
for _, candidate := range append(rolo.GetIndexes(), rolo.GetRootIndex()) {
if candidate == nil {
continue
}
if candidate.GetSpecAbsolutePath() == foundRef.RemoteLocation {
foundIndex = candidate
break
}
}
}
}
foundCtx := ctx
if foundRef.RemoteLocation != "" {
foundCtx = context.WithValue(foundCtx, index.CurrentPathKey, foundRef.RemoteLocation)
}
// if this is a ref node, we need to keep diving
// until we hit something that isn't a ref.
if jh, _, _ := utils.IsNodeRefValue(found[rv].Node); jh {
if jh, _, _ := utils.IsNodeRefValue(foundRef.Node); jh {
// if this node is circular, stop drop and roll.
if !IsCircular(found[rv].Node, idx) && found[rv].Node != root {
return LocateRefNodeWithContext(ctx, found[rv].Node, idx)
if !IsCircular(foundRef.Node, foundIndex) && foundRef.Node != root {
return LocateRefNodeWithContext(foundCtx, foundRef.Node, foundIndex)
} else {

crr := GetCircularReferenceResult(found[rv].Node, idx)
crr := GetCircularReferenceResult(foundRef.Node, foundIndex)
jp := ""
if crr != nil {
jp = crr.GenerateJourneyPath()
}
return found[rv].Node, idx, fmt.Errorf("circular reference '%s' found during lookup at line "+
return foundRef.Node, foundIndex, fmt.Errorf("circular reference '%s' found during lookup at line "+
"%d, column %d, It cannot be resolved",
jp,
found[rv].Node.Line,
found[rv].Node.Column), ctx
foundRef.Node.Line,
foundRef.Node.Column), foundCtx
}
}
return utils.NodeAlias(found[rv].Node), idx, nil, ctx
return utils.NodeAlias(foundRef.Node), foundIndex, nil, foundCtx
}
}

Expand Down
75 changes: 75 additions & 0 deletions datamodel/low/extraction_functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,81 @@ func TestLocateRefNode_CurrentPathKey_HttpLink_RemoteCtx_WithPath(t *testing.T)
assert.NotNil(t, c)
}

func TestLocateRefNodeWithContext_RemoteIndexLookup(t *testing.T) {
tempDir := t.TempDir()
rootPath := filepath.Join(tempDir, "root.yaml")
externalPath := filepath.Join(tempDir, "external.yaml")

rootCfg := index.CreateClosedAPIIndexConfig()
rootCfg.SpecAbsolutePath = rootPath
rootNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
rootIdx := index.NewSpecIndexWithConfig(rootNode, rootCfg)

externalCfg := index.CreateClosedAPIIndexConfig()
externalCfg.SpecAbsolutePath = externalPath
externalNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
externalIdx := index.NewSpecIndexWithConfig(externalNode, externalCfg)

rolo := index.NewRolodex(rootCfg)
rolo.AddExternalIndex(externalIdx, externalPath)

rootIdx.SetRolodex(rolo)
externalIdx.SetRolodex(rolo)

refValue := "external.yaml#/components/schemas/Thing"
refNode := utils.CreateRefNode(refValue)

rootIdx.SetMappedReferences(map[string]*index.Reference{
refValue: {
FullDefinition: refValue,
Node: utils.CreateStringNode("value"),
RemoteLocation: externalPath,
Index: rootIdx,
},
})

_, foundIdx, _, foundCtx := LocateRefNodeWithContext(context.Background(), refNode, rootIdx)
assert.Equal(t, externalIdx, foundIdx)
assert.Equal(t, externalPath, foundCtx.Value(index.CurrentPathKey))
}

func TestLocateRefNodeWithContext_RolodexNilCandidate(t *testing.T) {
tempDir := t.TempDir()
rootPath := filepath.Join(tempDir, "root.yaml")
dummyPath := filepath.Join(tempDir, "dummy.yaml")

rootCfg := index.CreateClosedAPIIndexConfig()
rootCfg.SpecAbsolutePath = rootPath
rootNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
rootIdx := index.NewSpecIndexWithConfig(rootNode, rootCfg)

dummyCfg := index.CreateClosedAPIIndexConfig()
dummyCfg.SpecAbsolutePath = dummyPath
dummyNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
dummyIdx := index.NewSpecIndexWithConfig(dummyNode, dummyCfg)

rolo := index.NewRolodex(rootCfg)
rolo.AddExternalIndex(dummyIdx, dummyPath)

rootIdx.SetRolodex(rolo)
dummyIdx.SetRolodex(rolo)

refValue := "missing.yaml#/components/schemas/Thing"
refNode := utils.CreateRefNode(refValue)

rootIdx.SetMappedReferences(map[string]*index.Reference{
refValue: {
FullDefinition: refValue,
Node: utils.CreateStringNode("value"),
RemoteLocation: "not-matching.yaml",
Index: rootIdx,
},
})

_, foundIdx, _, _ := LocateRefNodeWithContext(context.Background(), refNode, rootIdx)
assert.Equal(t, rootIdx, foundIdx)
}

func TestLocateRefNode_CurrentPathKey_Path_Link(t *testing.T) {
no := yaml.Node{
Kind: yaml.MappingNode,
Expand Down
4 changes: 4 additions & 0 deletions index/index_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ type SpecIndexConfig struct {
// When enabled, properties from referenced schemas will be merged with local sibling properties.
MergeReferencedProperties bool

// ResolveNestedRefsWithDocumentContext uses the referenced document's path/index as the base for any nested refs.
// This is disabled by default to preserve historical resolver behavior.
ResolveNestedRefsWithDocumentContext bool

// PropertyMergeStrategy defines how to handle conflicts when merging properties.
PropertyMergeStrategy datamodel.PropertyMergeStrategy

Expand Down
55 changes: 47 additions & 8 deletions index/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package index

import (
"context"
"errors"
"fmt"
"net/url"
Expand Down Expand Up @@ -348,6 +349,35 @@ func visitIndex(res *Resolver, idx *SpecIndex) {
}
}

// searchReferenceWithContext resolves a reference using document context when enabled in the config.
func (resolver *Resolver) searchReferenceWithContext(sourceRef, searchRef *Reference) (*Reference, *SpecIndex, context.Context) {
if resolver.specIndex == nil || resolver.specIndex.config == nil || !resolver.specIndex.config.ResolveNestedRefsWithDocumentContext {
ref, idx := resolver.specIndex.SearchIndexForReferenceByReference(searchRef)
return ref, idx, context.Background()
}

searchIndex := resolver.specIndex
if searchRef != nil && searchRef.Index != nil {
searchIndex = searchRef.Index
} else if sourceRef != nil && sourceRef.Index != nil {
searchIndex = sourceRef.Index
}

ctx := context.Background()
currentPath := ""
if sourceRef != nil {
currentPath = sourceRef.RemoteLocation
}
if currentPath == "" && searchIndex != nil {
currentPath = searchIndex.specAbsolutePath
}
if currentPath != "" {
ctx = context.WithValue(ctx, CurrentPathKey, currentPath)
}

return searchIndex.SearchIndexForReferenceByReferenceWithContext(ctx, searchRef)
}

// VisitReference will visit a reference as part of a journey and will return resolved nodes.
func (resolver *Resolver) VisitReference(ref *Reference, seen map[string]bool, journey []*Reference, resolve bool) []*yaml.Node {
resolver.referencesVisited++
Expand All @@ -374,7 +404,7 @@ func (resolver *Resolver) VisitReference(ref *Reference, seen map[string]bool, j
if j.FullDefinition == r.FullDefinition {

var foundDup *Reference
foundRef, _ := resolver.specIndex.SearchIndexForReferenceByReference(r)
foundRef, _, _ := resolver.searchReferenceWithContext(ref, r)
if foundRef != nil {
foundDup = foundRef
}
Expand Down Expand Up @@ -421,7 +451,7 @@ func (resolver *Resolver) VisitReference(ref *Reference, seen map[string]bool, j

if !skip {
var original *Reference
foundRef, _ := resolver.specIndex.SearchIndexForReferenceByReference(r)
foundRef, _, _ := resolver.searchReferenceWithContext(ref, r)
if foundRef != nil {
original = foundRef
}
Expand Down Expand Up @@ -530,7 +560,7 @@ func (resolver *Resolver) extractRelatives(ref *Reference, node, parent *yaml.No
depth++

var foundRef *Reference
foundRef, _ = resolver.specIndex.SearchIndexForReferenceByReference(ref)
foundRef, _, _ = resolver.searchReferenceWithContext(ref, ref)
if foundRef != nil && !foundRef.Circular {
found = append(found, resolver.extractRelatives(foundRef, n, node, foundRelatives, journey, seen, resolve, depth)...)
depth--
Expand Down Expand Up @@ -591,10 +621,14 @@ func (resolver *Resolver) extractRelatives(ref *Reference, node, parent *yaml.No
}
} else {
// local component, full def is based on passed in ref
if strings.HasPrefix(ref.FullDefinition, "http") {
baseLocation := ref.FullDefinition
if ref.RemoteLocation != "" {
baseLocation = ref.RemoteLocation
}
if strings.HasPrefix(baseLocation, "http") {

// split the http URI into parts
httpExp := strings.Split(ref.FullDefinition, "#/")
httpExp := strings.Split(baseLocation, "#/")

// parse a URL from the full def
u, _ := url.Parse(httpExp[0])
Expand All @@ -604,7 +638,7 @@ func (resolver *Resolver) extractRelatives(ref *Reference, node, parent *yaml.No

} else {
// split the full def into parts
fileDef := strings.Split(ref.FullDefinition, "#/")
fileDef := strings.Split(baseLocation, "#/")
fullDef = fmt.Sprintf("%s#/%s", fileDef[0], exp[1])
}
}
Expand All @@ -618,7 +652,11 @@ func (resolver *Resolver) extractRelatives(ref *Reference, node, parent *yaml.No
} else {

// split the full def into parts
fileDef := strings.Split(ref.FullDefinition, "#/")
baseLocation := ref.FullDefinition
if ref.RemoteLocation != "" {
baseLocation = ref.RemoteLocation
}
fileDef := strings.Split(baseLocation, "#/")

// is the file def a http link?
if strings.HasPrefix(fileDef[0], "http") {
Expand All @@ -639,9 +677,10 @@ func (resolver *Resolver) extractRelatives(ref *Reference, node, parent *yaml.No
FullDefinition: fullDef,
RemoteLocation: ref.RemoteLocation,
IsRemote: true,
Index: ref.Index,
}

locatedRef, _ = resolver.specIndex.SearchIndexForReferenceByReference(searchRef)
locatedRef, _, _ = resolver.searchReferenceWithContext(ref, searchRef)

if locatedRef == nil {
_, path := utils.ConvertComponentIdIntoFriendlyPathSearch(value)
Expand Down
Loading
Loading