diff --git a/index/extract_refs.go b/index/extract_refs.go index b1c4fcb..984b94d 100644 --- a/index/extract_refs.go +++ b/index/extract_refs.go @@ -4,430 +4,429 @@ package index import ( - "errors" - "fmt" - "github.com/pb33f/libopenapi/utils" - "gopkg.in/yaml.v3" - "strings" + "errors" + "fmt" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" + "strings" ) // 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() func (index *SpecIndex) ExtractRefs(node, parent *yaml.Node, seenPath []string, level int, poly bool, pName string) []*Reference { - if node == nil { - return nil - } - var found []*Reference - if len(node.Content) > 0 { - var prev, polyName string - for i, n := range node.Content { + if node == nil { + return nil + } + var found []*Reference + if len(node.Content) > 0 { + var prev, polyName string + for i, n := range node.Content { - if utils.IsNodeMap(n) || utils.IsNodeArray(n) { - level++ - // 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. - isPoly, _ := index.checkPolymorphicNode(prev) - polyName = pName - if isPoly { - poly = true - if prev != "" { - polyName = prev - } - } - found = append(found, index.ExtractRefs(n, node, seenPath, level, poly, polyName)...) - } + if utils.IsNodeMap(n) || utils.IsNodeArray(n) { + level++ + // 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. + isPoly, _ := index.checkPolymorphicNode(prev) + polyName = pName + if isPoly { + poly = true + if prev != "" { + polyName = prev + } + } + 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 - // (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 - if i%2 == 0 && n.Value == "schema" && !utils.IsNodeArray(node) && (i+1 < len(node.Content)) { - isRef, _, _ := utils.IsNodeRefValue(node.Content[i+1]) - if isRef { - continue - } - ref := &Reference{ - Node: node.Content[i+1], - Path: fmt.Sprintf("$.%s.schema", strings.Join(seenPath, ".")), - } - index.allInlineSchemaDefinitions = append(index.allInlineSchemaDefinitions, ref) + // 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) + // https://github.com/pb33f/libopenapi/issues/76 + if i%2 == 0 && n.Value == "schema" && !utils.IsNodeArray(node) && (i+1 < len(node.Content)) { + isRef, _, _ := utils.IsNodeRefValue(node.Content[i+1]) + if isRef { + continue + } + ref := &Reference{ + Node: node.Content[i+1], + Path: fmt.Sprintf("$.%s.schema", strings.Join(seenPath, ".")), + } + index.allInlineSchemaDefinitions = append(index.allInlineSchemaDefinitions, ref) - // check if the schema is an object or an array, - // and if so, add it to the list of inline schema object definitions. - k, v := utils.FindKeyNodeTop("type", node.Content[i+1].Content) - if k != nil && v != nil { - if v.Value == "object" || v.Value == "array" { - index.allInlineSchemaObjectDefinitions = append(index.allInlineSchemaObjectDefinitions, ref) - } - } - } + // check if the schema is an object or an array, + // and if so, add it to the list of inline schema object definitions. + k, v := utils.FindKeyNodeTop("type", node.Content[i+1].Content) + if k != nil && v != nil { + if v.Value == "object" || v.Value == "array" { + index.allInlineSchemaObjectDefinitions = append(index.allInlineSchemaObjectDefinitions, ref) + } + } + } - // Perform the same check for all properties in an inline schema definition - // https://github.com/pb33f/libopenapi/issues/76 - if i%2 == 0 && n.Value == "properties" && !utils.IsNodeArray(node) && (i+1 < len(node.Content)) { - isRef, _, _ := utils.IsNodeRefValue(node.Content[i+1]) - if isRef { - continue - } + // Perform the same check for all properties in an inline schema definition + // https://github.com/pb33f/libopenapi/issues/76 + if i%2 == 0 && n.Value == "properties" && !utils.IsNodeArray(node) && (i+1 < len(node.Content)) { + isRef, _, _ := utils.IsNodeRefValue(node.Content[i+1]) + if isRef { + continue + } - // for each property add it to our schema definitions - label := "" - for h, prop := range node.Content[i+1].Content { + // for each property add it to our schema definitions + label := "" + for h, prop := range node.Content[i+1].Content { - if h%2 == 0 { - label = prop.Value - continue - } + if h%2 == 0 { + label = prop.Value + continue + } - ref := &Reference{ - Node: prop, - Path: fmt.Sprintf("$.%s.properties.%s", strings.Join(seenPath, "."), label), - } - index.allInlineSchemaDefinitions = append(index.allInlineSchemaDefinitions, ref) + ref := &Reference{ + Node: prop, + Path: fmt.Sprintf("$.%s.properties.%s", strings.Join(seenPath, "."), label), + } + index.allInlineSchemaDefinitions = append(index.allInlineSchemaDefinitions, ref) - // check if the schema is an object or an array, - // and if so, add it to the list of inline schema object definitions. - k, v := utils.FindKeyNodeTop("type", node.Content[i+1].Content) - if k != nil && v != nil { - if v.Value == "object" || v.Value == "array" { - index.allInlineSchemaObjectDefinitions = append(index.allInlineSchemaObjectDefinitions, ref) - } - } - } - } + // check if the schema is an object or an array, + // and if so, add it to the list of inline schema object definitions. + k, v := utils.FindKeyNodeTop("type", node.Content[i+1].Content) + if k != nil && v != nil { + if v.Value == "object" || v.Value == "array" { + 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) - if !utils.IsNodeStringValue(node.Content[i+1]) { - continue - } + // only look at scalar values, not maps (looking at you k8s) + if !utils.IsNodeStringValue(node.Content[i+1]) { + continue + } - index.linesWithRefs[n.Line] = true + index.linesWithRefs[n.Line] = true - fp := make([]string, len(seenPath)) - for x, foundPathNode := range seenPath { - fp[x] = foundPathNode - } + fp := make([]string, len(seenPath)) + for x, foundPathNode := range seenPath { + fp[x] = foundPathNode + } - value := node.Content[i+1].Value + value := node.Content[i+1].Value - segs := strings.Split(value, "/") - name := segs[len(segs)-1] - _, p := utils.ConvertComponentIdIntoFriendlyPathSearch(value) - ref := &Reference{ - Definition: value, - Name: name, - Node: node, - Path: p, - } + segs := strings.Split(value, "/") + name := segs[len(segs)-1] + _, p := utils.ConvertComponentIdIntoFriendlyPathSearch(value) + ref := &Reference{ + Definition: value, + Name: name, + Node: node, + Path: p, + } - // add to raw sequenced refs - index.rawSequencedRefs = append(index.rawSequencedRefs, ref) + // add to raw sequenced refs + index.rawSequencedRefs = append(index.rawSequencedRefs, ref) - // add ref by line number - refNameIndex := strings.LastIndex(value, "/") - refName := value[refNameIndex+1:] - if len(index.refsByLine[refName]) > 0 { - index.refsByLine[refName][n.Line] = true - } else { - v := make(map[int]bool) - v[n.Line] = true - index.refsByLine[refName] = v - } + // add ref by line number + refNameIndex := strings.LastIndex(value, "/") + refName := value[refNameIndex+1:] + if len(index.refsByLine[refName]) > 0 { + index.refsByLine[refName][n.Line] = true + } else { + v := make(map[int]bool) + v[n.Line] = true + index.refsByLine[refName] = v + } - // if this ref value has any siblings (node.Content is larger than two elements) - // then add to refs with siblings - if len(node.Content) > 2 { - copiedNode := *node - copied := Reference{ - Definition: ref.Definition, - Name: ref.Name, - Node: &copiedNode, - Path: p, - } - // protect this data using a copy, prevent the resolver from destroying things. - index.refsWithSiblings[value] = copied - } + // if this ref value has any siblings (node.Content is larger than two elements) + // then add to refs with siblings + if len(node.Content) > 2 { + copiedNode := *node + copied := Reference{ + Definition: ref.Definition, + Name: ref.Name, + Node: &copiedNode, + Path: p, + } + // protect this data using a copy, prevent the resolver from destroying things. + index.refsWithSiblings[value] = copied + } - // 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 - // the timeline, we will keep each poly ref in its own collection for later - // analysis. - if poly { - index.polymorphicRefs[value] = ref + // 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 + // the timeline, we will keep each poly ref in its own collection for later + // analysis. + if poly { + index.polymorphicRefs[value] = ref - // index each type - switch pName { - case "anyOf": - index.polymorphicAnyOfRefs = append(index.polymorphicAnyOfRefs, ref) - case "allOf": - index.polymorphicAllOfRefs = append(index.polymorphicAllOfRefs, ref) - case "oneOf": - index.polymorphicOneOfRefs = append(index.polymorphicOneOfRefs, ref) - } - continue - } + // index each type + switch pName { + case "anyOf": + index.polymorphicAnyOfRefs = append(index.polymorphicAnyOfRefs, ref) + case "allOf": + index.polymorphicAllOfRefs = append(index.polymorphicAllOfRefs, ref) + case "oneOf": + index.polymorphicOneOfRefs = append(index.polymorphicOneOfRefs, ref) + } + continue + } - // check if this is a dupe, if so, skip it, we don't care now. - if index.allRefs[value] != nil { // seen before, skip. - continue - } + // check if this is a dupe, if so, skip it, we don't care now. + if index.allRefs[value] != nil { // seen before, skip. + continue + } - if value == "" { + if value == "" { - completedPath := fmt.Sprintf("$.%s", strings.Join(fp, ".")) + completedPath := fmt.Sprintf("$.%s", strings.Join(fp, ".")) - indexError := &IndexingError{ - Err: errors.New("schema reference is empty and cannot be processed"), - Node: node.Content[i+1], - Path: completedPath, - } + indexError := &IndexingError{ + Err: errors.New("schema reference is empty and cannot be processed"), + Node: node.Content[i+1], + Path: completedPath, + } - index.refErrors = append(index.refErrors, indexError) + index.refErrors = append(index.refErrors, indexError) - continue - } + continue + } - index.allRefs[value] = ref - found = append(found, ref) - } + index.allRefs[value] = 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 - if n.Value == "description" { + // capture descriptions and summaries + if n.Value == "description" { - // if the parent is a sequence, ignore. - if utils.IsNodeArray(node) { - continue - } + // if the parent is a sequence, ignore. + if utils.IsNodeArray(node) { + continue + } - ref := &DescriptionReference{ - Content: node.Content[i+1].Value, - Path: nodePath, - Node: node.Content[i+1], - IsSummary: false, - } + ref := &DescriptionReference{ + Content: node.Content[i+1].Value, + Path: nodePath, + Node: node.Content[i+1], + IsSummary: false, + } - if !utils.IsNodeMap(ref.Node) { - index.allDescriptions = append(index.allDescriptions, ref) - index.descriptionCount++ - } - } + if !utils.IsNodeMap(ref.Node) { + index.allDescriptions = append(index.allDescriptions, ref) + index.descriptionCount++ + } + } - if n.Value == "summary" { + if n.Value == "summary" { - var b *yaml.Node - if len(node.Content) == i+1 { - b = node.Content[i] - } else { - b = node.Content[i+1] - } - ref := &DescriptionReference{ - Content: b.Value, - Path: nodePath, - Node: b, - IsSummary: true, - } + var b *yaml.Node + if len(node.Content) == i+1 { + b = node.Content[i] + } else { + b = node.Content[i+1] + } + ref := &DescriptionReference{ + Content: b.Value, + Path: nodePath, + Node: b, + IsSummary: true, + } - index.allSummaries = append(index.allSummaries, ref) - index.summaryCount++ - } + index.allSummaries = append(index.allSummaries, ref) + index.summaryCount++ + } - // 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. - if n.Value == "security" { - var b *yaml.Node - if len(node.Content) == i+1 { - b = node.Content[i] - } else { - b = node.Content[i+1] - } - if utils.IsNodeArray(b) { - var secKey string - for k := range b.Content { - if utils.IsNodeMap(b.Content[k]) { - for g := range b.Content[k].Content { - if g%2 == 0 { - secKey = b.Content[k].Content[g].Value - continue - } - if utils.IsNodeArray(b.Content[k].Content[g]) { - var refMap map[string][]*Reference - if index.securityRequirementRefs[secKey] == nil { - index.securityRequirementRefs[secKey] = make(map[string][]*Reference) - refMap = index.securityRequirementRefs[secKey] - } else { - refMap = index.securityRequirementRefs[secKey] - } - for r := range b.Content[k].Content[g].Content { - var refs []*Reference - if refMap[b.Content[k].Content[g].Content[r].Value] != nil { - refs = refMap[b.Content[k].Content[g].Content[r].Value] - } + // 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. + if n.Value == "security" { + var b *yaml.Node + if len(node.Content) == i+1 { + b = node.Content[i] + } else { + b = node.Content[i+1] + } + if utils.IsNodeArray(b) { + var secKey string + for k := range b.Content { + if utils.IsNodeMap(b.Content[k]) { + for g := range b.Content[k].Content { + if g%2 == 0 { + secKey = b.Content[k].Content[g].Value + continue + } + if utils.IsNodeArray(b.Content[k].Content[g]) { + var refMap map[string][]*Reference + if index.securityRequirementRefs[secKey] == nil { + index.securityRequirementRefs[secKey] = make(map[string][]*Reference) + refMap = index.securityRequirementRefs[secKey] + } else { + refMap = index.securityRequirementRefs[secKey] + } + for r := range b.Content[k].Content[g].Content { + var refs []*Reference + if refMap[b.Content[k].Content[g].Content[r].Value] != nil { + refs = refMap[b.Content[k].Content[g].Content[r].Value] + } - refs = append(refs, &Reference{ - Definition: b.Content[k].Content[g].Content[r].Value, - Path: fmt.Sprintf("%s.security[%d].%s[%d]", nodePath, k, secKey, r), - Node: b.Content[k].Content[g].Content[r], - }) + refs = append(refs, &Reference{ + Definition: b.Content[k].Content[g].Content[r].Value, + Path: fmt.Sprintf("%s.security[%d].%s[%d]", nodePath, k, secKey, r), + Node: b.Content[k].Content[g].Content[r], + }) - index.securityRequirementRefs[secKey][b.Content[k].Content[g].Content[r].Value] = refs - } - } - } - } - } - } - } - // capture enums - if n.Value == "enum" { + index.securityRequirementRefs[secKey][b.Content[k].Content[g].Content[r].Value] = refs + } + } + } + } + } + } + } + // capture enums + if n.Value == "enum" { - // all enums need to have a type, extract the type from the node where the enum was found. - _, enumKeyValueNode := utils.FindKeyNodeTop("type", node.Content) + // all enums need to have a type, extract the type from the node where the enum was found. + _, enumKeyValueNode := utils.FindKeyNodeTop("type", node.Content) - if enumKeyValueNode != nil { - ref := &EnumReference{ - Path: nodePath, - Node: node.Content[i+1], - Type: enumKeyValueNode, - SchemaNode: node, - ParentNode: parent, - } + if enumKeyValueNode != nil { + ref := &EnumReference{ + Path: nodePath, + Node: node.Content[i+1], + Type: enumKeyValueNode, + SchemaNode: node, + ParentNode: parent, + } - index.allEnums = append(index.allEnums, ref) - index.enumCount++ - } - } - // capture all objects with properties - if n.Value == "properties" { - _, typeKeyValueNode := utils.FindKeyNodeTop("type", node.Content) + index.allEnums = append(index.allEnums, ref) + index.enumCount++ + } + } + // capture all objects with properties + if n.Value == "properties" { + _, typeKeyValueNode := utils.FindKeyNodeTop("type", node.Content) - if typeKeyValueNode != nil { - isObject := false + if typeKeyValueNode != nil { + isObject := false - if typeKeyValueNode.Value == "object" { - isObject = true - } + if typeKeyValueNode.Value == "object" { + isObject = true + } - for _, v := range typeKeyValueNode.Content { - if v.Value == "object" { - isObject = true - } - } + for _, v := range typeKeyValueNode.Content { + if v.Value == "object" { + isObject = true + } + } - if isObject { - index.allObjectsWithProperties = append(index.allObjectsWithProperties, &ObjectReference{ - Path: nodePath, - Node: node, - ParentNode: parent, - }) - } - } - } + if isObject { + index.allObjectsWithProperties = append(index.allObjectsWithProperties, &ObjectReference{ + Path: nodePath, + Node: node, + ParentNode: parent, + }) + } + } + } - seenPath = append(seenPath, n.Value) - prev = n.Value - } + seenPath = append(seenPath, n.Value) + prev = n.Value + } - // if next node is map, don't add segment. - if i < len(node.Content)-1 { - next := node.Content[i+1] + // if next node is map, don't add segment. + if i < len(node.Content)-1 { + next := node.Content[i+1] - if i%2 != 0 && next != nil && !utils.IsNodeArray(next) && !utils.IsNodeMap(next) { - seenPath = seenPath[:len(seenPath)-1] - } - } - } - if len(seenPath) > 0 { - seenPath = seenPath[:len(seenPath)-1] - } + if i%2 != 0 && next != nil && !utils.IsNodeArray(next) && !utils.IsNodeMap(next) { + seenPath = seenPath[:len(seenPath)-1] + } + } + } + if len(seenPath) > 0 { + seenPath = seenPath[:len(seenPath)-1] + } - } - if len(seenPath) > 0 { - seenPath = seenPath[:len(seenPath)-1] - } + } + if len(seenPath) > 0 { + 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 // can be used for resolving as they contain the actual object properties. 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 - 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 { + //run this async because when things get recursive, it can take a while + c := make(chan bool) - _, path := utils.ConvertComponentIdIntoFriendlyPathSearch(ref.Definition) - 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 - } + 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 { - var refsToCheck []*Reference - for _, ref := range refs { + _, path := utils.ConvertComponentIdIntoFriendlyPathSearch(ref.Definition) + 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!) - if strings.Contains(ref.Definition, "\\") { // this was from blazemeter.com haha! - _, 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 + var refsToCheck []*Reference + for _, ref := range refs { - } - refsToCheck = append(refsToCheck, ref) - } - mappedRefsInSequence := make([]*ReferenceMapped, len(refsToCheck)) + // check reference for backslashes (hah yeah seen this too!) + if strings.Contains(ref.Definition, "\\") { // this was from blazemeter.com haha! + _, 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 - for r := range refsToCheck { - // expand our index of all mapped refs - go locate(refsToCheck[r], r, mappedRefsInSequence) - //locate(refsToCheck[r], r, mappedRefsInSequence) // used for sync testing. - } + } + refsToCheck = append(refsToCheck, ref) + } + mappedRefsInSequence := make([]*ReferenceMapped, len(refsToCheck)) - completedRefs := 0 - for completedRefs < len(refsToCheck) { - select { - case <-c: - completedRefs++ - } - } - for m := range mappedRefsInSequence { - if mappedRefsInSequence[m] != nil { - index.allMappedRefsSequenced = append(index.allMappedRefsSequenced, mappedRefsInSequence[m]) - } - } - return found + for r := range refsToCheck { + // expand our index of all mapped refs + go locate(refsToCheck[r], r, mappedRefsInSequence) + //locate(refsToCheck[r], r, mappedRefsInSequence) // used for sync testing. + } + + completedRefs := 0 + for completedRefs < len(refsToCheck) { + select { + case <-c: + completedRefs++ + } + } + for m := range mappedRefsInSequence { + if mappedRefsInSequence[m] != nil { + index.allMappedRefsSequenced = append(index.allMappedRefsSequenced, mappedRefsInSequence[m]) + } + } + return found } - diff --git a/index/find_component.go b/index/find_component.go index d2fd219..50e2473 100644 --- a/index/find_component.go +++ b/index/find_component.go @@ -267,11 +267,16 @@ func (index *SpecIndex) FindComponentInRoot(componentId string) *Reference { return nil // no component found } res, _ := path.Find(index.root) + if len(res) == 1 { + resNode := res[0] + if res[0].Kind == yaml.DocumentNode { + resNode = res[0].Content[0] + } ref := &Reference{ Definition: componentId, Name: name, - Node: res[0], + Node: resNode, Path: friendlySearch, 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, - lookupFunction ExternalLookupFunction, parent *yaml.Node, -) *Reference { + lookupFunction ExternalLookupFunction, parent *yaml.Node) *Reference { if len(uri) > 0 { index.externalLock.RLock() externalSpecIndex := index.externalSpecIndex[uri[0]] @@ -349,21 +353,29 @@ func (index *SpecIndex) performExternalLookup(uri []string, componentId string, BasePath: newBasePath, AllowRemoteLookup: index.config.AllowRemoteLookup, AllowFileLookup: index.config.AllowFileLookup, + ParentIndex: index, seenRemoteSources: index.config.seenRemoteSources, remoteLock: index.config.remoteLock, + uri: uri, } var newIndex *SpecIndex - newIndex = NewSpecIndexWithConfig(newRoot, newConfig) - index.refLock.Lock() - index.externalLock.Lock() - index.externalSpecIndex[uri[0]] = newIndex - index.externalLock.Unlock() - newIndex.relativePath = path - newIndex.parentIndex = index - index.AddChild(newIndex) - index.refLock.Unlock() - externalSpecIndex = newIndex + seen := index.SearchAncestryForSeenURI(uri[0]) + if seen == nil { + + newIndex = NewSpecIndexWithConfig(newRoot, newConfig) + index.refLock.Lock() + index.externalLock.Lock() + index.externalSpecIndex[uri[0]] = newIndex + index.externalLock.Unlock() + newIndex.relativePath = path + newIndex.parentIndex = index + index.AddChild(newIndex) + index.refLock.Unlock() + externalSpecIndex = newIndex + } else { + externalSpecIndex = seen + } } } diff --git a/index/find_component_test.go b/index/find_component_test.go index 82ac5e2..048b104 100644 --- a/index/find_component_test.go +++ b/index/find_component_test.go @@ -6,6 +6,7 @@ package index import ( "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" + "os" "testing" ) @@ -26,6 +27,22 @@ func TestSpecIndex_performExternalLookup(t *testing.T) { 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) { yml := `openapi: 3.1.0 components: diff --git a/index/index_model.go b/index/index_model.go index c2fa6bc..ddb811b 100644 --- a/index/index_model.go +++ b/index/index_model.go @@ -4,77 +4,82 @@ package index import ( - "golang.org/x/sync/syncmap" - "gopkg.in/yaml.v3" - "net/http" - "net/url" - "os" - "sync" + "golang.org/x/sync/syncmap" + "gopkg.in/yaml.v3" + "net/http" + "net/url" + "os" + "sync" ) // Constants used to determine if resolving is local, file based or remote file based. const ( - LocalResolve = iota - HttpResolve - FileResolve + LocalResolve = iota + HttpResolve + FileResolve ) // 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. type Reference struct { - Definition string - Name string - Node *yaml.Node - ParentNode *yaml.Node - Resolved bool - Circular bool - Seen bool - IsRemote bool - RemoteLocation string - 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 + Definition string + Name string + Node *yaml.Node + ParentNode *yaml.Node + Resolved bool + Circular bool + Seen bool + IsRemote bool + RemoteLocation string + 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 } // ReferenceMapped is a helper struct for mapped references put into sequence (we lose the key) type ReferenceMapped struct { - Reference *Reference - Definition string + Reference *Reference + Definition string } // 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 // 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 { - // The BaseURL will be the root from which relative references will be resolved from if they can't be found locally. - // - // For example: - // - $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). - // 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: - // - $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 - BaseURL *url.URL // set the Base URL for resolving relative references if the spec is exploded. + // The BaseURL will be the root from which relative references will be resolved from if they can't be found locally. + // + // For example: + // - $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). + // 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: + // - $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 + 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 - BasePath string // set the Base Path 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 + 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 - // 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 - // 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 - AllowRemoteLookup bool // Allow remote lookups for references. Defaults to false - AllowFileLookup bool // Allow file lookups for references. Defaults to false + // 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 + // 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. + // + // 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 + AllowFileLookup bool // Allow file lookups for references. Defaults to false - // private fields - seenRemoteSources *syncmap.Map - remoteLock *sync.Mutex + // ParentIndex allows the index to be created with knowledge of a parent, before being parsed. This allows + // a breakglass to be used to prevent loops, checking the tree before recursing down. + 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 @@ -82,13 +87,13 @@ type SpecIndexConfig struct { // // The default BasePath is the current working directory. func CreateOpenAPIIndexConfig() *SpecIndexConfig { - cw, _ := os.Getwd() - return &SpecIndexConfig{ - BasePath: cw, - AllowRemoteLookup: true, - AllowFileLookup: true, - seenRemoteSources: &syncmap.Map{}, - } + cw, _ := os.Getwd() + return &SpecIndexConfig{ + BasePath: cw, + AllowRemoteLookup: true, + AllowFileLookup: true, + seenRemoteSources: &syncmap.Map{}, + } } // 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. func CreateClosedAPIIndexConfig() *SpecIndexConfig { - cw, _ := os.Getwd() - return &SpecIndexConfig{ - BasePath: cw, - AllowRemoteLookup: false, - AllowFileLookup: false, - seenRemoteSources: &syncmap.Map{}, - } + cw, _ := os.Getwd() + return &SpecIndexConfig{ + BasePath: cw, + AllowRemoteLookup: false, + AllowFileLookup: false, + seenRemoteSources: &syncmap.Map{}, + } } // 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, // everything is pre-walked if you need it. type SpecIndex struct { - allRefs map[string]*Reference // all (deduplicated) refs - rawSequencedRefs []*Reference // all raw references in sequence as they are scanned, not deduped. - linesWithRefs map[int]bool // lines that link to references. - allMappedRefs map[string]*Reference // these are the located mapped refs - allMappedRefsSequenced []*ReferenceMapped // sequenced mapped refs - refsByLine map[string]map[int]bool // every reference and the lines it's referenced from - pathRefs map[string]map[string]*Reference // all path references - paramOpRefs map[string]map[string]map[string][]*Reference // params in operations. - paramCompRefs map[string]*Reference // params in components - paramAllRefs map[string]*Reference // combined components and ops - paramInlineDuplicateNames map[string][]*Reference // inline params all with the same name - globalTagRefs map[string]*Reference // top level global tags - securitySchemeRefs map[string]*Reference // top level security schemes - requestBodiesRefs map[string]*Reference // top level request bodies - responsesRefs map[string]*Reference // top level responses - headersRefs map[string]*Reference // top level responses - examplesRefs map[string]*Reference // top level examples - securityRequirementRefs map[string]map[string][]*Reference // (NOT $ref) but a name based lookup for requirements - callbacksRefs map[string]map[string][]*Reference // all links - linksRefs map[string]map[string][]*Reference // all callbacks - operationTagsRefs map[string]map[string][]*Reference // tags found in operations - operationDescriptionRefs map[string]map[string]*Reference // descriptions in operations. - operationSummaryRefs map[string]map[string]*Reference // summaries in operations - callbackRefs map[string]*Reference // top level callback refs - serversRefs []*Reference // all top level server refs - rootServersNode *yaml.Node // servers root node - opServersRefs map[string]map[string][]*Reference // all operation level server overrides. - polymorphicRefs map[string]*Reference // every reference to a polymorphic ref - polymorphicAllOfRefs []*Reference // every reference to 'allOf' references - polymorphicOneOfRefs []*Reference // every reference to 'oneOf' references - polymorphicAnyOfRefs []*Reference // every reference to 'anyOf' references - externalDocumentsRef []*Reference // all external documents in spec - rootSecurity []*Reference // root security definitions. - rootSecurityNode *yaml.Node // root security node. - 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 - externalDocumentsCount int // number of externalDocument nodes found - operationTagsCount int // number of unique tags in operations - globalTagsCount int // number of global tags defined - totalTagsCount int // number unique tags in spec - securitySchemesCount int // security schemes - globalRequestBodiesCount int // component request bodies - globalResponsesCount int // component responses - globalHeadersCount int // component headers - globalExamplesCount int // component examples - globalLinksCount int // component links - globalCallbacksCount int // component callbacks - globalCallbacks int // component callbacks. - pathCount int // number of paths - operationCount int // number of operations - operationParamCount int // number of params defined in operations - componentParamCount int // number of params defined in components - componentsInlineParamUniqueCount int // number of inline params with unique names - componentsInlineParamDuplicateCount int // number of inline params with duplicate names - schemaCount int // number of schemas - refCount int // total ref count - root *yaml.Node // the root document - pathsNode *yaml.Node // paths node - tagsNode *yaml.Node // tags node - componentsNode *yaml.Node // components node - parametersNode *yaml.Node // components/parameters node - allParametersNode map[string]*Reference // all parameters node - allParameters map[string]*Reference // all parameters (components/defs) - schemasNode *yaml.Node // components/schemas node - 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). - allComponentSchemaDefinitions map[string]*Reference // all schemas found in components (openapi) or definitions (swagger). - securitySchemesNode *yaml.Node // components/securitySchemes node - allSecuritySchemes map[string]*Reference // all security schemes / definitions. - requestBodiesNode *yaml.Node // components/requestBodies node - allRequestBodies map[string]*Reference // all request bodies - responsesNode *yaml.Node // components/responses node - allResponses map[string]*Reference // all responses - headersNode *yaml.Node // components/headers node - allHeaders map[string]*Reference // all headers - examplesNode *yaml.Node // components/examples node - allExamples map[string]*Reference // all components examples - linksNode *yaml.Node // components/links node - allLinks map[string]*Reference // all links - callbacksNode *yaml.Node // components/callbacks node - allCallbacks map[string]*Reference // all components examples - externalDocumentsNode *yaml.Node // external documents node - allExternalDocuments map[string]*Reference // all external documents - externalSpecIndex map[string]*SpecIndex // create a primary index of all external specs and componentIds - refErrors []error // errors when indexing references - operationParamErrors []error // errors when indexing parameters - allDescriptions []*DescriptionReference // every single description found in the spec. - allSummaries []*DescriptionReference // every single summary found in the spec. - allEnums []*EnumReference // every single enum found in the spec. - allObjectsWithProperties []*ObjectReference // every single object with properties found in the spec. - enumCount int - descriptionCount int - summaryCount int - seenRemoteSources map[string]*yaml.Node - seenLocalSources map[string]*yaml.Node - refLock sync.Mutex - sourceLock sync.Mutex - componentLock sync.RWMutex - externalLock sync.RWMutex - errorLock sync.RWMutex - 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. - relativePath string // relative path of the spec file. - config *SpecIndexConfig // configuration for the index - httpClient *http.Client - componentIndexChan chan bool - polyComponentIndexChan chan bool + allRefs map[string]*Reference // all (deduplicated) refs + rawSequencedRefs []*Reference // all raw references in sequence as they are scanned, not deduped. + linesWithRefs map[int]bool // lines that link to references. + allMappedRefs map[string]*Reference // these are the located mapped refs + allMappedRefsSequenced []*ReferenceMapped // sequenced mapped refs + refsByLine map[string]map[int]bool // every reference and the lines it's referenced from + pathRefs map[string]map[string]*Reference // all path references + paramOpRefs map[string]map[string]map[string][]*Reference // params in operations. + paramCompRefs map[string]*Reference // params in components + paramAllRefs map[string]*Reference // combined components and ops + paramInlineDuplicateNames map[string][]*Reference // inline params all with the same name + globalTagRefs map[string]*Reference // top level global tags + securitySchemeRefs map[string]*Reference // top level security schemes + requestBodiesRefs map[string]*Reference // top level request bodies + responsesRefs map[string]*Reference // top level responses + headersRefs map[string]*Reference // top level responses + examplesRefs map[string]*Reference // top level examples + securityRequirementRefs map[string]map[string][]*Reference // (NOT $ref) but a name based lookup for requirements + callbacksRefs map[string]map[string][]*Reference // all links + linksRefs map[string]map[string][]*Reference // all callbacks + operationTagsRefs map[string]map[string][]*Reference // tags found in operations + operationDescriptionRefs map[string]map[string]*Reference // descriptions in operations. + operationSummaryRefs map[string]map[string]*Reference // summaries in operations + callbackRefs map[string]*Reference // top level callback refs + serversRefs []*Reference // all top level server refs + rootServersNode *yaml.Node // servers root node + opServersRefs map[string]map[string][]*Reference // all operation level server overrides. + polymorphicRefs map[string]*Reference // every reference to a polymorphic ref + polymorphicAllOfRefs []*Reference // every reference to 'allOf' references + polymorphicOneOfRefs []*Reference // every reference to 'oneOf' references + polymorphicAnyOfRefs []*Reference // every reference to 'anyOf' references + externalDocumentsRef []*Reference // all external documents in spec + rootSecurity []*Reference // root security definitions. + rootSecurityNode *yaml.Node // root security node. + 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 + externalDocumentsCount int // number of externalDocument nodes found + operationTagsCount int // number of unique tags in operations + globalTagsCount int // number of global tags defined + totalTagsCount int // number unique tags in spec + securitySchemesCount int // security schemes + globalRequestBodiesCount int // component request bodies + globalResponsesCount int // component responses + globalHeadersCount int // component headers + globalExamplesCount int // component examples + globalLinksCount int // component links + globalCallbacksCount int // component callbacks + globalCallbacks int // component callbacks. + pathCount int // number of paths + operationCount int // number of operations + operationParamCount int // number of params defined in operations + componentParamCount int // number of params defined in components + componentsInlineParamUniqueCount int // number of inline params with unique names + componentsInlineParamDuplicateCount int // number of inline params with duplicate names + schemaCount int // number of schemas + refCount int // total ref count + root *yaml.Node // the root document + pathsNode *yaml.Node // paths node + tagsNode *yaml.Node // tags node + componentsNode *yaml.Node // components node + parametersNode *yaml.Node // components/parameters node + allParametersNode map[string]*Reference // all parameters node + allParameters map[string]*Reference // all parameters (components/defs) + schemasNode *yaml.Node // components/schemas node + 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). + allComponentSchemaDefinitions map[string]*Reference // all schemas found in components (openapi) or definitions (swagger). + securitySchemesNode *yaml.Node // components/securitySchemes node + allSecuritySchemes map[string]*Reference // all security schemes / definitions. + requestBodiesNode *yaml.Node // components/requestBodies node + allRequestBodies map[string]*Reference // all request bodies + responsesNode *yaml.Node // components/responses node + allResponses map[string]*Reference // all responses + headersNode *yaml.Node // components/headers node + allHeaders map[string]*Reference // all headers + examplesNode *yaml.Node // components/examples node + allExamples map[string]*Reference // all components examples + linksNode *yaml.Node // components/links node + allLinks map[string]*Reference // all links + callbacksNode *yaml.Node // components/callbacks node + allCallbacks map[string]*Reference // all components examples + externalDocumentsNode *yaml.Node // external documents node + allExternalDocuments map[string]*Reference // all external documents + externalSpecIndex map[string]*SpecIndex // create a primary index of all external specs and componentIds + refErrors []error // errors when indexing references + operationParamErrors []error // errors when indexing parameters + allDescriptions []*DescriptionReference // every single description found in the spec. + allSummaries []*DescriptionReference // every single summary found in the spec. + allEnums []*EnumReference // every single enum found in the spec. + allObjectsWithProperties []*ObjectReference // every single object with properties found in the spec. + enumCount int + descriptionCount int + summaryCount int + seenRemoteSources map[string]*yaml.Node + seenLocalSources map[string]*yaml.Node + refLock sync.Mutex + sourceLock sync.Mutex + componentLock sync.RWMutex + externalLock sync.RWMutex + errorLock sync.RWMutex + 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. + relativePath string // relative path of the spec file. + config *SpecIndexConfig // configuration for the index + httpClient *http.Client + componentIndexChan chan bool + polyComponentIndexChan chan bool - // 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 - // cto avoid re-downloading sources. - parentIndex *SpecIndex - children []*SpecIndex + // 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 + // cto avoid re-downloading sources. + parentIndex *SpecIndex + uri []string + children []*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. 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 @@ -239,35 +245,35 @@ type ExternalLookupFunction func(id string) (foundNode *yaml.Node, rootNode *yam // IndexingError holds data about something that went wrong during indexing. type IndexingError struct { - Err error - Node *yaml.Node - Path string + Err error + Node *yaml.Node + Path 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. type DescriptionReference struct { - Content string - Path string - Node *yaml.Node - IsSummary bool + Content string + Path string + Node *yaml.Node + IsSummary bool } type EnumReference struct { - Node *yaml.Node - Type *yaml.Node - Path string - SchemaNode *yaml.Node - ParentNode *yaml.Node + Node *yaml.Node + Type *yaml.Node + Path string + SchemaNode *yaml.Node + ParentNode *yaml.Node } type ObjectReference struct { - Node *yaml.Node - Path string - ParentNode *yaml.Node + Node *yaml.Node + Path string + ParentNode *yaml.Node } var methodTypes = []string{"get", "post", "put", "patch", "options", "head", "delete"} diff --git a/index/search_index.go b/index/search_index.go index 8f5b5ce..0c8f698 100644 --- a/index/search_index.go +++ b/index/search_index.go @@ -3,37 +3,32 @@ package index -import "gopkg.in/yaml.v3" - // 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 // extracted when parsing the OpenAPI Spec. func (index *SpecIndex) SearchIndexForReference(ref string) []*Reference { - if r, ok := index.allMappedRefs[ref]; ok { - if r.Node.Kind == yaml.DocumentNode { - // the reference is an entire document, so we need to dig down a level and rewire the reference. - r.Node = r.Node.Content[0] - } - return []*Reference{r} - } - if r, ok := index.externalSpecIndex[ref]; ok { - return []*Reference{ - { - Node: r.root.Content[0], - Name: ref, - Definition: ref, - }, - } - } - for c := range index.children { - found := goFindMeSomething(index.children[c], ref) - if found != nil { - return found - } - } - return nil + if r, ok := index.allMappedRefs[ref]; ok { + return []*Reference{r} + } + for c := range index.children { + found := goFindMeSomething(index.children[c], ref) + if found != nil { + return found + } + } + return nil +} + +func (index *SpecIndex) SearchAncestryForSeenURI(uri string) *SpecIndex { + if index.parentIndex == nil { + return nil + } + if index.uri[0] != uri { + return index.parentIndex.SearchAncestryForSeenURI(uri) + } + return index } func goFindMeSomething(i *SpecIndex, ref string) []*Reference { - return i.SearchIndexForReference(ref) + return i.SearchIndexForReference(ref) } diff --git a/index/search_index_test.go b/index/search_index_test.go index 3ce9864..3ebed4b 100644 --- a/index/search_index_test.go +++ b/index/search_index_test.go @@ -4,50 +4,50 @@ package index import ( - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" - "io/ioutil" - "net/url" - "testing" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "net/url" + "os" + "testing" ) func TestSpecIndex_SearchIndexForReference(t *testing.T) { - petstore, _ := ioutil.ReadFile("../test_specs/petstorev3.json") - var rootNode yaml.Node - _ = yaml.Unmarshal(petstore, &rootNode) + petstore, _ := os.ReadFile("../test_specs/petstorev3.json") + var rootNode yaml.Node + _ = yaml.Unmarshal(petstore, &rootNode) - c := CreateOpenAPIIndexConfig() - idx := NewSpecIndexWithConfig(&rootNode, c) + c := CreateOpenAPIIndexConfig() + idx := NewSpecIndexWithConfig(&rootNode, c) - ref := idx.SearchIndexForReference("#/components/schemas/Pet") - assert.NotNil(t, ref) + ref := idx.SearchIndexForReference("#/components/schemas/Pet") + assert.NotNil(t, ref) } func TestSpecIndex_SearchIndexForReference_ExternalSpecs(t *testing.T) { - // load up an index with lots of references - petstore, _ := ioutil.ReadFile("../test_specs/digitalocean.yaml") - var rootNode yaml.Node - _ = yaml.Unmarshal(petstore, &rootNode) + // load up an index with lots of references + petstore, _ := os.ReadFile("../test_specs/digitalocean.yaml") + var rootNode yaml.Node + _ = yaml.Unmarshal(petstore, &rootNode) - c := CreateOpenAPIIndexConfig() - c.BaseURL, _ = url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification") - idx := NewSpecIndexWithConfig(&rootNode, c) + c := CreateOpenAPIIndexConfig() + c.BaseURL, _ = url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification") + idx := NewSpecIndexWithConfig(&rootNode, c) - ref := idx.SearchIndexForReference("resources/apps/apps_list_instanceSizes.yml") - assert.NotNil(t, ref) - assert.Equal(t, "operationId", ref[0].Node.Content[0].Value) + ref := idx.SearchIndexForReference("resources/apps/apps_list_instanceSizes.yml") + assert.NotNil(t, ref) + assert.Equal(t, "operationId", ref[0].Node.Content[0].Value) - ref = idx.SearchIndexForReference("examples/ruby/domains_create.yml") - assert.NotNil(t, ref) - assert.Equal(t, "lang", ref[0].Node.Content[0].Value) + ref = idx.SearchIndexForReference("examples/ruby/domains_create.yml") + assert.NotNil(t, ref) + assert.Equal(t, "lang", ref[0].Node.Content[0].Value) - ref = idx.SearchIndexForReference("../../shared/responses/server_error.yml") - assert.NotNil(t, ref) - assert.Equal(t, "description", ref[0].Node.Content[0].Value) + ref = idx.SearchIndexForReference("../../shared/responses/server_error.yml") + assert.NotNil(t, ref) + assert.Equal(t, "description", ref[0].Node.Content[0].Value) - ref = idx.SearchIndexForReference("../models/options.yml") - assert.NotNil(t, ref) - assert.Equal(t, "kubernetes_options", ref[0].Node.Content[0].Value) + ref = idx.SearchIndexForReference("../models/options.yml") + assert.NotNil(t, ref) + assert.Equal(t, "kubernetes_options", ref[0].Node.Content[0].Value) } diff --git a/index/spec_index.go b/index/spec_index.go index 33f5e23..4e401ab 100644 --- a/index/spec_index.go +++ b/index/spec_index.go @@ -32,6 +32,8 @@ func NewSpecIndexWithConfig(rootNode *yaml.Node, config *SpecIndexConfig) *SpecI } config.remoteLock = &sync.Mutex{} index.config = config + index.parentIndex = config.ParentIndex + index.uri = config.uri if rootNode == nil || len(rootNode.Content) <= 0 { 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 // 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. -// - https://github.com/pb33f/libopenapi/issues/73 +// - https://github.com/pb33f/libopenapi/issues/73 func NewSpecIndex(rootNode *yaml.Node) *SpecIndex { index := new(SpecIndex) index.config = CreateOpenAPIIndexConfig() diff --git a/index/spec_index_test.go b/index/spec_index_test.go index fb60983..fad95e4 100644 --- a/index/spec_index_test.go +++ b/index/spec_index_test.go @@ -86,7 +86,7 @@ func TestSpecIndex_Asana(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 _ = yaml.Unmarshal(do, &rootNode) @@ -603,7 +603,6 @@ func TestSpecIndex_FindComponenth(t *testing.T) { assert.Nil(t, index.FindComponent("I-do-not-exist", nil)) } - func TestSpecIndex_TestPathsNodeAsArray(t *testing.T) { yml := `components: schemas: @@ -633,21 +632,21 @@ func TestSpecIndex_lookupRemoteReference_SeenSourceSimulation_BadFind(t *testing index := new(SpecIndex) index.seenRemoteSources = make(map[string]*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") - assert.Error(t, err) - assert.Nil(t, a) + a, b, err := index.lookupRemoteReference("https://no-hope-for-a-dope.com#/hey") + assert.Error(t, err) + assert.Nil(t, a) assert.Nil(t, b) } // Discovered in issue https://github.com/pb33f/libopenapi/issues/37 func TestSpecIndex_lookupRemoteReference_NoComponent(t *testing.T) { - index := new(SpecIndex) - index.seenRemoteSources = make(map[string]*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") - assert.NoError(t, err) - assert.NotNil(t, a) - assert.NotNil(t, b) + index := new(SpecIndex) + index.seenRemoteSources = make(map[string]*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") + assert.NoError(t, err) + assert.NotNil(t, a) + assert.NotNil(t, b) } // 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) { - yml := `openapi: 3.1.0 + yml := `openapi: 3.1.0 paths: /cakes: post: parameters: - $ref: 'httpsss://badurl'` - var rootNode yaml.Node + var rootNode yaml.Node _ = yaml.Unmarshal([]byte(yml), &rootNode) index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig()) - assert.Len(t, index.refErrors, 2) + assert.Len(t, index.refErrors, 2) } func TestSpecIndex_CheckBadURLRefNoRemoteAllowed(t *testing.T) { - yml := `openapi: 3.1.0 + yml := `openapi: 3.1.0 paths: /cakes: post: parameters: - $ref: 'httpsss://badurl'` - var rootNode yaml.Node + var rootNode yaml.Node _ = yaml.Unmarshal([]byte(yml), &rootNode) - c := CreateClosedAPIIndexConfig() - idx := NewSpecIndexWithConfig(&rootNode, c) + c := CreateClosedAPIIndexConfig() + idx := NewSpecIndexWithConfig(&rootNode, c) - assert.Len(t, idx.refErrors, 2) - assert.Equal(t, "remote lookups are not permitted, "+ - "please set AllowRemoteLookup to true in the configuration", idx.refErrors[0].Error()) + assert.Len(t, idx.refErrors, 2) + assert.Equal(t, "remote lookups are not permitted, "+ + "please set AllowRemoteLookup to true in the configuration", idx.refErrors[0].Error()) } func TestSpecIndex_CheckIndexDiscoversNoComponentLocalFileReference(t *testing.T) { - _ = ioutil.WriteFile("coffee-time.yaml", []byte("name: time for coffee"), 0o664) - defer os.Remove("coffee-time.yaml") + _ = ioutil.WriteFile("coffee-time.yaml", []byte("name: time for coffee"), 0o664) + defer os.Remove("coffee-time.yaml") - yml := `openapi: 3.0.3 + yml := `openapi: 3.0.3 paths: /cakes: post: diff --git a/resolver/resolver_test.go b/resolver/resolver_test.go index 6e03594..2bded19 100644 --- a/resolver/resolver_test.go +++ b/resolver/resolver_test.go @@ -1,170 +1,170 @@ package resolver import ( - "errors" - "fmt" - "io/ioutil" - "net/url" - "testing" + "errors" + "fmt" + "io/ioutil" + "net/url" + "testing" - "github.com/pb33f/libopenapi/index" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi/index" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestNewResolver(t *testing.T) { - assert.Nil(t, NewResolver(nil)) + assert.Nil(t, NewResolver(nil)) } func Benchmark_ResolveDocumentStripe(b *testing.B) { - stripe, _ := ioutil.ReadFile("../test_specs/stripe.yaml") - for n := 0; n < b.N; n++ { - var rootNode yaml.Node - yaml.Unmarshal(stripe, &rootNode) - index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - resolver.Resolve() - } + stripe, _ := ioutil.ReadFile("../test_specs/stripe.yaml") + for n := 0; n < b.N; n++ { + var rootNode yaml.Node + yaml.Unmarshal(stripe, &rootNode) + index := index.NewSpecIndex(&rootNode) + resolver := NewResolver(index) + resolver.Resolve() + } } func TestResolver_ResolveComponents_CircularSpec(t *testing.T) { - circular, _ := ioutil.ReadFile("../test_specs/circular-tests.yaml") - var rootNode yaml.Node - yaml.Unmarshal(circular, &rootNode) + circular, _ := ioutil.ReadFile("../test_specs/circular-tests.yaml") + var rootNode yaml.Node + yaml.Unmarshal(circular, &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.Resolve() - assert.Len(t, circ, 3) + circ := resolver.Resolve() + assert.Len(t, circ, 3) - _, err := yaml.Marshal(resolver.resolvedRoot) - assert.NoError(t, err) + _, err := yaml.Marshal(resolver.resolvedRoot) + assert.NoError(t, err) } func TestResolver_CheckForCircularReferences(t *testing.T) { - circular, _ := ioutil.ReadFile("../test_specs/circular-tests.yaml") - var rootNode yaml.Node - yaml.Unmarshal(circular, &rootNode) + circular, _ := ioutil.ReadFile("../test_specs/circular-tests.yaml") + var rootNode yaml.Node + yaml.Unmarshal(circular, &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.CheckForCircularReferences() - assert.Len(t, circ, 3) - assert.Len(t, resolver.GetResolvingErrors(), 3) - assert.Len(t, resolver.GetCircularErrors(), 3) + circ := resolver.CheckForCircularReferences() + assert.Len(t, circ, 3) + assert.Len(t, resolver.GetResolvingErrors(), 3) + assert.Len(t, resolver.GetCircularErrors(), 3) - _, err := yaml.Marshal(resolver.resolvedRoot) - assert.NoError(t, err) + _, err := yaml.Marshal(resolver.resolvedRoot) + assert.NoError(t, err) } func TestResolver_CheckForCircularReferences_DigitalOcean(t *testing.T) { - circular, _ := ioutil.ReadFile("../test_specs/digitalocean.yaml") - var rootNode yaml.Node - yaml.Unmarshal(circular, &rootNode) + circular, _ := ioutil.ReadFile("../test_specs/digitalocean.yaml") + var rootNode yaml.Node + 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{ - AllowRemoteLookup: true, - AllowFileLookup: true, - BaseURL: baseURL, - }) + index := index.NewSpecIndexWithConfig(&rootNode, &index.SpecIndexConfig{ + AllowRemoteLookup: true, + AllowFileLookup: true, + BaseURL: baseURL, + }) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.CheckForCircularReferences() - assert.Len(t, circ, 0) - assert.Len(t, resolver.GetResolvingErrors(), 0) - assert.Len(t, resolver.GetCircularErrors(), 0) + circ := resolver.CheckForCircularReferences() + assert.Len(t, circ, 0) + assert.Len(t, resolver.GetResolvingErrors(), 0) + assert.Len(t, resolver.GetCircularErrors(), 0) - _, err := yaml.Marshal(resolver.resolvedRoot) - assert.NoError(t, err) + _, err := yaml.Marshal(resolver.resolvedRoot) + assert.NoError(t, err) } func TestResolver_CircularReferencesRequiredValid(t *testing.T) { - circular, _ := ioutil.ReadFile("../test_specs/swagger-valid-recursive-model.yaml") - var rootNode yaml.Node - yaml.Unmarshal(circular, &rootNode) + circular, _ := ioutil.ReadFile("../test_specs/swagger-valid-recursive-model.yaml") + var rootNode yaml.Node + yaml.Unmarshal(circular, &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.CheckForCircularReferences() - assert.Len(t, circ, 0) + circ := resolver.CheckForCircularReferences() + assert.Len(t, circ, 0) - _, err := yaml.Marshal(resolver.resolvedRoot) - assert.NoError(t, err) + _, err := yaml.Marshal(resolver.resolvedRoot) + assert.NoError(t, err) } func TestResolver_CircularReferencesRequiredInvalid(t *testing.T) { - circular, _ := ioutil.ReadFile("../test_specs/swagger-invalid-recursive-model.yaml") - var rootNode yaml.Node - yaml.Unmarshal(circular, &rootNode) + circular, _ := ioutil.ReadFile("../test_specs/swagger-invalid-recursive-model.yaml") + var rootNode yaml.Node + yaml.Unmarshal(circular, &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.CheckForCircularReferences() - assert.Len(t, circ, 2) + circ := resolver.CheckForCircularReferences() + assert.Len(t, circ, 2) - _, err := yaml.Marshal(resolver.resolvedRoot) - assert.NoError(t, err) + _, err := yaml.Marshal(resolver.resolvedRoot) + assert.NoError(t, err) } func TestResolver_DeepJourney(t *testing.T) { - var journey []*index.Reference - for f := 0; f < 200; f++ { - journey = append(journey, nil) - } - index := index.NewSpecIndex(nil) - resolver := NewResolver(index) - assert.Nil(t, resolver.extractRelatives(nil, nil, journey, false)) + var journey []*index.Reference + for f := 0; f < 200; f++ { + journey = append(journey, nil) + } + index := index.NewSpecIndex(nil) + resolver := NewResolver(index) + assert.Nil(t, resolver.extractRelatives(nil, nil, journey, false)) } func TestResolver_ResolveComponents_Stripe(t *testing.T) { - stripe, _ := ioutil.ReadFile("../test_specs/stripe.yaml") - var rootNode yaml.Node - yaml.Unmarshal(stripe, &rootNode) + stripe, _ := ioutil.ReadFile("../test_specs/stripe.yaml") + var rootNode yaml.Node + yaml.Unmarshal(stripe, &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.Resolve() - assert.Len(t, circ, 3) + circ := resolver.Resolve() + assert.Len(t, circ, 3) - assert.Len(t, resolver.GetNonPolymorphicCircularErrors(), 3) - assert.Len(t, resolver.GetPolymorphicCircularErrors(), 0) + assert.Len(t, resolver.GetNonPolymorphicCircularErrors(), 3) + assert.Len(t, resolver.GetPolymorphicCircularErrors(), 0) } func TestResolver_ResolveComponents_BurgerShop(t *testing.T) { - mixedref, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") - var rootNode yaml.Node - yaml.Unmarshal(mixedref, &rootNode) + mixedref, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") + var rootNode yaml.Node + yaml.Unmarshal(mixedref, &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.Resolve() - assert.Len(t, circ, 0) + circ := resolver.Resolve() + assert.Len(t, circ, 0) } func TestResolver_ResolveComponents_PolyNonCircRef(t *testing.T) { - yml := `paths: + yml := `paths: /hey: get: responses: @@ -184,20 +184,20 @@ components: tea: description: tea` - var rootNode yaml.Node - yaml.Unmarshal([]byte(yml), &rootNode) + var rootNode yaml.Node + yaml.Unmarshal([]byte(yml), &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.CheckForCircularReferences() - assert.Len(t, circ, 0) + circ := resolver.CheckForCircularReferences() + assert.Len(t, circ, 0) } func TestResolver_ResolveComponents_PolyCircRef(t *testing.T) { - yml := `openapi: 3.1.0 + yml := `openapi: 3.1.0 components: schemas: cheese: @@ -211,24 +211,24 @@ components: tea: description: tea` - var rootNode yaml.Node - yaml.Unmarshal([]byte(yml), &rootNode) + var rootNode yaml.Node + yaml.Unmarshal([]byte(yml), &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - _ = resolver.CheckForCircularReferences() - resolver.circularReferences[0].IsInfiniteLoop = true // override - assert.Len(t, index.GetCircularReferences(), 1) - assert.Len(t, resolver.GetPolymorphicCircularErrors(), 1) - assert.Equal(t, 2, index.GetCircularReferences()[0].LoopIndex) + _ = resolver.CheckForCircularReferences() + resolver.circularReferences[0].IsInfiniteLoop = true // override + assert.Len(t, index.GetCircularReferences(), 1) + assert.Len(t, resolver.GetPolymorphicCircularErrors(), 1) + assert.Equal(t, 2, index.GetCircularReferences()[0].LoopIndex) } func TestResolver_ResolveComponents_Missing(t *testing.T) { - yml := `paths: + yml := `paths: /hey: get: responses: @@ -247,93 +247,95 @@ components: butter: $ref: 'go home, I am drunk'` - var rootNode yaml.Node - yaml.Unmarshal([]byte(yml), &rootNode) + var rootNode yaml.Node + yaml.Unmarshal([]byte(yml), &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - err := resolver.Resolve() - 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()) + err := resolver.Resolve() + 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()) } func TestResolver_ResolveComponents_MixedRef(t *testing.T) { - mixedref, _ := ioutil.ReadFile("../test_specs/mixedref-burgershop.openapi.yaml") - var rootNode yaml.Node - yaml.Unmarshal(mixedref, &rootNode) + mixedref, _ := ioutil.ReadFile("../test_specs/mixedref-burgershop.openapi.yaml") + var rootNode yaml.Node + yaml.Unmarshal(mixedref, &rootNode) - b := index.CreateOpenAPIIndexConfig() - idx := index.NewSpecIndexWithConfig(&rootNode, b) + b := index.CreateOpenAPIIndexConfig() + idx := index.NewSpecIndexWithConfig(&rootNode, b) - resolver := NewResolver(idx) - assert.NotNil(t, resolver) + resolver := NewResolver(idx) + assert.NotNil(t, resolver) - circ := resolver.Resolve() - assert.Len(t, circ, 0) - assert.Equal(t, 5, resolver.GetIndexesVisited()) - assert.Equal(t, 209, resolver.GetRelativesSeen()) - assert.Equal(t, 35, resolver.GetJourneysTaken()) - assert.Equal(t, 62, resolver.GetReferenceVisited()) + circ := resolver.Resolve() + assert.Len(t, circ, 0) + assert.Equal(t, 5, resolver.GetIndexesVisited()) + + // in v0.8.2 a new check was added when indexing, to prevent re-indexing the same file multiple times. + 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) { - k8s, _ := ioutil.ReadFile("../test_specs/k8s.json") - var rootNode yaml.Node - yaml.Unmarshal(k8s, &rootNode) + k8s, _ := ioutil.ReadFile("../test_specs/k8s.json") + var rootNode yaml.Node + yaml.Unmarshal(k8s, &rootNode) - index := index.NewSpecIndex(&rootNode) + index := index.NewSpecIndex(&rootNode) - resolver := NewResolver(index) - assert.NotNil(t, resolver) + resolver := NewResolver(index) + assert.NotNil(t, resolver) - circ := resolver.Resolve() - assert.Len(t, circ, 0) + circ := resolver.Resolve() + assert.Len(t, circ, 0) } // Example of how to resolve the Stripe OpenAPI specification, and check for circular reference errors func ExampleNewResolver() { - // create a yaml.Node reference as a root node. - var rootNode yaml.Node + // create a yaml.Node reference as a root node. + var rootNode yaml.Node - // load in the Stripe OpenAPI spec (lots of polymorphic complexity in here) - stripeBytes, _ := ioutil.ReadFile("../test_specs/stripe.yaml") + // load in the Stripe OpenAPI spec (lots of polymorphic complexity in here) + stripeBytes, _ := ioutil.ReadFile("../test_specs/stripe.yaml") - // unmarshal bytes into our rootNode. - _ = yaml.Unmarshal(stripeBytes, &rootNode) + // unmarshal bytes into our rootNode. + _ = yaml.Unmarshal(stripeBytes, &rootNode) - // create a new spec index (resolver depends on it) - indexConfig := index.CreateClosedAPIIndexConfig() - index := index.NewSpecIndexWithConfig(&rootNode, indexConfig) + // create a new spec index (resolver depends on it) + indexConfig := index.CreateClosedAPIIndexConfig() + index := index.NewSpecIndexWithConfig(&rootNode, indexConfig) - // create a new resolver using the index. - resolver := NewResolver(index) + // create a new resolver using the index. + resolver := NewResolver(index) - // 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 - circularErrors := resolver.Resolve() + // 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 + circularErrors := resolver.Resolve() - // The Stripe API has a bunch of circular reference problems, mainly from polymorphism. - // So let's print them out. - // - 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())) - // Output: There are 3 circular reference errors, 0 of them are polymorphic errors, 3 are not + // The Stripe API has a bunch of circular reference problems, mainly from polymorphism. + // So let's print them out. + // + 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())) + // Output: There are 3 circular reference errors, 0 of them are polymorphic errors, 3 are not } func ExampleResolvingError() { - re := ResolvingError{ - ErrorRef: errors.New("Je suis une erreur"), - Node: &yaml.Node{ - Line: 5, - Column: 21, - }, - Path: "#/definitions/JeSuisUneErreur", - CircularReference: &index.CircularReferenceResult{}, - } + re := ResolvingError{ + ErrorRef: errors.New("Je suis une erreur"), + Node: &yaml.Node{ + Line: 5, + Column: 21, + }, + Path: "#/definitions/JeSuisUneErreur", + CircularReference: &index.CircularReferenceResult{}, + } - fmt.Printf("%s", re.Error()) - // Output: Je suis une erreur: #/definitions/JeSuisUneErreur [5:21] + fmt.Printf("%s", re.Error()) + // Output: Je suis une erreur: #/definitions/JeSuisUneErreur [5:21] } diff --git a/test_specs/first.yaml b/test_specs/first.yaml new file mode 100644 index 0000000..82a3643 --- /dev/null +++ b/test_specs/first.yaml @@ -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" \ No newline at end of file diff --git a/test_specs/second.yaml b/test_specs/second.yaml new file mode 100644 index 0000000..7cf1bdf --- /dev/null +++ b/test_specs/second.yaml @@ -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 \ No newline at end of file diff --git a/test_specs/third.yaml b/test_specs/third.yaml new file mode 100644 index 0000000..ba14cc2 --- /dev/null +++ b/test_specs/third.yaml @@ -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' \ No newline at end of file diff --git a/utils/utils.go b/utils/utils.go index b733425..d9c27e5 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,331 +1,331 @@ package utils import ( - "encoding/json" - "fmt" - "net/url" - "regexp" - "strconv" - "strings" + "encoding/json" + "fmt" + "net/url" + "regexp" + "strconv" + "strings" - "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" - "gopkg.in/yaml.v3" + "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" + "gopkg.in/yaml.v3" ) type Case int8 const ( - // OpenApi3 is used by all OpenAPI 3+ docs - OpenApi3 = "openapi" + // OpenApi3 is used by all OpenAPI 3+ docs + OpenApi3 = "openapi" - // OpenApi2 is used by all OpenAPI 2 docs, formerly known as swagger. - OpenApi2 = "swagger" + // OpenApi2 is used by all OpenAPI 2 docs, formerly known as swagger. + OpenApi2 = "swagger" - // AsyncApi is used by akk AsyncAPI docs, all versions. - AsyncApi = "asyncapi" + // AsyncApi is used by akk AsyncAPI docs, all versions. + AsyncApi = "asyncapi" - PascalCase Case = iota - CamelCase - ScreamingSnakeCase - SnakeCase - KebabCase - ScreamingKebabCase - RegularCase - UnknownCase + PascalCase Case = iota + CamelCase + ScreamingSnakeCase + SnakeCase + KebabCase + ScreamingKebabCase + RegularCase + UnknownCase ) // FindNodes will find a node based on JSONPath, it accepts raw yaml/json as input. func FindNodes(yamlData []byte, jsonPath string) ([]*yaml.Node, error) { - jsonPath = FixContext(jsonPath) + jsonPath = FixContext(jsonPath) - var node yaml.Node - yaml.Unmarshal(yamlData, &node) + var node yaml.Node + yaml.Unmarshal(yamlData, &node) - path, err := yamlpath.NewPath(jsonPath) - if err != nil { - return nil, err - } - results, _ := path.Find(&node) - return results, nil + path, err := yamlpath.NewPath(jsonPath) + if err != nil { + return nil, err + } + results, _ := path.Find(&node) + return results, nil } // FindLastChildNode will find the last node in a tree, based on a starting node. // Deprecated: This function is deprecated, use FindLastChildNodeWithLevel instead. // this has the potential to cause a stack overflow, so use with caution. It will be removed later. func FindLastChildNode(node *yaml.Node) *yaml.Node { - s := len(node.Content) - 1 - if s < 0 { - s = 0 - } - if len(node.Content) > 0 && len(node.Content[s].Content) > 0 { - return FindLastChildNode(node.Content[s]) - } else { - if len(node.Content) > 0 { - return node.Content[s] - } - return node - } + s := len(node.Content) - 1 + if s < 0 { + s = 0 + } + if len(node.Content) > 0 && len(node.Content[s].Content) > 0 { + return FindLastChildNode(node.Content[s]) + } else { + if len(node.Content) > 0 { + return node.Content[s] + } + return node + } } // FindLastChildNodeWithLevel will find the last node in a tree, based on a starting node. // Will stop searching after 100 levels, because that's just silly, we probably have a loop. func FindLastChildNodeWithLevel(node *yaml.Node, level int) *yaml.Node { - if level > 100 { - return node // we've gone too far, give up. - } - s := len(node.Content) - 1 - if s < 0 { - s = 0 - } - if len(node.Content) > 0 && len(node.Content[s].Content) > 0 { - level++ - return FindLastChildNodeWithLevel(node.Content[s], level) - } else { - if len(node.Content) > 0 { - return node.Content[s] - } - return node - } + if level > 100 { + return node // we've gone too far, give up. + } + s := len(node.Content) - 1 + if s < 0 { + s = 0 + } + if len(node.Content) > 0 && len(node.Content[s].Content) > 0 { + level++ + return FindLastChildNodeWithLevel(node.Content[s], level) + } else { + if len(node.Content) > 0 { + return node.Content[s] + } + return node + } } // BuildPath will construct a JSONPath from a base and an array of strings. func BuildPath(basePath string, segs []string) string { - path := strings.Join(segs, ".") + path := strings.Join(segs, ".") - // trim that last period. - if len(path) > 0 && path[len(path)-1] == '.' { - path = path[:len(path)-1] - } - return fmt.Sprintf("%s.%s", basePath, path) + // trim that last period. + if len(path) > 0 && path[len(path)-1] == '.' { + path = path[:len(path)-1] + } + return fmt.Sprintf("%s.%s", basePath, path) } // FindNodesWithoutDeserializing will find a node based on JSONPath, without deserializing from yaml/json func FindNodesWithoutDeserializing(node *yaml.Node, jsonPath string) ([]*yaml.Node, error) { - jsonPath = FixContext(jsonPath) + jsonPath = FixContext(jsonPath) - path, err := yamlpath.NewPath(jsonPath) - if err != nil { - return nil, err - } - results, _ := path.Find(node) - return results, nil + path, err := yamlpath.NewPath(jsonPath) + if err != nil { + return nil, err + } + results, _ := path.Find(node) + return results, nil } // ConvertInterfaceIntoStringMap will convert an unknown input into a string map. func ConvertInterfaceIntoStringMap(context interface{}) map[string]string { - converted := make(map[string]string) - if context != nil { - if v, ok := context.(map[string]interface{}); ok { - for k, n := range v { - if s, okB := n.(string); okB { - converted[k] = s - } - } - } - if v, ok := context.(map[string]string); ok { - for k, n := range v { - converted[k] = n - } - } - } - return converted + converted := make(map[string]string) + if context != nil { + if v, ok := context.(map[string]interface{}); ok { + for k, n := range v { + if s, okB := n.(string); okB { + converted[k] = s + } + } + } + if v, ok := context.(map[string]string); ok { + for k, n := range v { + converted[k] = n + } + } + } + return converted } // ConvertInterfaceToStringArray will convert an unknown input map type into a string array/slice func ConvertInterfaceToStringArray(raw interface{}) []string { - if vals, ok := raw.(map[string]interface{}); ok { - var s []string - for _, v := range vals { - if g, y := v.([]interface{}); y { - for _, q := range g { - s = append(s, fmt.Sprint(q)) - } - } - } - return s - } - if vals, ok := raw.(map[string][]string); ok { - var s []string - for _, v := range vals { - s = append(s, v...) - } - return s - } - return nil + if vals, ok := raw.(map[string]interface{}); ok { + var s []string + for _, v := range vals { + if g, y := v.([]interface{}); y { + for _, q := range g { + s = append(s, fmt.Sprint(q)) + } + } + } + return s + } + if vals, ok := raw.(map[string][]string); ok { + var s []string + for _, v := range vals { + s = append(s, v...) + } + return s + } + return nil } // ConvertInterfaceArrayToStringArray will convert an unknown interface array type, into a string slice func ConvertInterfaceArrayToStringArray(raw interface{}) []string { - if vals, ok := raw.([]interface{}); ok { - s := make([]string, len(vals)) - for i, v := range vals { - s[i] = fmt.Sprint(v) - } - return s - } - if vals, ok := raw.([]string); ok { - return vals - } - return nil + if vals, ok := raw.([]interface{}); ok { + s := make([]string, len(vals)) + for i, v := range vals { + s[i] = fmt.Sprint(v) + } + return s + } + if vals, ok := raw.([]string); ok { + return vals + } + return nil } // ExtractValueFromInterfaceMap pulls out an unknown value from a map using a string key func ExtractValueFromInterfaceMap(name string, raw interface{}) interface{} { - if propMap, ok := raw.(map[string]interface{}); ok { - if props, okn := propMap[name].([]interface{}); okn { - return props - } else { - return propMap[name] - } - } - if propMap, ok := raw.(map[string][]string); ok { - return propMap[name] - } + if propMap, ok := raw.(map[string]interface{}); ok { + if props, okn := propMap[name].([]interface{}); okn { + return props + } else { + return propMap[name] + } + } + if propMap, ok := raw.(map[string][]string); ok { + return propMap[name] + } - return nil + return nil } // FindFirstKeyNode will locate the first key and value yaml.Node based on a key. func FindFirstKeyNode(key string, nodes []*yaml.Node, depth int) (keyNode *yaml.Node, valueNode *yaml.Node) { - if depth > 40 { - return nil, nil - } - for i, v := range nodes { - if key != "" && key == v.Value { - if i+1 >= len(nodes) { - return v, nodes[i] // this is the node we need. - } - return v, nodes[i+1] // next node is what we need. - } - if len(v.Content) > 0 { - depth++ - x, y := FindFirstKeyNode(key, v.Content, depth) - if x != nil && y != nil { - return x, y - } - } - } - return nil, nil + if depth > 40 { + return nil, nil + } + for i, v := range nodes { + if key != "" && key == v.Value { + if i+1 >= len(nodes) { + return v, nodes[i] // this is the node we need. + } + return v, nodes[i+1] // next node is what we need. + } + if len(v.Content) > 0 { + depth++ + x, y := FindFirstKeyNode(key, v.Content, depth) + if x != nil && y != nil { + return x, y + } + } + } + return nil, nil } // KeyNodeResult is a result from a KeyNodeSearch performed by the FindAllKeyNodesWithPath type KeyNodeResult struct { - KeyNode *yaml.Node - ValueNode *yaml.Node - Parent *yaml.Node - Path []yaml.Node + KeyNode *yaml.Node + ValueNode *yaml.Node + Parent *yaml.Node + Path []yaml.Node } // KeyNodeSearch keeps a track of everything we have found on our adventure down the trees. type KeyNodeSearch struct { - Key string - Ignore []string - Results []*KeyNodeResult - AllowExtensions bool + Key string + Ignore []string + Results []*KeyNodeResult + AllowExtensions bool } // FindKeyNodeTop is a non-recursive search of top level nodes for a key, will not look at content. // Returns the key and value func FindKeyNodeTop(key string, nodes []*yaml.Node) (keyNode *yaml.Node, valueNode *yaml.Node) { - for i, v := range nodes { - if i%2 != 0 { - continue - } - if strings.ToLower(key) == strings.ToLower(v.Value) { - return v, nodes[i+1] // next node is what we need. - } - } - return nil, nil + for i, v := range nodes { + if i%2 != 0 { + continue + } + if strings.ToLower(key) == strings.ToLower(v.Value) { + return v, nodes[i+1] // next node is what we need. + } + } + return nil, nil } // FindKeyNode is a non-recursive search of a *yaml.Node Content for a child node with a key. // Returns the key and value func FindKeyNode(key string, nodes []*yaml.Node) (keyNode *yaml.Node, valueNode *yaml.Node) { - //numNodes := len(nodes) - for i, v := range nodes { - if i%2 == 0 && key == v.Value { - return v, nodes[i+1] // next node is what we need. - } - for x, j := range v.Content { - if key == j.Value { - if IsNodeMap(v) { - if x+1 == len(v.Content) { - return v, v.Content[x] - } - return v, v.Content[x+1] // next node is what we need. + //numNodes := len(nodes) + for i, v := range nodes { + if i%2 == 0 && key == v.Value { + return v, nodes[i+1] // next node is what we need. + } + for x, j := range v.Content { + if key == j.Value { + if IsNodeMap(v) { + if x+1 == len(v.Content) { + return v, v.Content[x] + } + return v, v.Content[x+1] // next node is what we need. - } - if IsNodeArray(v) { - return v, v.Content[x] - } - } - } - } - return nil, nil + } + if IsNodeArray(v) { + return v, v.Content[x] + } + } + } + } + return nil, nil } // FindKeyNodeFull is an overloaded version of FindKeyNode. This version however returns keys, labels and values. // generally different things are required from different node trees, so depending on what this function is looking at // it will return different things. func FindKeyNodeFull(key string, nodes []*yaml.Node) (keyNode *yaml.Node, labelNode *yaml.Node, valueNode *yaml.Node) { - for i := range nodes { - if i%2 == 0 && key == nodes[i].Value { - return nodes[i], nodes[i], nodes[i+1] // next node is what we need. - } - } - for _, v := range nodes { - for x := range v.Content { - if key == v.Content[x].Value { - if IsNodeMap(v) { - if x+1 == len(v.Content) { - return v, v.Content[x], v.Content[x] - } - return v, v.Content[x], v.Content[x+1] - } - if IsNodeArray(v) { - return v, v.Content[x], v.Content[x] - } - } - } - } - return nil, nil, nil + for i := range nodes { + if i%2 == 0 && key == nodes[i].Value { + return nodes[i], nodes[i], nodes[i+1] // next node is what we need. + } + } + for _, v := range nodes { + for x := range v.Content { + if key == v.Content[x].Value { + if IsNodeMap(v) { + if x+1 == len(v.Content) { + return v, v.Content[x], v.Content[x] + } + return v, v.Content[x], v.Content[x+1] + } + if IsNodeArray(v) { + return v, v.Content[x], v.Content[x] + } + } + } + } + return nil, nil, nil } // FindKeyNodeFullTop is an overloaded version of FindKeyNodeFull. This version only looks at the top // level of the node and not the children. func FindKeyNodeFullTop(key string, nodes []*yaml.Node) (keyNode *yaml.Node, labelNode *yaml.Node, valueNode *yaml.Node) { - for i := range nodes { - if i%2 != 0 { - continue - } - if i%2 == 0 && key == nodes[i].Value { - return nodes[i], nodes[i], nodes[i+1] // next node is what we need. - } - } - return nil, nil, nil + for i := range nodes { + if i%2 != 0 { + continue + } + if i%2 == 0 && key == nodes[i].Value { + return nodes[i], nodes[i], nodes[i+1] // next node is what we need. + } + } + return nil, nil, nil } type ExtensionNode struct { - Key *yaml.Node - Value *yaml.Node + Key *yaml.Node + Value *yaml.Node } func FindExtensionNodes(nodes []*yaml.Node) []*ExtensionNode { - var extensions []*ExtensionNode - for i, v := range nodes { - if i%2 == 0 && strings.HasPrefix(v.Value, "x-") { - if i+1 < len(nodes) { - extensions = append(extensions, &ExtensionNode{ - Key: v, - Value: nodes[i+1], - }) - } - } - } - return extensions + var extensions []*ExtensionNode + for i, v := range nodes { + if i%2 == 0 && strings.HasPrefix(v.Value, "x-") { + if i+1 < len(nodes) { + extensions = append(extensions, &ExtensionNode{ + Key: v, + Value: nodes[i+1], + }) + } + } + } + return extensions } var ObjectLabel = "object" @@ -339,296 +339,300 @@ var SchemaSource = "https://json-schema.org/draft/2020-12/schema" var SchemaId = "https://pb33f.io/openapi-changes/schema" func MakeTagReadable(node *yaml.Node) string { - switch node.Tag { - case "!!map": - return ObjectLabel - case "!!seq": - return ArrayLabel - case "!!str": - return StringLabel - case "!!int": - return IntegerLabel - case "!!float": - return NumberLabel - case "!!bool": - return BooleanLabel - } - return "unknown" + switch node.Tag { + case "!!map": + return ObjectLabel + case "!!seq": + return ArrayLabel + case "!!str": + return StringLabel + case "!!int": + return IntegerLabel + case "!!float": + return NumberLabel + case "!!bool": + return BooleanLabel + } + return "unknown" } // IsNodeMap checks if the node is a map type func IsNodeMap(node *yaml.Node) bool { - if node == nil { - return false - } - return node.Tag == "!!map" + if node == nil { + return false + } + return node.Tag == "!!map" } // IsNodePolyMorphic will return true if the node contains polymorphic keys. func IsNodePolyMorphic(node *yaml.Node) bool { - for i, v := range node.Content { - if i%2 == 0 { - if v.Value == "anyOf" || v.Value == "oneOf" || v.Value == "allOf" { - return true - } - } - } - return false + for i, v := range node.Content { + if i%2 == 0 { + if v.Value == "anyOf" || v.Value == "oneOf" || v.Value == "allOf" { + return true + } + } + } + return false } // IsNodeArray checks if a node is an array type func IsNodeArray(node *yaml.Node) bool { - if node == nil { - return false - } - return node.Tag == "!!seq" + if node == nil { + return false + } + return node.Tag == "!!seq" } // IsNodeStringValue checks if a node is a string value func IsNodeStringValue(node *yaml.Node) bool { - if node == nil { - return false - } - return node.Tag == "!!str" + if node == nil { + return false + } + return node.Tag == "!!str" } // IsNodeIntValue will check if a node is an int value func IsNodeIntValue(node *yaml.Node) bool { - if node == nil { - return false - } - return node.Tag == "!!int" + if node == nil { + return false + } + return node.Tag == "!!int" } // IsNodeFloatValue will check is a node is a float value. func IsNodeFloatValue(node *yaml.Node) bool { - if node == nil { - return false - } - return node.Tag == "!!float" + if node == nil { + return false + } + return node.Tag == "!!float" } // IsNodeBoolValue will check is a node is a bool func IsNodeBoolValue(node *yaml.Node) bool { - if node == nil { - return false - } - return node.Tag == "!!bool" + if node == nil { + return false + } + return node.Tag == "!!bool" } func IsNodeRefValue(node *yaml.Node) (bool, *yaml.Node, string) { - for i, r := range node.Content { - if i%2 == 0 { - if r.Value == "$ref" { - return true, r, node.Content[i+1].Value - } - } - } - return false, nil, "" + if node == nil { + return false, nil, "" + } + + for i, r := range node.Content { + if i%2 == 0 { + if r.Value == "$ref" { + return true, r, node.Content[i+1].Value + } + } + } + return false, nil, "" } // FixContext will clean up a JSONpath string to be correctly traversable. func FixContext(context string) string { - tokens := strings.Split(context, ".") - var cleaned = []string{} + tokens := strings.Split(context, ".") + var cleaned = []string{} - for i, t := range tokens { - if v, err := strconv.Atoi(t); err == nil { - if v < 200 { // codes start here - if cleaned[i-1] != "" { - cleaned[i-1] += fmt.Sprintf("[%v]", t) - } - } else { - cleaned = append(cleaned, t) - } - continue - } - cleaned = append(cleaned, strings.ReplaceAll(t, "(root)", "$")) - } + for i, t := range tokens { + if v, err := strconv.Atoi(t); err == nil { + if v < 200 { // codes start here + if cleaned[i-1] != "" { + cleaned[i-1] += fmt.Sprintf("[%v]", t) + } + } else { + cleaned = append(cleaned, t) + } + continue + } + cleaned = append(cleaned, strings.ReplaceAll(t, "(root)", "$")) + } - return strings.Join(cleaned, ".") + return strings.Join(cleaned, ".") } // IsJSON will tell you if a string is JSON or not. func IsJSON(testString string) bool { - if testString == "" { - return false - } - runes := []rune(strings.TrimSpace(testString)) - if runes[0] == '{' && runes[len(runes)-1] == '}' { - return true - } - return false + if testString == "" { + return false + } + runes := []rune(strings.TrimSpace(testString)) + if runes[0] == '{' && runes[len(runes)-1] == '}' { + return true + } + return false } // IsYAML will tell you if a string is YAML or not. func IsYAML(testString string) bool { - if testString == "" { - return false - } - if IsJSON(testString) { - return false - } - var n interface{} - err := yaml.Unmarshal([]byte(testString), &n) - if err != nil { - return false - } - _, err = yaml.Marshal(n) - return err == nil + if testString == "" { + return false + } + if IsJSON(testString) { + return false + } + var n interface{} + err := yaml.Unmarshal([]byte(testString), &n) + if err != nil { + return false + } + _, err = yaml.Marshal(n) + return err == nil } // ConvertYAMLtoJSON will do exactly what you think it will. It will deserialize YAML into serialized JSON. func ConvertYAMLtoJSON(yamlData []byte) ([]byte, error) { - var decodedYaml map[string]interface{} - err := yaml.Unmarshal(yamlData, &decodedYaml) - if err != nil { - return nil, err - } - // if the data can be decoded, it can be encoded (that's my view anyway). no need for an error check. - jsonData, _ := json.Marshal(decodedYaml) - return jsonData, nil + var decodedYaml map[string]interface{} + err := yaml.Unmarshal(yamlData, &decodedYaml) + if err != nil { + return nil, err + } + // if the data can be decoded, it can be encoded (that's my view anyway). no need for an error check. + jsonData, _ := json.Marshal(decodedYaml) + return jsonData, nil } // IsHttpVerb will check if an operation is valid or not. func IsHttpVerb(verb string) bool { - verbs := []string{"get", "post", "put", "patch", "delete", "options", "trace", "head"} - for _, v := range verbs { - if verb == v { - return true - } - } - return false + verbs := []string{"get", "post", "put", "patch", "delete", "options", "trace", "head"} + for _, v := range verbs { + if verb == v { + return true + } + } + return false } func ConvertComponentIdIntoFriendlyPathSearch(id string) (string, string) { - segs := strings.Split(id, "/") - name, _ := url.QueryUnescape(strings.ReplaceAll(segs[len(segs)-1], "~1", "/")) - var cleaned []string + segs := strings.Split(id, "/") + name, _ := url.QueryUnescape(strings.ReplaceAll(segs[len(segs)-1], "~1", "/")) + var cleaned []string - // check for strange spaces, chars and if found, wrap them up, clean them and create a new cleaned path. - for i := range segs { - reg, _ := regexp.MatchString("[%=;~.]", segs[i]) - if reg { - segs[i], _ = url.QueryUnescape(strings.ReplaceAll(segs[i], "~1", "/")) - segs[i] = fmt.Sprintf("['%s']", segs[i]) - if len(cleaned) > 0 { - cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", segs[i-1], segs[i]) - continue - } - } else { - intVal, err := strconv.ParseInt(segs[i], 10, 32) - if err == nil && intVal <= 99 { - segs[i] = fmt.Sprintf("[%d]", intVal) - cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", cleaned[len(cleaned)-1], segs[i]) - continue - } - if err == nil && intVal > 99 { - segs[i] = fmt.Sprintf("['%d']", intVal) - cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", cleaned[len(cleaned)-1], segs[i]) - continue - } - cleaned = append(cleaned, segs[i]) - } - } - _, err := strconv.ParseInt(name, 10, 32) - var replaced string - if err != nil { - replaced = strings.ReplaceAll(fmt.Sprintf("%s", - strings.Join(cleaned, ".")), "#", "$") - } else { - replaced = strings.ReplaceAll(fmt.Sprintf("%s", - strings.Join(cleaned, ".")), "#", "$") - } + // check for strange spaces, chars and if found, wrap them up, clean them and create a new cleaned path. + for i := range segs { + reg, _ := regexp.MatchString("[%=;~.]", segs[i]) + if reg { + segs[i], _ = url.QueryUnescape(strings.ReplaceAll(segs[i], "~1", "/")) + segs[i] = fmt.Sprintf("['%s']", segs[i]) + if len(cleaned) > 0 { + cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", segs[i-1], segs[i]) + continue + } + } else { + intVal, err := strconv.ParseInt(segs[i], 10, 32) + if err == nil && intVal <= 99 { + segs[i] = fmt.Sprintf("[%d]", intVal) + cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", cleaned[len(cleaned)-1], segs[i]) + continue + } + if err == nil && intVal > 99 { + segs[i] = fmt.Sprintf("['%d']", intVal) + cleaned[len(cleaned)-1] = fmt.Sprintf("%s%s", cleaned[len(cleaned)-1], segs[i]) + continue + } + cleaned = append(cleaned, segs[i]) + } + } + _, err := strconv.ParseInt(name, 10, 32) + var replaced string + if err != nil { + replaced = strings.ReplaceAll(fmt.Sprintf("%s", + strings.Join(cleaned, ".")), "#", "$") + } else { + replaced = strings.ReplaceAll(fmt.Sprintf("%s", + strings.Join(cleaned, ".")), "#", "$") + } - if len(replaced) > 0 { - if replaced[0] != '$' { - replaced = fmt.Sprintf("$%s", replaced) - } - } - return name, replaced + if len(replaced) > 0 { + if replaced[0] != '$' { + replaced = fmt.Sprintf("$%s", replaced) + } + } + return name, replaced } func ConvertComponentIdIntoPath(id string) (string, string) { - segs := strings.Split(id, "/") - name := segs[len(segs)-1] + segs := strings.Split(id, "/") + name := segs[len(segs)-1] - return name, strings.ReplaceAll(fmt.Sprintf("%s.%s", - strings.Join(segs[:len(segs)-1], "."), name), "#", "$") + return name, strings.ReplaceAll(fmt.Sprintf("%s.%s", + strings.Join(segs[:len(segs)-1], "."), name), "#", "$") } func RenderCodeSnippet(startNode *yaml.Node, specData []string, before, after int) string { - buf := new(strings.Builder) + buf := new(strings.Builder) - startLine := startNode.Line - before - endLine := startNode.Line + after + startLine := startNode.Line - before + endLine := startNode.Line + after - if startLine < 0 { - startLine = 0 - } + if startLine < 0 { + startLine = 0 + } - if endLine >= len(specData) { - endLine = len(specData) - 1 - } + if endLine >= len(specData) { + endLine = len(specData) - 1 + } - delta := endLine - startLine + delta := endLine - startLine - for i := 0; i < delta; i++ { - l := startLine + i - if l < len(specData) { - line := specData[l] - buf.WriteString(fmt.Sprintf("%s\n", line)) - } - } + for i := 0; i < delta; i++ { + l := startLine + i + if l < len(specData) { + line := specData[l] + buf.WriteString(fmt.Sprintf("%s\n", line)) + } + } - return buf.String() + return buf.String() } func DetectCase(input string) Case { - trim := strings.TrimSpace(input) - if trim == "" { - return UnknownCase - } + trim := strings.TrimSpace(input) + if trim == "" { + return UnknownCase + } - pascalCase := regexp.MustCompile("^[A-Z][a-z]+(?:[A-Z][a-z]+)*$") - camelCase := regexp.MustCompile("^[a-z]+(?:[A-Z][a-z]+)*$") - screamingSnakeCase := regexp.MustCompile("^[A-Z]+(_[A-Z]+)*$") - snakeCase := regexp.MustCompile("^[a-z]+(_[a-z]+)*$") - kebabCase := regexp.MustCompile("^[a-z]+(-[a-z]+)*$") - screamingKebabCase := regexp.MustCompile("^[A-Z]+(-[A-Z]+)*$") - if pascalCase.MatchString(trim) { - return PascalCase - } - if camelCase.MatchString(trim) { - return CamelCase - } - if screamingSnakeCase.MatchString(trim) { - return ScreamingSnakeCase - } - if snakeCase.MatchString(trim) { - return SnakeCase - } - if kebabCase.MatchString(trim) { - return KebabCase - } - if screamingKebabCase.MatchString(trim) { - return ScreamingKebabCase - } - return RegularCase + pascalCase := regexp.MustCompile("^[A-Z][a-z]+(?:[A-Z][a-z]+)*$") + camelCase := regexp.MustCompile("^[a-z]+(?:[A-Z][a-z]+)*$") + screamingSnakeCase := regexp.MustCompile("^[A-Z]+(_[A-Z]+)*$") + snakeCase := regexp.MustCompile("^[a-z]+(_[a-z]+)*$") + kebabCase := regexp.MustCompile("^[a-z]+(-[a-z]+)*$") + screamingKebabCase := regexp.MustCompile("^[A-Z]+(-[A-Z]+)*$") + if pascalCase.MatchString(trim) { + return PascalCase + } + if camelCase.MatchString(trim) { + return CamelCase + } + if screamingSnakeCase.MatchString(trim) { + return ScreamingSnakeCase + } + if snakeCase.MatchString(trim) { + return SnakeCase + } + if kebabCase.MatchString(trim) { + return KebabCase + } + if screamingKebabCase.MatchString(trim) { + return ScreamingKebabCase + } + return RegularCase } // CheckEnumForDuplicates will check an array of nodes to check if there are any duplicate values. func CheckEnumForDuplicates(seq []*yaml.Node) []*yaml.Node { - var res []*yaml.Node - seen := make(map[string]*yaml.Node) + var res []*yaml.Node + seen := make(map[string]*yaml.Node) - for _, enum := range seq { - if seen[enum.Value] != nil { - res = append(res, enum) - continue - } - seen[enum.Value] = enum - } - return res + for _, enum := range seq { + if seen[enum.Value] != nil { + res = append(res, enum) + continue + } + seen[enum.Value] = enum + } + return res }