fix for resolving looping relative references

In vacuum, a usecase was reported where an infinite loop occurred due to re-parsing the same reference over and over in a loop. It was re-creatable and it was because the loop happened before the index was ready.

This should be resolved now, at least for this use case. To be sure, I have included the specs as a new test.

https://github.com/daveshanley/vacuum/issues/268
Signed-off-by: Dave Shanley <dave@quobix.com>
This commit is contained in:
Dave Shanley
2023-05-16 16:12:32 -04:00
parent 08c9ca8c26
commit f629c0ff58
13 changed files with 1383 additions and 1268 deletions

View File

@@ -4,430 +4,429 @@
package index package index
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/pb33f/libopenapi/utils" "github.com/pb33f/libopenapi/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"strings" "strings"
) )
// ExtractRefs will return a deduplicated slice of references for every unique ref found in the document. // ExtractRefs will return a deduplicated slice of references for every unique ref found in the document.
// The total number of refs, will generally be much higher, you can extract those from GetRawReferenceCount() // The total number of refs, will generally be much higher, you can extract those from GetRawReferenceCount()
func (index *SpecIndex) ExtractRefs(node, parent *yaml.Node, seenPath []string, level int, poly bool, pName string) []*Reference { func (index *SpecIndex) ExtractRefs(node, parent *yaml.Node, seenPath []string, level int, poly bool, pName string) []*Reference {
if node == nil { if node == nil {
return nil return nil
} }
var found []*Reference var found []*Reference
if len(node.Content) > 0 { if len(node.Content) > 0 {
var prev, polyName string var prev, polyName string
for i, n := range node.Content { for i, n := range node.Content {
if utils.IsNodeMap(n) || utils.IsNodeArray(n) { if utils.IsNodeMap(n) || utils.IsNodeArray(n) {
level++ level++
// check if we're using polymorphic values. These tend to create rabbit warrens of circular // check if we're using polymorphic values. These tend to create rabbit warrens of circular
// references if every single link is followed. We don't resolve polymorphic values. // references if every single link is followed. We don't resolve polymorphic values.
isPoly, _ := index.checkPolymorphicNode(prev) isPoly, _ := index.checkPolymorphicNode(prev)
polyName = pName polyName = pName
if isPoly { if isPoly {
poly = true poly = true
if prev != "" { if prev != "" {
polyName = prev polyName = prev
} }
} }
found = append(found, index.ExtractRefs(n, node, seenPath, level, poly, polyName)...) found = append(found, index.ExtractRefs(n, node, seenPath, level, poly, polyName)...)
} }
// check if we're dealing with an inline schema definition, that isn't part of an array // check if we're dealing with an inline schema definition, that isn't part of an array
// (which means it's being used as a value in an array, and it's not a label) // (which means it's being used as a value in an array, and it's not a label)
// https://github.com/pb33f/libopenapi/issues/76 // https://github.com/pb33f/libopenapi/issues/76
if i%2 == 0 && n.Value == "schema" && !utils.IsNodeArray(node) && (i+1 < len(node.Content)) { if i%2 == 0 && n.Value == "schema" && !utils.IsNodeArray(node) && (i+1 < len(node.Content)) {
isRef, _, _ := utils.IsNodeRefValue(node.Content[i+1]) isRef, _, _ := utils.IsNodeRefValue(node.Content[i+1])
if isRef { if isRef {
continue continue
} }
ref := &Reference{ ref := &Reference{
Node: node.Content[i+1], Node: node.Content[i+1],
Path: fmt.Sprintf("$.%s.schema", strings.Join(seenPath, ".")), Path: fmt.Sprintf("$.%s.schema", strings.Join(seenPath, ".")),
} }
index.allInlineSchemaDefinitions = append(index.allInlineSchemaDefinitions, ref) index.allInlineSchemaDefinitions = append(index.allInlineSchemaDefinitions, ref)
// check if the schema is an object or an array, // check if the schema is an object or an array,
// and if so, add it to the list of inline schema object definitions. // and if so, add it to the list of inline schema object definitions.
k, v := utils.FindKeyNodeTop("type", node.Content[i+1].Content) k, v := utils.FindKeyNodeTop("type", node.Content[i+1].Content)
if k != nil && v != nil { if k != nil && v != nil {
if v.Value == "object" || v.Value == "array" { if v.Value == "object" || v.Value == "array" {
index.allInlineSchemaObjectDefinitions = append(index.allInlineSchemaObjectDefinitions, ref) index.allInlineSchemaObjectDefinitions = append(index.allInlineSchemaObjectDefinitions, ref)
} }
} }
} }
// Perform the same check for all properties in an inline schema definition // Perform the same check for all properties in an inline schema definition
// https://github.com/pb33f/libopenapi/issues/76 // https://github.com/pb33f/libopenapi/issues/76
if i%2 == 0 && n.Value == "properties" && !utils.IsNodeArray(node) && (i+1 < len(node.Content)) { if i%2 == 0 && n.Value == "properties" && !utils.IsNodeArray(node) && (i+1 < len(node.Content)) {
isRef, _, _ := utils.IsNodeRefValue(node.Content[i+1]) isRef, _, _ := utils.IsNodeRefValue(node.Content[i+1])
if isRef { if isRef {
continue continue
} }
// for each property add it to our schema definitions // for each property add it to our schema definitions
label := "" label := ""
for h, prop := range node.Content[i+1].Content { for h, prop := range node.Content[i+1].Content {
if h%2 == 0 { if h%2 == 0 {
label = prop.Value label = prop.Value
continue continue
} }
ref := &Reference{ ref := &Reference{
Node: prop, Node: prop,
Path: fmt.Sprintf("$.%s.properties.%s", strings.Join(seenPath, "."), label), Path: fmt.Sprintf("$.%s.properties.%s", strings.Join(seenPath, "."), label),
} }
index.allInlineSchemaDefinitions = append(index.allInlineSchemaDefinitions, ref) index.allInlineSchemaDefinitions = append(index.allInlineSchemaDefinitions, ref)
// check if the schema is an object or an array, // check if the schema is an object or an array,
// and if so, add it to the list of inline schema object definitions. // and if so, add it to the list of inline schema object definitions.
k, v := utils.FindKeyNodeTop("type", node.Content[i+1].Content) k, v := utils.FindKeyNodeTop("type", node.Content[i+1].Content)
if k != nil && v != nil { if k != nil && v != nil {
if v.Value == "object" || v.Value == "array" { if v.Value == "object" || v.Value == "array" {
index.allInlineSchemaObjectDefinitions = append(index.allInlineSchemaObjectDefinitions, ref) index.allInlineSchemaObjectDefinitions = append(index.allInlineSchemaObjectDefinitions, ref)
} }
} }
} }
} }
if i%2 == 0 && n.Value == "$ref" { if i%2 == 0 && n.Value == "$ref" {
// only look at scalar values, not maps (looking at you k8s) // only look at scalar values, not maps (looking at you k8s)
if !utils.IsNodeStringValue(node.Content[i+1]) { if !utils.IsNodeStringValue(node.Content[i+1]) {
continue continue
} }
index.linesWithRefs[n.Line] = true index.linesWithRefs[n.Line] = true
fp := make([]string, len(seenPath)) fp := make([]string, len(seenPath))
for x, foundPathNode := range seenPath { for x, foundPathNode := range seenPath {
fp[x] = foundPathNode fp[x] = foundPathNode
} }
value := node.Content[i+1].Value value := node.Content[i+1].Value
segs := strings.Split(value, "/") segs := strings.Split(value, "/")
name := segs[len(segs)-1] name := segs[len(segs)-1]
_, p := utils.ConvertComponentIdIntoFriendlyPathSearch(value) _, p := utils.ConvertComponentIdIntoFriendlyPathSearch(value)
ref := &Reference{ ref := &Reference{
Definition: value, Definition: value,
Name: name, Name: name,
Node: node, Node: node,
Path: p, Path: p,
} }
// add to raw sequenced refs // add to raw sequenced refs
index.rawSequencedRefs = append(index.rawSequencedRefs, ref) index.rawSequencedRefs = append(index.rawSequencedRefs, ref)
// add ref by line number // add ref by line number
refNameIndex := strings.LastIndex(value, "/") refNameIndex := strings.LastIndex(value, "/")
refName := value[refNameIndex+1:] refName := value[refNameIndex+1:]
if len(index.refsByLine[refName]) > 0 { if len(index.refsByLine[refName]) > 0 {
index.refsByLine[refName][n.Line] = true index.refsByLine[refName][n.Line] = true
} else { } else {
v := make(map[int]bool) v := make(map[int]bool)
v[n.Line] = true v[n.Line] = true
index.refsByLine[refName] = v index.refsByLine[refName] = v
} }
// if this ref value has any siblings (node.Content is larger than two elements) // if this ref value has any siblings (node.Content is larger than two elements)
// then add to refs with siblings // then add to refs with siblings
if len(node.Content) > 2 { if len(node.Content) > 2 {
copiedNode := *node copiedNode := *node
copied := Reference{ copied := Reference{
Definition: ref.Definition, Definition: ref.Definition,
Name: ref.Name, Name: ref.Name,
Node: &copiedNode, Node: &copiedNode,
Path: p, Path: p,
} }
// protect this data using a copy, prevent the resolver from destroying things. // protect this data using a copy, prevent the resolver from destroying things.
index.refsWithSiblings[value] = copied index.refsWithSiblings[value] = copied
} }
// if this is a polymorphic reference, we're going to leave it out // if this is a polymorphic reference, we're going to leave it out
// allRefs. We don't ever want these resolved, so instead of polluting // allRefs. We don't ever want these resolved, so instead of polluting
// the timeline, we will keep each poly ref in its own collection for later // the timeline, we will keep each poly ref in its own collection for later
// analysis. // analysis.
if poly { if poly {
index.polymorphicRefs[value] = ref index.polymorphicRefs[value] = ref
// index each type // index each type
switch pName { switch pName {
case "anyOf": case "anyOf":
index.polymorphicAnyOfRefs = append(index.polymorphicAnyOfRefs, ref) index.polymorphicAnyOfRefs = append(index.polymorphicAnyOfRefs, ref)
case "allOf": case "allOf":
index.polymorphicAllOfRefs = append(index.polymorphicAllOfRefs, ref) index.polymorphicAllOfRefs = append(index.polymorphicAllOfRefs, ref)
case "oneOf": case "oneOf":
index.polymorphicOneOfRefs = append(index.polymorphicOneOfRefs, ref) index.polymorphicOneOfRefs = append(index.polymorphicOneOfRefs, ref)
} }
continue continue
} }
// check if this is a dupe, if so, skip it, we don't care now. // check if this is a dupe, if so, skip it, we don't care now.
if index.allRefs[value] != nil { // seen before, skip. if index.allRefs[value] != nil { // seen before, skip.
continue continue
} }
if value == "" { if value == "" {
completedPath := fmt.Sprintf("$.%s", strings.Join(fp, ".")) completedPath := fmt.Sprintf("$.%s", strings.Join(fp, "."))
indexError := &IndexingError{ indexError := &IndexingError{
Err: errors.New("schema reference is empty and cannot be processed"), Err: errors.New("schema reference is empty and cannot be processed"),
Node: node.Content[i+1], Node: node.Content[i+1],
Path: completedPath, Path: completedPath,
} }
index.refErrors = append(index.refErrors, indexError) index.refErrors = append(index.refErrors, indexError)
continue continue
} }
index.allRefs[value] = ref index.allRefs[value] = ref
found = append(found, ref) found = append(found, ref)
} }
if i%2 == 0 && n.Value != "$ref" && n.Value != "" { if i%2 == 0 && n.Value != "$ref" && n.Value != "" {
nodePath := fmt.Sprintf("$.%s", strings.Join(seenPath, ".")) nodePath := fmt.Sprintf("$.%s", strings.Join(seenPath, "."))
// capture descriptions and summaries // capture descriptions and summaries
if n.Value == "description" { if n.Value == "description" {
// if the parent is a sequence, ignore. // if the parent is a sequence, ignore.
if utils.IsNodeArray(node) { if utils.IsNodeArray(node) {
continue continue
} }
ref := &DescriptionReference{ ref := &DescriptionReference{
Content: node.Content[i+1].Value, Content: node.Content[i+1].Value,
Path: nodePath, Path: nodePath,
Node: node.Content[i+1], Node: node.Content[i+1],
IsSummary: false, IsSummary: false,
} }
if !utils.IsNodeMap(ref.Node) { if !utils.IsNodeMap(ref.Node) {
index.allDescriptions = append(index.allDescriptions, ref) index.allDescriptions = append(index.allDescriptions, ref)
index.descriptionCount++ index.descriptionCount++
} }
} }
if n.Value == "summary" { if n.Value == "summary" {
var b *yaml.Node var b *yaml.Node
if len(node.Content) == i+1 { if len(node.Content) == i+1 {
b = node.Content[i] b = node.Content[i]
} else { } else {
b = node.Content[i+1] b = node.Content[i+1]
} }
ref := &DescriptionReference{ ref := &DescriptionReference{
Content: b.Value, Content: b.Value,
Path: nodePath, Path: nodePath,
Node: b, Node: b,
IsSummary: true, IsSummary: true,
} }
index.allSummaries = append(index.allSummaries, ref) index.allSummaries = append(index.allSummaries, ref)
index.summaryCount++ index.summaryCount++
} }
// capture security requirement references (these are not traditional references, but they // capture security requirement references (these are not traditional references, but they
// are used as a look-up. This is the only exception to the design. // are used as a look-up. This is the only exception to the design.
if n.Value == "security" { if n.Value == "security" {
var b *yaml.Node var b *yaml.Node
if len(node.Content) == i+1 { if len(node.Content) == i+1 {
b = node.Content[i] b = node.Content[i]
} else { } else {
b = node.Content[i+1] b = node.Content[i+1]
} }
if utils.IsNodeArray(b) { if utils.IsNodeArray(b) {
var secKey string var secKey string
for k := range b.Content { for k := range b.Content {
if utils.IsNodeMap(b.Content[k]) { if utils.IsNodeMap(b.Content[k]) {
for g := range b.Content[k].Content { for g := range b.Content[k].Content {
if g%2 == 0 { if g%2 == 0 {
secKey = b.Content[k].Content[g].Value secKey = b.Content[k].Content[g].Value
continue continue
} }
if utils.IsNodeArray(b.Content[k].Content[g]) { if utils.IsNodeArray(b.Content[k].Content[g]) {
var refMap map[string][]*Reference var refMap map[string][]*Reference
if index.securityRequirementRefs[secKey] == nil { if index.securityRequirementRefs[secKey] == nil {
index.securityRequirementRefs[secKey] = make(map[string][]*Reference) index.securityRequirementRefs[secKey] = make(map[string][]*Reference)
refMap = index.securityRequirementRefs[secKey] refMap = index.securityRequirementRefs[secKey]
} else { } else {
refMap = index.securityRequirementRefs[secKey] refMap = index.securityRequirementRefs[secKey]
} }
for r := range b.Content[k].Content[g].Content { for r := range b.Content[k].Content[g].Content {
var refs []*Reference var refs []*Reference
if refMap[b.Content[k].Content[g].Content[r].Value] != nil { if refMap[b.Content[k].Content[g].Content[r].Value] != nil {
refs = refMap[b.Content[k].Content[g].Content[r].Value] refs = refMap[b.Content[k].Content[g].Content[r].Value]
} }
refs = append(refs, &Reference{ refs = append(refs, &Reference{
Definition: b.Content[k].Content[g].Content[r].Value, Definition: b.Content[k].Content[g].Content[r].Value,
Path: fmt.Sprintf("%s.security[%d].%s[%d]", nodePath, k, secKey, r), Path: fmt.Sprintf("%s.security[%d].%s[%d]", nodePath, k, secKey, r),
Node: b.Content[k].Content[g].Content[r], Node: b.Content[k].Content[g].Content[r],
}) })
index.securityRequirementRefs[secKey][b.Content[k].Content[g].Content[r].Value] = refs index.securityRequirementRefs[secKey][b.Content[k].Content[g].Content[r].Value] = refs
} }
} }
} }
} }
} }
} }
} }
// capture enums // capture enums
if n.Value == "enum" { if n.Value == "enum" {
// all enums need to have a type, extract the type from the node where the enum was found. // all enums need to have a type, extract the type from the node where the enum was found.
_, enumKeyValueNode := utils.FindKeyNodeTop("type", node.Content) _, enumKeyValueNode := utils.FindKeyNodeTop("type", node.Content)
if enumKeyValueNode != nil { if enumKeyValueNode != nil {
ref := &EnumReference{ ref := &EnumReference{
Path: nodePath, Path: nodePath,
Node: node.Content[i+1], Node: node.Content[i+1],
Type: enumKeyValueNode, Type: enumKeyValueNode,
SchemaNode: node, SchemaNode: node,
ParentNode: parent, ParentNode: parent,
} }
index.allEnums = append(index.allEnums, ref) index.allEnums = append(index.allEnums, ref)
index.enumCount++ index.enumCount++
} }
} }
// capture all objects with properties // capture all objects with properties
if n.Value == "properties" { if n.Value == "properties" {
_, typeKeyValueNode := utils.FindKeyNodeTop("type", node.Content) _, typeKeyValueNode := utils.FindKeyNodeTop("type", node.Content)
if typeKeyValueNode != nil { if typeKeyValueNode != nil {
isObject := false isObject := false
if typeKeyValueNode.Value == "object" { if typeKeyValueNode.Value == "object" {
isObject = true isObject = true
} }
for _, v := range typeKeyValueNode.Content { for _, v := range typeKeyValueNode.Content {
if v.Value == "object" { if v.Value == "object" {
isObject = true isObject = true
} }
} }
if isObject { if isObject {
index.allObjectsWithProperties = append(index.allObjectsWithProperties, &ObjectReference{ index.allObjectsWithProperties = append(index.allObjectsWithProperties, &ObjectReference{
Path: nodePath, Path: nodePath,
Node: node, Node: node,
ParentNode: parent, ParentNode: parent,
}) })
} }
} }
} }
seenPath = append(seenPath, n.Value) seenPath = append(seenPath, n.Value)
prev = n.Value prev = n.Value
} }
// if next node is map, don't add segment. // if next node is map, don't add segment.
if i < len(node.Content)-1 { if i < len(node.Content)-1 {
next := node.Content[i+1] next := node.Content[i+1]
if i%2 != 0 && next != nil && !utils.IsNodeArray(next) && !utils.IsNodeMap(next) { if i%2 != 0 && next != nil && !utils.IsNodeArray(next) && !utils.IsNodeMap(next) {
seenPath = seenPath[:len(seenPath)-1] seenPath = seenPath[:len(seenPath)-1]
} }
} }
} }
if len(seenPath) > 0 { if len(seenPath) > 0 {
seenPath = seenPath[:len(seenPath)-1] seenPath = seenPath[:len(seenPath)-1]
} }
} }
if len(seenPath) > 0 { if len(seenPath) > 0 {
seenPath = seenPath[:len(seenPath)-1] seenPath = seenPath[:len(seenPath)-1]
} }
index.refCount = len(index.allRefs) index.refCount = len(index.allRefs)
return found return found
} }
// ExtractComponentsFromRefs returns located components from references. The returned nodes from here // ExtractComponentsFromRefs returns located components from references. The returned nodes from here
// can be used for resolving as they contain the actual object properties. // can be used for resolving as they contain the actual object properties.
func (index *SpecIndex) ExtractComponentsFromRefs(refs []*Reference) []*Reference { func (index *SpecIndex) ExtractComponentsFromRefs(refs []*Reference) []*Reference {
var found []*Reference var found []*Reference
//run this async because when things get recursive, it can take a while //run this async because when things get recursive, it can take a while
c := make(chan bool) c := make(chan bool)
locate := func(ref *Reference, refIndex int, sequence []*ReferenceMapped) {
located := index.FindComponent(ref.Definition, ref.Node)
if located != nil {
index.refLock.Lock()
if index.allMappedRefs[ref.Definition] == nil {
found = append(found, located)
index.allMappedRefs[ref.Definition] = located
sequence[refIndex] = &ReferenceMapped{
Reference: located,
Definition: ref.Definition,
}
}
index.refLock.Unlock()
} else {
_, path := utils.ConvertComponentIdIntoFriendlyPathSearch(ref.Definition) locate := func(ref *Reference, refIndex int, sequence []*ReferenceMapped) {
indexError := &IndexingError{ located := index.FindComponent(ref.Definition, ref.Node)
Err: fmt.Errorf("component '%s' does not exist in the specification", ref.Definition), if located != nil {
Node: ref.Node, index.refLock.Lock()
Path: path, if index.allMappedRefs[ref.Definition] == nil {
} found = append(found, located)
index.errorLock.Lock() index.allMappedRefs[ref.Definition] = located
index.refErrors = append(index.refErrors, indexError) sequence[refIndex] = &ReferenceMapped{
index.errorLock.Unlock() Reference: located,
} Definition: ref.Definition,
c <- true }
} }
index.refLock.Unlock()
} else {
var refsToCheck []*Reference _, path := utils.ConvertComponentIdIntoFriendlyPathSearch(ref.Definition)
for _, ref := range refs { indexError := &IndexingError{
Err: fmt.Errorf("component '%s' does not exist in the specification", ref.Definition),
Node: ref.Node,
Path: path,
}
index.errorLock.Lock()
index.refErrors = append(index.refErrors, indexError)
index.errorLock.Unlock()
}
c <- true
}
// check reference for backslashes (hah yeah seen this too!) var refsToCheck []*Reference
if strings.Contains(ref.Definition, "\\") { // this was from blazemeter.com haha! for _, ref := range refs {
_, path := utils.ConvertComponentIdIntoFriendlyPathSearch(ref.Definition)
indexError := &IndexingError{
Err: fmt.Errorf("component '%s' contains a backslash '\\'. It's not valid", ref.Definition),
Node: ref.Node,
Path: path,
}
index.refErrors = append(index.refErrors, indexError)
continue
} // check reference for backslashes (hah yeah seen this too!)
refsToCheck = append(refsToCheck, ref) if strings.Contains(ref.Definition, "\\") { // this was from blazemeter.com haha!
} _, path := utils.ConvertComponentIdIntoFriendlyPathSearch(ref.Definition)
mappedRefsInSequence := make([]*ReferenceMapped, len(refsToCheck)) indexError := &IndexingError{
Err: fmt.Errorf("component '%s' contains a backslash '\\'. It's not valid", ref.Definition),
Node: ref.Node,
Path: path,
}
index.refErrors = append(index.refErrors, indexError)
continue
for r := range refsToCheck { }
// expand our index of all mapped refs refsToCheck = append(refsToCheck, ref)
go locate(refsToCheck[r], r, mappedRefsInSequence) }
//locate(refsToCheck[r], r, mappedRefsInSequence) // used for sync testing. mappedRefsInSequence := make([]*ReferenceMapped, len(refsToCheck))
}
completedRefs := 0 for r := range refsToCheck {
for completedRefs < len(refsToCheck) { // expand our index of all mapped refs
select { go locate(refsToCheck[r], r, mappedRefsInSequence)
case <-c: //locate(refsToCheck[r], r, mappedRefsInSequence) // used for sync testing.
completedRefs++ }
}
} completedRefs := 0
for m := range mappedRefsInSequence { for completedRefs < len(refsToCheck) {
if mappedRefsInSequence[m] != nil { select {
index.allMappedRefsSequenced = append(index.allMappedRefsSequenced, mappedRefsInSequence[m]) case <-c:
} completedRefs++
} }
return found }
for m := range mappedRefsInSequence {
if mappedRefsInSequence[m] != nil {
index.allMappedRefsSequenced = append(index.allMappedRefsSequenced, mappedRefsInSequence[m])
}
}
return found
} }

View File

@@ -267,11 +267,16 @@ func (index *SpecIndex) FindComponentInRoot(componentId string) *Reference {
return nil // no component found return nil // no component found
} }
res, _ := path.Find(index.root) res, _ := path.Find(index.root)
if len(res) == 1 { if len(res) == 1 {
resNode := res[0]
if res[0].Kind == yaml.DocumentNode {
resNode = res[0].Content[0]
}
ref := &Reference{ ref := &Reference{
Definition: componentId, Definition: componentId,
Name: name, Name: name,
Node: res[0], Node: resNode,
Path: friendlySearch, Path: friendlySearch,
RequiredRefProperties: index.extractDefinitionRequiredRefProperties(res[0], map[string][]string{}), RequiredRefProperties: index.extractDefinitionRequiredRefProperties(res[0], map[string][]string{}),
} }
@@ -283,8 +288,7 @@ func (index *SpecIndex) FindComponentInRoot(componentId string) *Reference {
} }
func (index *SpecIndex) performExternalLookup(uri []string, componentId string, func (index *SpecIndex) performExternalLookup(uri []string, componentId string,
lookupFunction ExternalLookupFunction, parent *yaml.Node, lookupFunction ExternalLookupFunction, parent *yaml.Node) *Reference {
) *Reference {
if len(uri) > 0 { if len(uri) > 0 {
index.externalLock.RLock() index.externalLock.RLock()
externalSpecIndex := index.externalSpecIndex[uri[0]] externalSpecIndex := index.externalSpecIndex[uri[0]]
@@ -349,21 +353,29 @@ func (index *SpecIndex) performExternalLookup(uri []string, componentId string,
BasePath: newBasePath, BasePath: newBasePath,
AllowRemoteLookup: index.config.AllowRemoteLookup, AllowRemoteLookup: index.config.AllowRemoteLookup,
AllowFileLookup: index.config.AllowFileLookup, AllowFileLookup: index.config.AllowFileLookup,
ParentIndex: index,
seenRemoteSources: index.config.seenRemoteSources, seenRemoteSources: index.config.seenRemoteSources,
remoteLock: index.config.remoteLock, remoteLock: index.config.remoteLock,
uri: uri,
} }
var newIndex *SpecIndex var newIndex *SpecIndex
newIndex = NewSpecIndexWithConfig(newRoot, newConfig) seen := index.SearchAncestryForSeenURI(uri[0])
index.refLock.Lock() if seen == nil {
index.externalLock.Lock()
index.externalSpecIndex[uri[0]] = newIndex newIndex = NewSpecIndexWithConfig(newRoot, newConfig)
index.externalLock.Unlock() index.refLock.Lock()
newIndex.relativePath = path index.externalLock.Lock()
newIndex.parentIndex = index index.externalSpecIndex[uri[0]] = newIndex
index.AddChild(newIndex) index.externalLock.Unlock()
index.refLock.Unlock() newIndex.relativePath = path
externalSpecIndex = newIndex newIndex.parentIndex = index
index.AddChild(newIndex)
index.refLock.Unlock()
externalSpecIndex = newIndex
} else {
externalSpecIndex = seen
}
} }
} }

View File

@@ -6,6 +6,7 @@ package index
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"os"
"testing" "testing"
) )
@@ -26,6 +27,22 @@ func TestSpecIndex_performExternalLookup(t *testing.T) {
assert.Len(t, index.GetPathsNode().Content, 1) assert.Len(t, index.GetPathsNode().Content, 1)
} }
func TestSpecIndex_CheckCircularIndex(t *testing.T) {
yml, _ := os.ReadFile("../test_specs/first.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
c := CreateOpenAPIIndexConfig()
c.BasePath = "../test_specs"
index := NewSpecIndexWithConfig(&rootNode, c)
assert.Nil(t, index.uri)
assert.NotNil(t, index.children[0].uri)
assert.NotNil(t, index.children[0].children[0].uri)
assert.NotNil(t, index.SearchIndexForReference("second.yaml#/properties/property2"))
assert.NotNil(t, index.SearchIndexForReference("second.yaml"))
assert.Nil(t, index.SearchIndexForReference("fourth.yaml"))
}
func TestSpecIndex_performExternalLookup_invalidURL(t *testing.T) { func TestSpecIndex_performExternalLookup_invalidURL(t *testing.T) {
yml := `openapi: 3.1.0 yml := `openapi: 3.1.0
components: components:

View File

@@ -4,77 +4,82 @@
package index package index
import ( import (
"golang.org/x/sync/syncmap" "golang.org/x/sync/syncmap"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"sync" "sync"
) )
// Constants used to determine if resolving is local, file based or remote file based. // Constants used to determine if resolving is local, file based or remote file based.
const ( const (
LocalResolve = iota LocalResolve = iota
HttpResolve HttpResolve
FileResolve FileResolve
) )
// Reference is a wrapper around *yaml.Node results to make things more manageable when performing // Reference is a wrapper around *yaml.Node results to make things more manageable when performing
// algorithms on data models. the *yaml.Node def is just a bit too low level for tracking state. // algorithms on data models. the *yaml.Node def is just a bit too low level for tracking state.
type Reference struct { type Reference struct {
Definition string Definition string
Name string Name string
Node *yaml.Node Node *yaml.Node
ParentNode *yaml.Node ParentNode *yaml.Node
Resolved bool Resolved bool
Circular bool Circular bool
Seen bool Seen bool
IsRemote bool IsRemote bool
RemoteLocation string RemoteLocation string
Path string // this won't always be available. Path string // this won't always be available.
RequiredRefProperties map[string][]string // definition names (eg, #/definitions/One) to a list of required properties on this definition which reference that definition RequiredRefProperties map[string][]string // definition names (eg, #/definitions/One) to a list of required properties on this definition which reference that definition
} }
// ReferenceMapped is a helper struct for mapped references put into sequence (we lose the key) // ReferenceMapped is a helper struct for mapped references put into sequence (we lose the key)
type ReferenceMapped struct { type ReferenceMapped struct {
Reference *Reference Reference *Reference
Definition string Definition string
} }
// SpecIndexConfig is a configuration struct for the SpecIndex introduced in 0.6.0 that provides an expandable // SpecIndexConfig is a configuration struct for the SpecIndex introduced in 0.6.0 that provides an expandable
// set of granular options. The first being the ability to set the Base URL for resolving relative references, and // set of granular options. The first being the ability to set the Base URL for resolving relative references, and
// allowing or disallowing remote or local file lookups. // allowing or disallowing remote or local file lookups.
// - https://github.com/pb33f/libopenapi/issues/73 // - https://github.com/pb33f/libopenapi/issues/73
type SpecIndexConfig struct { type SpecIndexConfig struct {
// The BaseURL will be the root from which relative references will be resolved from if they can't be found locally. // The BaseURL will be the root from which relative references will be resolved from if they can't be found locally.
// //
// For example: // For example:
// - $ref: somefile.yaml#/components/schemas/SomeSchema // - $ref: somefile.yaml#/components/schemas/SomeSchema
// //
// Might not be found locally, if the file was pulled in from a remote server (a good example is the DigitalOcean API). // Might not be found locally, if the file was pulled in from a remote server (a good example is the DigitalOcean API).
// so by setting a BaseURL, the reference will try to be resolved from the remote server. // so by setting a BaseURL, the reference will try to be resolved from the remote server.
// //
// If our baseURL is set to https://pb33f.io/libopenapi then our reference will try to be resolved from: // If our baseURL is set to https://pb33f.io/libopenapi then our reference will try to be resolved from:
// - $ref: https://pb33f.io/libopenapi/somefile.yaml#/components/schemas/SomeSchema // - $ref: https://pb33f.io/libopenapi/somefile.yaml#/components/schemas/SomeSchema
// //
// More details on relative references can be found in issue #73: https://github.com/pb33f/libopenapi/issues/73 // More details on relative references can be found in issue #73: https://github.com/pb33f/libopenapi/issues/73
BaseURL *url.URL // set the Base URL for resolving relative references if the spec is exploded. BaseURL *url.URL // set the Base URL for resolving relative references if the spec is exploded.
// If resolving locally, the BasePath will be the root from which relative references will be resolved from // If resolving locally, the BasePath will be the root from which relative references will be resolved from
BasePath string // set the Base Path for resolving relative references if the spec is exploded. BasePath string // set the Base Path for resolving relative references if the spec is exploded.
// In an earlier version of libopenapi (pre 0.6.0) the index would automatically resolve all references // In an earlier version of libopenapi (pre 0.6.0) the index would automatically resolve all references
// They could have been local, or they could have been remote. This was a problem because it meant // They could have been local, or they could have been remote. This was a problem because it meant
// There was a potential for a remote exploit if a remote reference was malicious. There aren't any known // There was a potential for a remote exploit if a remote reference was malicious. There aren't any known
// exploits, but it's better to be safe than sorry. // exploits, but it's better to be safe than sorry.
// //
// To read more about this, you can find a discussion here: https://github.com/pb33f/libopenapi/pull/64 // To read more about this, you can find a discussion here: https://github.com/pb33f/libopenapi/pull/64
AllowRemoteLookup bool // Allow remote lookups for references. Defaults to false AllowRemoteLookup bool // Allow remote lookups for references. Defaults to false
AllowFileLookup bool // Allow file lookups for references. Defaults to false AllowFileLookup bool // Allow file lookups for references. Defaults to false
// private fields // ParentIndex allows the index to be created with knowledge of a parent, before being parsed. This allows
seenRemoteSources *syncmap.Map // a breakglass to be used to prevent loops, checking the tree before recursing down.
remoteLock *sync.Mutex ParentIndex *SpecIndex
// private fields
seenRemoteSources *syncmap.Map
remoteLock *sync.Mutex
uri []string
} }
// CreateOpenAPIIndexConfig is a helper function to create a new SpecIndexConfig with the AllowRemoteLookup and // CreateOpenAPIIndexConfig is a helper function to create a new SpecIndexConfig with the AllowRemoteLookup and
@@ -82,13 +87,13 @@ type SpecIndexConfig struct {
// //
// The default BasePath is the current working directory. // The default BasePath is the current working directory.
func CreateOpenAPIIndexConfig() *SpecIndexConfig { func CreateOpenAPIIndexConfig() *SpecIndexConfig {
cw, _ := os.Getwd() cw, _ := os.Getwd()
return &SpecIndexConfig{ return &SpecIndexConfig{
BasePath: cw, BasePath: cw,
AllowRemoteLookup: true, AllowRemoteLookup: true,
AllowFileLookup: true, AllowFileLookup: true,
seenRemoteSources: &syncmap.Map{}, seenRemoteSources: &syncmap.Map{},
} }
} }
// CreateClosedAPIIndexConfig is a helper function to create a new SpecIndexConfig with the AllowRemoteLookup and // CreateClosedAPIIndexConfig is a helper function to create a new SpecIndexConfig with the AllowRemoteLookup and
@@ -96,141 +101,142 @@ func CreateOpenAPIIndexConfig() *SpecIndexConfig {
// //
// The default BasePath is the current working directory. // The default BasePath is the current working directory.
func CreateClosedAPIIndexConfig() *SpecIndexConfig { func CreateClosedAPIIndexConfig() *SpecIndexConfig {
cw, _ := os.Getwd() cw, _ := os.Getwd()
return &SpecIndexConfig{ return &SpecIndexConfig{
BasePath: cw, BasePath: cw,
AllowRemoteLookup: false, AllowRemoteLookup: false,
AllowFileLookup: false, AllowFileLookup: false,
seenRemoteSources: &syncmap.Map{}, seenRemoteSources: &syncmap.Map{},
} }
} }
// SpecIndex is a complete pre-computed index of the entire specification. Numbers are pre-calculated and // SpecIndex is a complete pre-computed index of the entire specification. Numbers are pre-calculated and
// quick direct access to paths, operations, tags are all available. No need to walk the entire node tree in rules, // quick direct access to paths, operations, tags are all available. No need to walk the entire node tree in rules,
// everything is pre-walked if you need it. // everything is pre-walked if you need it.
type SpecIndex struct { type SpecIndex struct {
allRefs map[string]*Reference // all (deduplicated) refs allRefs map[string]*Reference // all (deduplicated) refs
rawSequencedRefs []*Reference // all raw references in sequence as they are scanned, not deduped. rawSequencedRefs []*Reference // all raw references in sequence as they are scanned, not deduped.
linesWithRefs map[int]bool // lines that link to references. linesWithRefs map[int]bool // lines that link to references.
allMappedRefs map[string]*Reference // these are the located mapped refs allMappedRefs map[string]*Reference // these are the located mapped refs
allMappedRefsSequenced []*ReferenceMapped // sequenced mapped refs allMappedRefsSequenced []*ReferenceMapped // sequenced mapped refs
refsByLine map[string]map[int]bool // every reference and the lines it's referenced from refsByLine map[string]map[int]bool // every reference and the lines it's referenced from
pathRefs map[string]map[string]*Reference // all path references pathRefs map[string]map[string]*Reference // all path references
paramOpRefs map[string]map[string]map[string][]*Reference // params in operations. paramOpRefs map[string]map[string]map[string][]*Reference // params in operations.
paramCompRefs map[string]*Reference // params in components paramCompRefs map[string]*Reference // params in components
paramAllRefs map[string]*Reference // combined components and ops paramAllRefs map[string]*Reference // combined components and ops
paramInlineDuplicateNames map[string][]*Reference // inline params all with the same name paramInlineDuplicateNames map[string][]*Reference // inline params all with the same name
globalTagRefs map[string]*Reference // top level global tags globalTagRefs map[string]*Reference // top level global tags
securitySchemeRefs map[string]*Reference // top level security schemes securitySchemeRefs map[string]*Reference // top level security schemes
requestBodiesRefs map[string]*Reference // top level request bodies requestBodiesRefs map[string]*Reference // top level request bodies
responsesRefs map[string]*Reference // top level responses responsesRefs map[string]*Reference // top level responses
headersRefs map[string]*Reference // top level responses headersRefs map[string]*Reference // top level responses
examplesRefs map[string]*Reference // top level examples examplesRefs map[string]*Reference // top level examples
securityRequirementRefs map[string]map[string][]*Reference // (NOT $ref) but a name based lookup for requirements securityRequirementRefs map[string]map[string][]*Reference // (NOT $ref) but a name based lookup for requirements
callbacksRefs map[string]map[string][]*Reference // all links callbacksRefs map[string]map[string][]*Reference // all links
linksRefs map[string]map[string][]*Reference // all callbacks linksRefs map[string]map[string][]*Reference // all callbacks
operationTagsRefs map[string]map[string][]*Reference // tags found in operations operationTagsRefs map[string]map[string][]*Reference // tags found in operations
operationDescriptionRefs map[string]map[string]*Reference // descriptions in operations. operationDescriptionRefs map[string]map[string]*Reference // descriptions in operations.
operationSummaryRefs map[string]map[string]*Reference // summaries in operations operationSummaryRefs map[string]map[string]*Reference // summaries in operations
callbackRefs map[string]*Reference // top level callback refs callbackRefs map[string]*Reference // top level callback refs
serversRefs []*Reference // all top level server refs serversRefs []*Reference // all top level server refs
rootServersNode *yaml.Node // servers root node rootServersNode *yaml.Node // servers root node
opServersRefs map[string]map[string][]*Reference // all operation level server overrides. opServersRefs map[string]map[string][]*Reference // all operation level server overrides.
polymorphicRefs map[string]*Reference // every reference to a polymorphic ref polymorphicRefs map[string]*Reference // every reference to a polymorphic ref
polymorphicAllOfRefs []*Reference // every reference to 'allOf' references polymorphicAllOfRefs []*Reference // every reference to 'allOf' references
polymorphicOneOfRefs []*Reference // every reference to 'oneOf' references polymorphicOneOfRefs []*Reference // every reference to 'oneOf' references
polymorphicAnyOfRefs []*Reference // every reference to 'anyOf' references polymorphicAnyOfRefs []*Reference // every reference to 'anyOf' references
externalDocumentsRef []*Reference // all external documents in spec externalDocumentsRef []*Reference // all external documents in spec
rootSecurity []*Reference // root security definitions. rootSecurity []*Reference // root security definitions.
rootSecurityNode *yaml.Node // root security node. rootSecurityNode *yaml.Node // root security node.
refsWithSiblings map[string]Reference // references with sibling elements next to them refsWithSiblings map[string]Reference // references with sibling elements next to them
pathRefsLock sync.Mutex // create lock for all refs maps, we want to build data as fast as we can pathRefsLock sync.Mutex // create lock for all refs maps, we want to build data as fast as we can
externalDocumentsCount int // number of externalDocument nodes found externalDocumentsCount int // number of externalDocument nodes found
operationTagsCount int // number of unique tags in operations operationTagsCount int // number of unique tags in operations
globalTagsCount int // number of global tags defined globalTagsCount int // number of global tags defined
totalTagsCount int // number unique tags in spec totalTagsCount int // number unique tags in spec
securitySchemesCount int // security schemes securitySchemesCount int // security schemes
globalRequestBodiesCount int // component request bodies globalRequestBodiesCount int // component request bodies
globalResponsesCount int // component responses globalResponsesCount int // component responses
globalHeadersCount int // component headers globalHeadersCount int // component headers
globalExamplesCount int // component examples globalExamplesCount int // component examples
globalLinksCount int // component links globalLinksCount int // component links
globalCallbacksCount int // component callbacks globalCallbacksCount int // component callbacks
globalCallbacks int // component callbacks. globalCallbacks int // component callbacks.
pathCount int // number of paths pathCount int // number of paths
operationCount int // number of operations operationCount int // number of operations
operationParamCount int // number of params defined in operations operationParamCount int // number of params defined in operations
componentParamCount int // number of params defined in components componentParamCount int // number of params defined in components
componentsInlineParamUniqueCount int // number of inline params with unique names componentsInlineParamUniqueCount int // number of inline params with unique names
componentsInlineParamDuplicateCount int // number of inline params with duplicate names componentsInlineParamDuplicateCount int // number of inline params with duplicate names
schemaCount int // number of schemas schemaCount int // number of schemas
refCount int // total ref count refCount int // total ref count
root *yaml.Node // the root document root *yaml.Node // the root document
pathsNode *yaml.Node // paths node pathsNode *yaml.Node // paths node
tagsNode *yaml.Node // tags node tagsNode *yaml.Node // tags node
componentsNode *yaml.Node // components node componentsNode *yaml.Node // components node
parametersNode *yaml.Node // components/parameters node parametersNode *yaml.Node // components/parameters node
allParametersNode map[string]*Reference // all parameters node allParametersNode map[string]*Reference // all parameters node
allParameters map[string]*Reference // all parameters (components/defs) allParameters map[string]*Reference // all parameters (components/defs)
schemasNode *yaml.Node // components/schemas node schemasNode *yaml.Node // components/schemas node
allInlineSchemaDefinitions []*Reference // all schemas found in document outside of components (openapi) or definitions (swagger). allInlineSchemaDefinitions []*Reference // all schemas found in document outside of components (openapi) or definitions (swagger).
allInlineSchemaObjectDefinitions []*Reference // all schemas that are objects found in document outside of components (openapi) or definitions (swagger). allInlineSchemaObjectDefinitions []*Reference // all schemas that are objects found in document outside of components (openapi) or definitions (swagger).
allComponentSchemaDefinitions map[string]*Reference // all schemas found in components (openapi) or definitions (swagger). allComponentSchemaDefinitions map[string]*Reference // all schemas found in components (openapi) or definitions (swagger).
securitySchemesNode *yaml.Node // components/securitySchemes node securitySchemesNode *yaml.Node // components/securitySchemes node
allSecuritySchemes map[string]*Reference // all security schemes / definitions. allSecuritySchemes map[string]*Reference // all security schemes / definitions.
requestBodiesNode *yaml.Node // components/requestBodies node requestBodiesNode *yaml.Node // components/requestBodies node
allRequestBodies map[string]*Reference // all request bodies allRequestBodies map[string]*Reference // all request bodies
responsesNode *yaml.Node // components/responses node responsesNode *yaml.Node // components/responses node
allResponses map[string]*Reference // all responses allResponses map[string]*Reference // all responses
headersNode *yaml.Node // components/headers node headersNode *yaml.Node // components/headers node
allHeaders map[string]*Reference // all headers allHeaders map[string]*Reference // all headers
examplesNode *yaml.Node // components/examples node examplesNode *yaml.Node // components/examples node
allExamples map[string]*Reference // all components examples allExamples map[string]*Reference // all components examples
linksNode *yaml.Node // components/links node linksNode *yaml.Node // components/links node
allLinks map[string]*Reference // all links allLinks map[string]*Reference // all links
callbacksNode *yaml.Node // components/callbacks node callbacksNode *yaml.Node // components/callbacks node
allCallbacks map[string]*Reference // all components examples allCallbacks map[string]*Reference // all components examples
externalDocumentsNode *yaml.Node // external documents node externalDocumentsNode *yaml.Node // external documents node
allExternalDocuments map[string]*Reference // all external documents allExternalDocuments map[string]*Reference // all external documents
externalSpecIndex map[string]*SpecIndex // create a primary index of all external specs and componentIds externalSpecIndex map[string]*SpecIndex // create a primary index of all external specs and componentIds
refErrors []error // errors when indexing references refErrors []error // errors when indexing references
operationParamErrors []error // errors when indexing parameters operationParamErrors []error // errors when indexing parameters
allDescriptions []*DescriptionReference // every single description found in the spec. allDescriptions []*DescriptionReference // every single description found in the spec.
allSummaries []*DescriptionReference // every single summary found in the spec. allSummaries []*DescriptionReference // every single summary found in the spec.
allEnums []*EnumReference // every single enum found in the spec. allEnums []*EnumReference // every single enum found in the spec.
allObjectsWithProperties []*ObjectReference // every single object with properties found in the spec. allObjectsWithProperties []*ObjectReference // every single object with properties found in the spec.
enumCount int enumCount int
descriptionCount int descriptionCount int
summaryCount int summaryCount int
seenRemoteSources map[string]*yaml.Node seenRemoteSources map[string]*yaml.Node
seenLocalSources map[string]*yaml.Node seenLocalSources map[string]*yaml.Node
refLock sync.Mutex refLock sync.Mutex
sourceLock sync.Mutex sourceLock sync.Mutex
componentLock sync.RWMutex componentLock sync.RWMutex
externalLock sync.RWMutex externalLock sync.RWMutex
errorLock sync.RWMutex errorLock sync.RWMutex
circularReferences []*CircularReferenceResult // only available when the resolver has been used. circularReferences []*CircularReferenceResult // only available when the resolver has been used.
allowCircularReferences bool // decide if you want to error out, or allow circular references, default is false. allowCircularReferences bool // decide if you want to error out, or allow circular references, default is false.
relativePath string // relative path of the spec file. relativePath string // relative path of the spec file.
config *SpecIndexConfig // configuration for the index config *SpecIndexConfig // configuration for the index
httpClient *http.Client httpClient *http.Client
componentIndexChan chan bool componentIndexChan chan bool
polyComponentIndexChan chan bool polyComponentIndexChan chan bool
// when things get complex (looking at you digital ocean) then we need to know // when things get complex (looking at you digital ocean) then we need to know
// what we have seen across indexes, so we need to be able to travel back up to the root // what we have seen across indexes, so we need to be able to travel back up to the root
// cto avoid re-downloading sources. // cto avoid re-downloading sources.
parentIndex *SpecIndex parentIndex *SpecIndex
children []*SpecIndex uri []string
children []*SpecIndex
} }
func (index *SpecIndex) AddChild(child *SpecIndex) { func (index *SpecIndex) AddChild(child *SpecIndex) {
index.children = append(index.children, child) index.children = append(index.children, child)
} }
// GetChildren returns the children of this index. // GetChildren returns the children of this index.
func (index *SpecIndex) GetChildren() []*SpecIndex { func (index *SpecIndex) GetChildren() []*SpecIndex {
return index.children return index.children
} }
// ExternalLookupFunction is for lookup functions that take a JSONSchema reference and tries to find that node in the // ExternalLookupFunction is for lookup functions that take a JSONSchema reference and tries to find that node in the
@@ -239,35 +245,35 @@ type ExternalLookupFunction func(id string) (foundNode *yaml.Node, rootNode *yam
// IndexingError holds data about something that went wrong during indexing. // IndexingError holds data about something that went wrong during indexing.
type IndexingError struct { type IndexingError struct {
Err error Err error
Node *yaml.Node Node *yaml.Node
Path string Path string
} }
func (i *IndexingError) Error() string { func (i *IndexingError) Error() string {
return i.Err.Error() return i.Err.Error()
} }
// DescriptionReference holds data about a description that was found and where it was found. // DescriptionReference holds data about a description that was found and where it was found.
type DescriptionReference struct { type DescriptionReference struct {
Content string Content string
Path string Path string
Node *yaml.Node Node *yaml.Node
IsSummary bool IsSummary bool
} }
type EnumReference struct { type EnumReference struct {
Node *yaml.Node Node *yaml.Node
Type *yaml.Node Type *yaml.Node
Path string Path string
SchemaNode *yaml.Node SchemaNode *yaml.Node
ParentNode *yaml.Node ParentNode *yaml.Node
} }
type ObjectReference struct { type ObjectReference struct {
Node *yaml.Node Node *yaml.Node
Path string Path string
ParentNode *yaml.Node ParentNode *yaml.Node
} }
var methodTypes = []string{"get", "post", "put", "patch", "options", "head", "delete"} var methodTypes = []string{"get", "post", "put", "patch", "options", "head", "delete"}

View File

@@ -3,37 +3,32 @@
package index package index
import "gopkg.in/yaml.v3"
// SearchIndexForReference searches the index for a reference, first looking through the mapped references // SearchIndexForReference searches the index for a reference, first looking through the mapped references
// and then externalSpecIndex for a match. If no match is found, it will recursively search the child indexes // and then externalSpecIndex for a match. If no match is found, it will recursively search the child indexes
// extracted when parsing the OpenAPI Spec. // extracted when parsing the OpenAPI Spec.
func (index *SpecIndex) SearchIndexForReference(ref string) []*Reference { func (index *SpecIndex) SearchIndexForReference(ref string) []*Reference {
if r, ok := index.allMappedRefs[ref]; ok { if r, ok := index.allMappedRefs[ref]; ok {
if r.Node.Kind == yaml.DocumentNode { return []*Reference{r}
// the reference is an entire document, so we need to dig down a level and rewire the reference. }
r.Node = r.Node.Content[0] for c := range index.children {
} found := goFindMeSomething(index.children[c], ref)
return []*Reference{r} if found != nil {
} return found
if r, ok := index.externalSpecIndex[ref]; ok { }
return []*Reference{ }
{ return nil
Node: r.root.Content[0], }
Name: ref,
Definition: ref, func (index *SpecIndex) SearchAncestryForSeenURI(uri string) *SpecIndex {
}, if index.parentIndex == nil {
} return nil
} }
for c := range index.children { if index.uri[0] != uri {
found := goFindMeSomething(index.children[c], ref) return index.parentIndex.SearchAncestryForSeenURI(uri)
if found != nil { }
return found return index
}
}
return nil
} }
func goFindMeSomething(i *SpecIndex, ref string) []*Reference { func goFindMeSomething(i *SpecIndex, ref string) []*Reference {
return i.SearchIndexForReference(ref) return i.SearchIndexForReference(ref)
} }

View File

@@ -4,50 +4,50 @@
package index package index
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"io/ioutil" "net/url"
"net/url" "os"
"testing" "testing"
) )
func TestSpecIndex_SearchIndexForReference(t *testing.T) { func TestSpecIndex_SearchIndexForReference(t *testing.T) {
petstore, _ := ioutil.ReadFile("../test_specs/petstorev3.json") petstore, _ := os.ReadFile("../test_specs/petstorev3.json")
var rootNode yaml.Node var rootNode yaml.Node
_ = yaml.Unmarshal(petstore, &rootNode) _ = yaml.Unmarshal(petstore, &rootNode)
c := CreateOpenAPIIndexConfig() c := CreateOpenAPIIndexConfig()
idx := NewSpecIndexWithConfig(&rootNode, c) idx := NewSpecIndexWithConfig(&rootNode, c)
ref := idx.SearchIndexForReference("#/components/schemas/Pet") ref := idx.SearchIndexForReference("#/components/schemas/Pet")
assert.NotNil(t, ref) assert.NotNil(t, ref)
} }
func TestSpecIndex_SearchIndexForReference_ExternalSpecs(t *testing.T) { func TestSpecIndex_SearchIndexForReference_ExternalSpecs(t *testing.T) {
// load up an index with lots of references // load up an index with lots of references
petstore, _ := ioutil.ReadFile("../test_specs/digitalocean.yaml") petstore, _ := os.ReadFile("../test_specs/digitalocean.yaml")
var rootNode yaml.Node var rootNode yaml.Node
_ = yaml.Unmarshal(petstore, &rootNode) _ = yaml.Unmarshal(petstore, &rootNode)
c := CreateOpenAPIIndexConfig() c := CreateOpenAPIIndexConfig()
c.BaseURL, _ = url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification") c.BaseURL, _ = url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification")
idx := NewSpecIndexWithConfig(&rootNode, c) idx := NewSpecIndexWithConfig(&rootNode, c)
ref := idx.SearchIndexForReference("resources/apps/apps_list_instanceSizes.yml") ref := idx.SearchIndexForReference("resources/apps/apps_list_instanceSizes.yml")
assert.NotNil(t, ref) assert.NotNil(t, ref)
assert.Equal(t, "operationId", ref[0].Node.Content[0].Value) assert.Equal(t, "operationId", ref[0].Node.Content[0].Value)
ref = idx.SearchIndexForReference("examples/ruby/domains_create.yml") ref = idx.SearchIndexForReference("examples/ruby/domains_create.yml")
assert.NotNil(t, ref) assert.NotNil(t, ref)
assert.Equal(t, "lang", ref[0].Node.Content[0].Value) assert.Equal(t, "lang", ref[0].Node.Content[0].Value)
ref = idx.SearchIndexForReference("../../shared/responses/server_error.yml") ref = idx.SearchIndexForReference("../../shared/responses/server_error.yml")
assert.NotNil(t, ref) assert.NotNil(t, ref)
assert.Equal(t, "description", ref[0].Node.Content[0].Value) assert.Equal(t, "description", ref[0].Node.Content[0].Value)
ref = idx.SearchIndexForReference("../models/options.yml") ref = idx.SearchIndexForReference("../models/options.yml")
assert.NotNil(t, ref) assert.NotNil(t, ref)
assert.Equal(t, "kubernetes_options", ref[0].Node.Content[0].Value) assert.Equal(t, "kubernetes_options", ref[0].Node.Content[0].Value)
} }

View File

@@ -32,6 +32,8 @@ func NewSpecIndexWithConfig(rootNode *yaml.Node, config *SpecIndexConfig) *SpecI
} }
config.remoteLock = &sync.Mutex{} config.remoteLock = &sync.Mutex{}
index.config = config index.config = config
index.parentIndex = config.ParentIndex
index.uri = config.uri
if rootNode == nil || len(rootNode.Content) <= 0 { if rootNode == nil || len(rootNode.Content) <= 0 {
return index return index
} }
@@ -47,7 +49,7 @@ func NewSpecIndexWithConfig(rootNode *yaml.Node, config *SpecIndexConfig) *SpecI
// defaults to allowing remote references and file references. This is a potential security risk and should be controlled by // defaults to allowing remote references and file references. This is a potential security risk and should be controlled by
// providing a SpecIndexConfig that explicitly sets the AllowRemoteLookup and AllowFileLookup to true. // providing a SpecIndexConfig that explicitly sets the AllowRemoteLookup and AllowFileLookup to true.
// This function also does not support specifications with relative references that may not exist locally. // This function also does not support specifications with relative references that may not exist locally.
// - https://github.com/pb33f/libopenapi/issues/73 // - https://github.com/pb33f/libopenapi/issues/73
func NewSpecIndex(rootNode *yaml.Node) *SpecIndex { func NewSpecIndex(rootNode *yaml.Node) *SpecIndex {
index := new(SpecIndex) index := new(SpecIndex)
index.config = CreateOpenAPIIndexConfig() index.config = CreateOpenAPIIndexConfig()

View File

@@ -86,7 +86,7 @@ func TestSpecIndex_Asana(t *testing.T) {
} }
func TestSpecIndex_DigitalOcean(t *testing.T) { func TestSpecIndex_DigitalOcean(t *testing.T) {
do, _ := ioutil.ReadFile("../test_specs/digitalocean.yaml") do, _ := os.ReadFile("../test_specs/digitalocean.yaml")
var rootNode yaml.Node var rootNode yaml.Node
_ = yaml.Unmarshal(do, &rootNode) _ = yaml.Unmarshal(do, &rootNode)
@@ -603,7 +603,6 @@ func TestSpecIndex_FindComponenth(t *testing.T) {
assert.Nil(t, index.FindComponent("I-do-not-exist", nil)) assert.Nil(t, index.FindComponent("I-do-not-exist", nil))
} }
func TestSpecIndex_TestPathsNodeAsArray(t *testing.T) { func TestSpecIndex_TestPathsNodeAsArray(t *testing.T) {
yml := `components: yml := `components:
schemas: schemas:
@@ -633,21 +632,21 @@ func TestSpecIndex_lookupRemoteReference_SeenSourceSimulation_BadFind(t *testing
index := new(SpecIndex) index := new(SpecIndex)
index.seenRemoteSources = make(map[string]*yaml.Node) index.seenRemoteSources = make(map[string]*yaml.Node)
index.seenRemoteSources["https://no-hope-for-a-dope.com"] = &yaml.Node{} index.seenRemoteSources["https://no-hope-for-a-dope.com"] = &yaml.Node{}
a, b, err := index.lookupRemoteReference("https://no-hope-for-a-dope.com#/hey") a, b, err := index.lookupRemoteReference("https://no-hope-for-a-dope.com#/hey")
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, a) assert.Nil(t, a)
assert.Nil(t, b) assert.Nil(t, b)
} }
// Discovered in issue https://github.com/pb33f/libopenapi/issues/37 // Discovered in issue https://github.com/pb33f/libopenapi/issues/37
func TestSpecIndex_lookupRemoteReference_NoComponent(t *testing.T) { func TestSpecIndex_lookupRemoteReference_NoComponent(t *testing.T) {
index := new(SpecIndex) index := new(SpecIndex)
index.seenRemoteSources = make(map[string]*yaml.Node) index.seenRemoteSources = make(map[string]*yaml.Node)
index.seenRemoteSources["https://api.rest.sh/schemas/ErrorModel.json"] = &yaml.Node{} index.seenRemoteSources["https://api.rest.sh/schemas/ErrorModel.json"] = &yaml.Node{}
a, b, err := index.lookupRemoteReference("https://api.rest.sh/schemas/ErrorModel.json") a, b, err := index.lookupRemoteReference("https://api.rest.sh/schemas/ErrorModel.json")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, a) assert.NotNil(t, a)
assert.NotNil(t, b) assert.NotNil(t, b)
} }
// Discovered in issue https://github.com/daveshanley/vacuum/issues/225 // Discovered in issue https://github.com/daveshanley/vacuum/issues/225
@@ -669,47 +668,47 @@ func TestSpecIndex_lookupFileReference_NoComponent(t *testing.T) {
func TestSpecIndex_CheckBadURLRef(t *testing.T) { func TestSpecIndex_CheckBadURLRef(t *testing.T) {
yml := `openapi: 3.1.0 yml := `openapi: 3.1.0
paths: paths:
/cakes: /cakes:
post: post:
parameters: parameters:
- $ref: 'httpsss://badurl'` - $ref: 'httpsss://badurl'`
var rootNode yaml.Node var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode) _ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig()) index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.refErrors, 2) assert.Len(t, index.refErrors, 2)
} }
func TestSpecIndex_CheckBadURLRefNoRemoteAllowed(t *testing.T) { func TestSpecIndex_CheckBadURLRefNoRemoteAllowed(t *testing.T) {
yml := `openapi: 3.1.0 yml := `openapi: 3.1.0
paths: paths:
/cakes: /cakes:
post: post:
parameters: parameters:
- $ref: 'httpsss://badurl'` - $ref: 'httpsss://badurl'`
var rootNode yaml.Node var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode) _ = yaml.Unmarshal([]byte(yml), &rootNode)
c := CreateClosedAPIIndexConfig() c := CreateClosedAPIIndexConfig()
idx := NewSpecIndexWithConfig(&rootNode, c) idx := NewSpecIndexWithConfig(&rootNode, c)
assert.Len(t, idx.refErrors, 2) assert.Len(t, idx.refErrors, 2)
assert.Equal(t, "remote lookups are not permitted, "+ assert.Equal(t, "remote lookups are not permitted, "+
"please set AllowRemoteLookup to true in the configuration", idx.refErrors[0].Error()) "please set AllowRemoteLookup to true in the configuration", idx.refErrors[0].Error())
} }
func TestSpecIndex_CheckIndexDiscoversNoComponentLocalFileReference(t *testing.T) { func TestSpecIndex_CheckIndexDiscoversNoComponentLocalFileReference(t *testing.T) {
_ = ioutil.WriteFile("coffee-time.yaml", []byte("name: time for coffee"), 0o664) _ = ioutil.WriteFile("coffee-time.yaml", []byte("name: time for coffee"), 0o664)
defer os.Remove("coffee-time.yaml") defer os.Remove("coffee-time.yaml")
yml := `openapi: 3.0.3 yml := `openapi: 3.0.3
paths: paths:
/cakes: /cakes:
post: post:

View File

@@ -1,170 +1,170 @@
package resolver package resolver
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"testing" "testing"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func TestNewResolver(t *testing.T) { func TestNewResolver(t *testing.T) {
assert.Nil(t, NewResolver(nil)) assert.Nil(t, NewResolver(nil))
} }
func Benchmark_ResolveDocumentStripe(b *testing.B) { func Benchmark_ResolveDocumentStripe(b *testing.B) {
stripe, _ := ioutil.ReadFile("../test_specs/stripe.yaml") stripe, _ := ioutil.ReadFile("../test_specs/stripe.yaml")
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(stripe, &rootNode) yaml.Unmarshal(stripe, &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
resolver.Resolve() resolver.Resolve()
} }
} }
func TestResolver_ResolveComponents_CircularSpec(t *testing.T) { func TestResolver_ResolveComponents_CircularSpec(t *testing.T) {
circular, _ := ioutil.ReadFile("../test_specs/circular-tests.yaml") circular, _ := ioutil.ReadFile("../test_specs/circular-tests.yaml")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(circular, &rootNode) yaml.Unmarshal(circular, &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.Resolve() circ := resolver.Resolve()
assert.Len(t, circ, 3) assert.Len(t, circ, 3)
_, err := yaml.Marshal(resolver.resolvedRoot) _, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestResolver_CheckForCircularReferences(t *testing.T) { func TestResolver_CheckForCircularReferences(t *testing.T) {
circular, _ := ioutil.ReadFile("../test_specs/circular-tests.yaml") circular, _ := ioutil.ReadFile("../test_specs/circular-tests.yaml")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(circular, &rootNode) yaml.Unmarshal(circular, &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences() circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 3) assert.Len(t, circ, 3)
assert.Len(t, resolver.GetResolvingErrors(), 3) assert.Len(t, resolver.GetResolvingErrors(), 3)
assert.Len(t, resolver.GetCircularErrors(), 3) assert.Len(t, resolver.GetCircularErrors(), 3)
_, err := yaml.Marshal(resolver.resolvedRoot) _, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestResolver_CheckForCircularReferences_DigitalOcean(t *testing.T) { func TestResolver_CheckForCircularReferences_DigitalOcean(t *testing.T) {
circular, _ := ioutil.ReadFile("../test_specs/digitalocean.yaml") circular, _ := ioutil.ReadFile("../test_specs/digitalocean.yaml")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(circular, &rootNode) yaml.Unmarshal(circular, &rootNode)
baseURL, _ := url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification") baseURL, _ := url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification")
index := index.NewSpecIndexWithConfig(&rootNode, &index.SpecIndexConfig{ index := index.NewSpecIndexWithConfig(&rootNode, &index.SpecIndexConfig{
AllowRemoteLookup: true, AllowRemoteLookup: true,
AllowFileLookup: true, AllowFileLookup: true,
BaseURL: baseURL, BaseURL: baseURL,
}) })
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences() circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0) assert.Len(t, circ, 0)
assert.Len(t, resolver.GetResolvingErrors(), 0) assert.Len(t, resolver.GetResolvingErrors(), 0)
assert.Len(t, resolver.GetCircularErrors(), 0) assert.Len(t, resolver.GetCircularErrors(), 0)
_, err := yaml.Marshal(resolver.resolvedRoot) _, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestResolver_CircularReferencesRequiredValid(t *testing.T) { func TestResolver_CircularReferencesRequiredValid(t *testing.T) {
circular, _ := ioutil.ReadFile("../test_specs/swagger-valid-recursive-model.yaml") circular, _ := ioutil.ReadFile("../test_specs/swagger-valid-recursive-model.yaml")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(circular, &rootNode) yaml.Unmarshal(circular, &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences() circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0) assert.Len(t, circ, 0)
_, err := yaml.Marshal(resolver.resolvedRoot) _, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestResolver_CircularReferencesRequiredInvalid(t *testing.T) { func TestResolver_CircularReferencesRequiredInvalid(t *testing.T) {
circular, _ := ioutil.ReadFile("../test_specs/swagger-invalid-recursive-model.yaml") circular, _ := ioutil.ReadFile("../test_specs/swagger-invalid-recursive-model.yaml")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(circular, &rootNode) yaml.Unmarshal(circular, &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences() circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 2) assert.Len(t, circ, 2)
_, err := yaml.Marshal(resolver.resolvedRoot) _, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestResolver_DeepJourney(t *testing.T) { func TestResolver_DeepJourney(t *testing.T) {
var journey []*index.Reference var journey []*index.Reference
for f := 0; f < 200; f++ { for f := 0; f < 200; f++ {
journey = append(journey, nil) journey = append(journey, nil)
} }
index := index.NewSpecIndex(nil) index := index.NewSpecIndex(nil)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.Nil(t, resolver.extractRelatives(nil, nil, journey, false)) assert.Nil(t, resolver.extractRelatives(nil, nil, journey, false))
} }
func TestResolver_ResolveComponents_Stripe(t *testing.T) { func TestResolver_ResolveComponents_Stripe(t *testing.T) {
stripe, _ := ioutil.ReadFile("../test_specs/stripe.yaml") stripe, _ := ioutil.ReadFile("../test_specs/stripe.yaml")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(stripe, &rootNode) yaml.Unmarshal(stripe, &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.Resolve() circ := resolver.Resolve()
assert.Len(t, circ, 3) assert.Len(t, circ, 3)
assert.Len(t, resolver.GetNonPolymorphicCircularErrors(), 3) assert.Len(t, resolver.GetNonPolymorphicCircularErrors(), 3)
assert.Len(t, resolver.GetPolymorphicCircularErrors(), 0) assert.Len(t, resolver.GetPolymorphicCircularErrors(), 0)
} }
func TestResolver_ResolveComponents_BurgerShop(t *testing.T) { func TestResolver_ResolveComponents_BurgerShop(t *testing.T) {
mixedref, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") mixedref, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(mixedref, &rootNode) yaml.Unmarshal(mixedref, &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.Resolve() circ := resolver.Resolve()
assert.Len(t, circ, 0) assert.Len(t, circ, 0)
} }
func TestResolver_ResolveComponents_PolyNonCircRef(t *testing.T) { func TestResolver_ResolveComponents_PolyNonCircRef(t *testing.T) {
yml := `paths: yml := `paths:
/hey: /hey:
get: get:
responses: responses:
@@ -184,20 +184,20 @@ components:
tea: tea:
description: tea` description: tea`
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal([]byte(yml), &rootNode) yaml.Unmarshal([]byte(yml), &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences() circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0) assert.Len(t, circ, 0)
} }
func TestResolver_ResolveComponents_PolyCircRef(t *testing.T) { func TestResolver_ResolveComponents_PolyCircRef(t *testing.T) {
yml := `openapi: 3.1.0 yml := `openapi: 3.1.0
components: components:
schemas: schemas:
cheese: cheese:
@@ -211,24 +211,24 @@ components:
tea: tea:
description: tea` description: tea`
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal([]byte(yml), &rootNode) yaml.Unmarshal([]byte(yml), &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
_ = resolver.CheckForCircularReferences() _ = resolver.CheckForCircularReferences()
resolver.circularReferences[0].IsInfiniteLoop = true // override resolver.circularReferences[0].IsInfiniteLoop = true // override
assert.Len(t, index.GetCircularReferences(), 1) assert.Len(t, index.GetCircularReferences(), 1)
assert.Len(t, resolver.GetPolymorphicCircularErrors(), 1) assert.Len(t, resolver.GetPolymorphicCircularErrors(), 1)
assert.Equal(t, 2, index.GetCircularReferences()[0].LoopIndex) assert.Equal(t, 2, index.GetCircularReferences()[0].LoopIndex)
} }
func TestResolver_ResolveComponents_Missing(t *testing.T) { func TestResolver_ResolveComponents_Missing(t *testing.T) {
yml := `paths: yml := `paths:
/hey: /hey:
get: get:
responses: responses:
@@ -247,93 +247,95 @@ components:
butter: butter:
$ref: 'go home, I am drunk'` $ref: 'go home, I am drunk'`
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal([]byte(yml), &rootNode) yaml.Unmarshal([]byte(yml), &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
err := resolver.Resolve() err := resolver.Resolve()
assert.Len(t, err, 1) assert.Len(t, err, 1)
assert.Equal(t, "cannot resolve reference `go home, I am drunk`, it's missing: $go home, I am drunk [18:11]", err[0].Error()) assert.Equal(t, "cannot resolve reference `go home, I am drunk`, it's missing: $go home, I am drunk [18:11]", err[0].Error())
} }
func TestResolver_ResolveComponents_MixedRef(t *testing.T) { func TestResolver_ResolveComponents_MixedRef(t *testing.T) {
mixedref, _ := ioutil.ReadFile("../test_specs/mixedref-burgershop.openapi.yaml") mixedref, _ := ioutil.ReadFile("../test_specs/mixedref-burgershop.openapi.yaml")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(mixedref, &rootNode) yaml.Unmarshal(mixedref, &rootNode)
b := index.CreateOpenAPIIndexConfig() b := index.CreateOpenAPIIndexConfig()
idx := index.NewSpecIndexWithConfig(&rootNode, b) idx := index.NewSpecIndexWithConfig(&rootNode, b)
resolver := NewResolver(idx) resolver := NewResolver(idx)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.Resolve() circ := resolver.Resolve()
assert.Len(t, circ, 0) assert.Len(t, circ, 0)
assert.Equal(t, 5, resolver.GetIndexesVisited()) assert.Equal(t, 5, resolver.GetIndexesVisited())
assert.Equal(t, 209, resolver.GetRelativesSeen())
assert.Equal(t, 35, resolver.GetJourneysTaken()) // in v0.8.2 a new check was added when indexing, to prevent re-indexing the same file multiple times.
assert.Equal(t, 62, resolver.GetReferenceVisited()) assert.Equal(t, 191, resolver.GetRelativesSeen())
assert.Equal(t, 35, resolver.GetJourneysTaken())
assert.Equal(t, 62, resolver.GetReferenceVisited())
} }
func TestResolver_ResolveComponents_k8s(t *testing.T) { func TestResolver_ResolveComponents_k8s(t *testing.T) {
k8s, _ := ioutil.ReadFile("../test_specs/k8s.json") k8s, _ := ioutil.ReadFile("../test_specs/k8s.json")
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal(k8s, &rootNode) yaml.Unmarshal(k8s, &rootNode)
index := index.NewSpecIndex(&rootNode) index := index.NewSpecIndex(&rootNode)
resolver := NewResolver(index) resolver := NewResolver(index)
assert.NotNil(t, resolver) assert.NotNil(t, resolver)
circ := resolver.Resolve() circ := resolver.Resolve()
assert.Len(t, circ, 0) assert.Len(t, circ, 0)
} }
// Example of how to resolve the Stripe OpenAPI specification, and check for circular reference errors // Example of how to resolve the Stripe OpenAPI specification, and check for circular reference errors
func ExampleNewResolver() { func ExampleNewResolver() {
// create a yaml.Node reference as a root node. // create a yaml.Node reference as a root node.
var rootNode yaml.Node var rootNode yaml.Node
// load in the Stripe OpenAPI spec (lots of polymorphic complexity in here) // load in the Stripe OpenAPI spec (lots of polymorphic complexity in here)
stripeBytes, _ := ioutil.ReadFile("../test_specs/stripe.yaml") stripeBytes, _ := ioutil.ReadFile("../test_specs/stripe.yaml")
// unmarshal bytes into our rootNode. // unmarshal bytes into our rootNode.
_ = yaml.Unmarshal(stripeBytes, &rootNode) _ = yaml.Unmarshal(stripeBytes, &rootNode)
// create a new spec index (resolver depends on it) // create a new spec index (resolver depends on it)
indexConfig := index.CreateClosedAPIIndexConfig() indexConfig := index.CreateClosedAPIIndexConfig()
index := index.NewSpecIndexWithConfig(&rootNode, indexConfig) index := index.NewSpecIndexWithConfig(&rootNode, indexConfig)
// create a new resolver using the index. // create a new resolver using the index.
resolver := NewResolver(index) resolver := NewResolver(index)
// resolve the document, if there are circular reference errors, they are returned/ // resolve the document, if there are circular reference errors, they are returned/
// WARNING: this is a destructive action and the rootNode will be PERMANENTLY altered and cannot be unresolved // WARNING: this is a destructive action and the rootNode will be PERMANENTLY altered and cannot be unresolved
circularErrors := resolver.Resolve() circularErrors := resolver.Resolve()
// The Stripe API has a bunch of circular reference problems, mainly from polymorphism. // The Stripe API has a bunch of circular reference problems, mainly from polymorphism.
// So let's print them out. // So let's print them out.
// //
fmt.Printf("There are %d circular reference errors, %d of them are polymorphic errors, %d are not", fmt.Printf("There are %d circular reference errors, %d of them are polymorphic errors, %d are not",
len(circularErrors), len(resolver.GetPolymorphicCircularErrors()), len(resolver.GetNonPolymorphicCircularErrors())) len(circularErrors), len(resolver.GetPolymorphicCircularErrors()), len(resolver.GetNonPolymorphicCircularErrors()))
// Output: There are 3 circular reference errors, 0 of them are polymorphic errors, 3 are not // Output: There are 3 circular reference errors, 0 of them are polymorphic errors, 3 are not
} }
func ExampleResolvingError() { func ExampleResolvingError() {
re := ResolvingError{ re := ResolvingError{
ErrorRef: errors.New("Je suis une erreur"), ErrorRef: errors.New("Je suis une erreur"),
Node: &yaml.Node{ Node: &yaml.Node{
Line: 5, Line: 5,
Column: 21, Column: 21,
}, },
Path: "#/definitions/JeSuisUneErreur", Path: "#/definitions/JeSuisUneErreur",
CircularReference: &index.CircularReferenceResult{}, CircularReference: &index.CircularReferenceResult{},
} }
fmt.Printf("%s", re.Error()) fmt.Printf("%s", re.Error())
// Output: Je suis une erreur: #/definitions/JeSuisUneErreur [5:21] // Output: Je suis une erreur: #/definitions/JeSuisUneErreur [5:21]
} }

33
test_specs/first.yaml Normal file
View File

@@ -0,0 +1,33 @@
openapi: 3.0.0
info:
title: title
description: description
version: 0.0.0
paths:
/items:
get:
tags:
- items
summary: summary
description: description
parameters: []
responses:
'200':
description: OK
content:
application/json:
schema:
title: Schema
description: description
type: object
additionalProperties:
type: object
title: first title
description: first description
additionalProperties: false
properties:
second:
$ref: "second.yaml"

31
test_specs/second.yaml Normal file
View File

@@ -0,0 +1,31 @@
title: second doc title
description: second doc description
type: object
additionalProperties: false
properties:
property1:
title: title
description: property 1 description
type: array
items:
title: item
description: third description
type: object
additionalProperties: false
properties:
details:
$ref: "third.yaml"
property2:
title: title
description: property 2 description
type: object
additionalProperties: false
properties:
property:
title: title
description: tasty description
type: integer

15
test_specs/third.yaml Normal file
View File

@@ -0,0 +1,15 @@
title: third doc title
description: third doc description
type: object
additionalProperties: false
maxProperties: 1
properties:
property:
title: title of third prop in third doc
type: object
properties:
statistics:
$ref: 'second.yaml#/properties/property2'

File diff suppressed because it is too large Load Diff