mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
Tests all passing, runs super fast, pulls in every single DigitalOcean spec and parses it. There may be some issues deeper down in the models, but for now high level tests all pass.
426 lines
15 KiB
Go
426 lines
15 KiB
Go
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package index
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/pb33f/libopenapi/utils"
|
|
"gopkg.in/yaml.v3"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
func (index *SpecIndex) extractDefinitionsAndSchemas(schemasNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, schema := range schemasNode.Content {
|
|
if i%2 == 0 {
|
|
name = schema.Value
|
|
continue
|
|
}
|
|
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: schema,
|
|
Path: fmt.Sprintf("$.components.schemas.%s", name),
|
|
ParentNode: schemasNode,
|
|
RequiredRefProperties: index.extractDefinitionRequiredRefProperties(schemasNode, map[string][]string{}),
|
|
}
|
|
index.allComponentSchemaDefinitions[def] = ref
|
|
}
|
|
}
|
|
|
|
// extractDefinitionRequiredRefProperties goes through the direct properties of a schema and extracts the map of required definitions from within it
|
|
func (index *SpecIndex) extractDefinitionRequiredRefProperties(schemaNode *yaml.Node, reqRefProps map[string][]string) map[string][]string {
|
|
if schemaNode == nil {
|
|
return reqRefProps
|
|
}
|
|
|
|
// If the node we're looking at is a direct ref to another model without any properties, mark it as required, but still continue to look for required properties
|
|
isRef, _, defPath := utils.IsNodeRefValue(schemaNode)
|
|
if isRef {
|
|
if _, ok := reqRefProps[defPath]; !ok {
|
|
reqRefProps[defPath] = []string{}
|
|
}
|
|
}
|
|
|
|
// Check for a required parameters list, and return if none exists, as any properties will be optional
|
|
_, requiredSeqNode := utils.FindKeyNodeTop("required", schemaNode.Content)
|
|
if requiredSeqNode == nil {
|
|
return reqRefProps
|
|
}
|
|
|
|
_, propertiesMapNode := utils.FindKeyNodeTop("properties", schemaNode.Content)
|
|
if propertiesMapNode == nil {
|
|
// TODO: Log a warning on the resolver, because if you have required properties, but no actual properties, something is wrong
|
|
return reqRefProps
|
|
}
|
|
|
|
name := ""
|
|
for i, param := range propertiesMapNode.Content {
|
|
if i%2 == 0 {
|
|
name = param.Value
|
|
continue
|
|
}
|
|
|
|
// Check to see if the current property is directly embedded within the current schema, and handle its properties if so
|
|
_, paramPropertiesMapNode := utils.FindKeyNodeTop("properties", param.Content)
|
|
if paramPropertiesMapNode != nil {
|
|
reqRefProps = index.extractDefinitionRequiredRefProperties(param, reqRefProps)
|
|
}
|
|
|
|
// Check to see if the current property is polymorphic, and dive into that model if so
|
|
for _, key := range []string{"allOf", "oneOf", "anyOf"} {
|
|
_, ofNode := utils.FindKeyNodeTop(key, param.Content)
|
|
if ofNode != nil {
|
|
for _, ofNodeItem := range ofNode.Content {
|
|
reqRefProps = index.extractRequiredReferenceProperties(ofNodeItem, name, reqRefProps)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run through each of the required properties and extract _their_ required references
|
|
for _, requiredPropertyNode := range requiredSeqNode.Content {
|
|
_, requiredPropDefNode := utils.FindKeyNodeTop(requiredPropertyNode.Value, propertiesMapNode.Content)
|
|
if requiredPropDefNode == nil {
|
|
continue
|
|
}
|
|
|
|
reqRefProps = index.extractRequiredReferenceProperties(requiredPropDefNode, requiredPropertyNode.Value, reqRefProps)
|
|
}
|
|
|
|
return reqRefProps
|
|
}
|
|
|
|
// extractRequiredReferenceProperties returns a map of definition names to the property or properties which reference it within a node
|
|
func (index *SpecIndex) extractRequiredReferenceProperties(requiredPropDefNode *yaml.Node, propName string, reqRefProps map[string][]string) map[string][]string {
|
|
isRef, _, defPath := utils.IsNodeRefValue(requiredPropDefNode)
|
|
if !isRef {
|
|
_, defItems := utils.FindKeyNodeTop("items", requiredPropDefNode.Content)
|
|
if defItems != nil {
|
|
isRef, _, defPath = utils.IsNodeRefValue(defItems)
|
|
}
|
|
}
|
|
|
|
if /* still */ !isRef {
|
|
return reqRefProps
|
|
}
|
|
|
|
if _, ok := reqRefProps[defPath]; !ok {
|
|
reqRefProps[defPath] = []string{}
|
|
}
|
|
reqRefProps[defPath] = append(reqRefProps[defPath], propName)
|
|
|
|
return reqRefProps
|
|
}
|
|
|
|
func (index *SpecIndex) extractComponentParameters(paramsNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, param := range paramsNode.Content {
|
|
if i%2 == 0 {
|
|
name = param.Value
|
|
continue
|
|
}
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: param,
|
|
}
|
|
index.allParameters[def] = ref
|
|
}
|
|
}
|
|
|
|
func (index *SpecIndex) extractComponentRequestBodies(requestBodiesNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, reqBod := range requestBodiesNode.Content {
|
|
if i%2 == 0 {
|
|
name = reqBod.Value
|
|
continue
|
|
}
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: reqBod,
|
|
}
|
|
index.allRequestBodies[def] = ref
|
|
}
|
|
}
|
|
|
|
func (index *SpecIndex) extractComponentResponses(responsesNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, response := range responsesNode.Content {
|
|
if i%2 == 0 {
|
|
name = response.Value
|
|
continue
|
|
}
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: response,
|
|
}
|
|
index.allResponses[def] = ref
|
|
}
|
|
}
|
|
|
|
func (index *SpecIndex) extractComponentHeaders(headersNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, header := range headersNode.Content {
|
|
if i%2 == 0 {
|
|
name = header.Value
|
|
continue
|
|
}
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: header,
|
|
}
|
|
index.allHeaders[def] = ref
|
|
}
|
|
}
|
|
|
|
func (index *SpecIndex) extractComponentCallbacks(callbacksNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, callback := range callbacksNode.Content {
|
|
if i%2 == 0 {
|
|
name = callback.Value
|
|
continue
|
|
}
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: callback,
|
|
}
|
|
index.allCallbacks[def] = ref
|
|
}
|
|
}
|
|
|
|
func (index *SpecIndex) extractComponentLinks(linksNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, link := range linksNode.Content {
|
|
if i%2 == 0 {
|
|
name = link.Value
|
|
continue
|
|
}
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: link,
|
|
}
|
|
index.allLinks[def] = ref
|
|
}
|
|
}
|
|
|
|
func (index *SpecIndex) extractComponentExamples(examplesNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, example := range examplesNode.Content {
|
|
if i%2 == 0 {
|
|
name = example.Value
|
|
continue
|
|
}
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: example,
|
|
}
|
|
index.allExamples[def] = ref
|
|
}
|
|
}
|
|
|
|
func (index *SpecIndex) extractComponentSecuritySchemes(securitySchemesNode *yaml.Node, pathPrefix string) {
|
|
var name string
|
|
for i, secScheme := range securitySchemesNode.Content {
|
|
if i%2 == 0 {
|
|
name = secScheme.Value
|
|
continue
|
|
}
|
|
def := fmt.Sprintf("%s%s", pathPrefix, name)
|
|
ref := &Reference{
|
|
Definition: def,
|
|
Name: name,
|
|
Node: secScheme,
|
|
ParentNode: securitySchemesNode,
|
|
Path: fmt.Sprintf("$.components.securitySchemes.%s", name),
|
|
}
|
|
index.allSecuritySchemes[def] = ref
|
|
}
|
|
}
|
|
|
|
func (index *SpecIndex) countUniqueInlineDuplicates() int {
|
|
if index.componentsInlineParamUniqueCount > 0 {
|
|
return index.componentsInlineParamUniqueCount
|
|
}
|
|
unique := 0
|
|
for _, p := range index.paramInlineDuplicates {
|
|
if len(p) == 1 {
|
|
unique++
|
|
}
|
|
}
|
|
index.componentsInlineParamUniqueCount = unique
|
|
return unique
|
|
}
|
|
|
|
func (index *SpecIndex) scanOperationParams(params []*yaml.Node, pathItemNode *yaml.Node, method string) {
|
|
for i, param := range params {
|
|
// param is ref
|
|
if len(param.Content) > 0 && param.Content[0].Value == "$ref" {
|
|
|
|
paramRefName := param.Content[1].Value
|
|
paramRef := index.allMappedRefs[paramRefName]
|
|
|
|
if index.paramOpRefs[pathItemNode.Value] == nil {
|
|
index.paramOpRefs[pathItemNode.Value] = make(map[string]map[string]*Reference)
|
|
index.paramOpRefs[pathItemNode.Value][method] = make(map[string]*Reference)
|
|
|
|
}
|
|
// if we know the path, but it's a new method
|
|
if index.paramOpRefs[pathItemNode.Value][method] == nil {
|
|
index.paramOpRefs[pathItemNode.Value][method] = make(map[string]*Reference)
|
|
}
|
|
|
|
// if this is a duplicate, add an error and ignore it
|
|
if index.paramOpRefs[pathItemNode.Value][method][paramRefName] != nil {
|
|
path := fmt.Sprintf("$.paths.%s.%s.parameters[%d]", pathItemNode.Value, method, i)
|
|
if method == "top" {
|
|
path = fmt.Sprintf("$.paths.%s.parameters[%d]", pathItemNode.Value, i)
|
|
}
|
|
|
|
index.operationParamErrors = append(index.operationParamErrors, &IndexingError{
|
|
Err: fmt.Errorf("the `%s` operation parameter at path `%s`, "+
|
|
"index %d has a duplicate ref `%s`", method, pathItemNode.Value, i, paramRefName),
|
|
Node: param,
|
|
Path: path,
|
|
})
|
|
} else {
|
|
index.paramOpRefs[pathItemNode.Value][method][paramRefName] = paramRef
|
|
}
|
|
|
|
continue
|
|
|
|
} else {
|
|
|
|
// param is inline.
|
|
_, vn := utils.FindKeyNode("name", param.Content)
|
|
|
|
path := fmt.Sprintf("$.paths.%s.%s.parameters[%d]", pathItemNode.Value, method, i)
|
|
if method == "top" {
|
|
path = fmt.Sprintf("$.paths.%s.parameters[%d]", pathItemNode.Value, i)
|
|
}
|
|
|
|
if vn == nil {
|
|
index.operationParamErrors = append(index.operationParamErrors, &IndexingError{
|
|
Err: fmt.Errorf("the '%s' operation parameter at path '%s', index %d has no 'name' value",
|
|
method, pathItemNode.Value, i),
|
|
Node: param,
|
|
Path: path,
|
|
})
|
|
continue
|
|
}
|
|
|
|
ref := &Reference{
|
|
Definition: vn.Value,
|
|
Name: vn.Value,
|
|
Node: param,
|
|
Path: path,
|
|
}
|
|
if index.paramOpRefs[pathItemNode.Value] == nil {
|
|
index.paramOpRefs[pathItemNode.Value] = make(map[string]map[string]*Reference)
|
|
index.paramOpRefs[pathItemNode.Value][method] = make(map[string]*Reference)
|
|
}
|
|
|
|
// if we know the path but this is a new method.
|
|
if index.paramOpRefs[pathItemNode.Value][method] == nil {
|
|
index.paramOpRefs[pathItemNode.Value][method] = make(map[string]*Reference)
|
|
}
|
|
|
|
// if this is a duplicate, add an error and ignore it
|
|
if index.paramOpRefs[pathItemNode.Value][method][ref.Name] != nil {
|
|
path := fmt.Sprintf("$.paths.%s.%s.parameters[%d]", pathItemNode.Value, method, i)
|
|
if method == "top" {
|
|
path = fmt.Sprintf("$.paths.%s.parameters[%d]", pathItemNode.Value, i)
|
|
}
|
|
|
|
index.operationParamErrors = append(index.operationParamErrors, &IndexingError{
|
|
Err: fmt.Errorf("the `%s` operation parameter at path `%s`, "+
|
|
"index %d has a duplicate name `%s`", method, pathItemNode.Value, i, vn.Value),
|
|
Node: param,
|
|
Path: path,
|
|
})
|
|
} else {
|
|
index.paramOpRefs[pathItemNode.Value][method][ref.Name] = ref
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func runIndexFunction(funcs []func() int, wg *sync.WaitGroup) {
|
|
for _, cFunc := range funcs {
|
|
go func(wg *sync.WaitGroup, cf func() int) {
|
|
cf()
|
|
wg.Done()
|
|
}(wg, cFunc)
|
|
}
|
|
}
|
|
|
|
func GenerateCleanSpecConfigBaseURL(baseURL *url.URL, dir string, includeFile bool) string {
|
|
|
|
cleanedPath := baseURL.Path // not cleaned yet!
|
|
|
|
// create a slice of path segments from existing path
|
|
pathSegs := strings.Split(cleanedPath, "/")
|
|
dirSegs := strings.Split(dir, "/")
|
|
|
|
var cleanedSegs []string
|
|
if !includeFile {
|
|
dirSegs = dirSegs[:len(dirSegs)-1]
|
|
}
|
|
|
|
// relative paths are a pain in the ass, damn you digital ocean, use a single spec, and break them
|
|
// down into services, please don't blast apart specs into a billion shards.
|
|
if strings.Contains(dir, "../") {
|
|
for s := range dirSegs {
|
|
if dirSegs[s] == ".." {
|
|
// chop off the last segment of the base path.
|
|
if len(pathSegs) > 0 {
|
|
pathSegs = pathSegs[:len(pathSegs)-1]
|
|
}
|
|
} else {
|
|
cleanedSegs = append(cleanedSegs, dirSegs[s])
|
|
}
|
|
}
|
|
cleanedPath = fmt.Sprintf("%s/%s", strings.Join(pathSegs, "/"), strings.Join(cleanedSegs, "/"))
|
|
} else {
|
|
if !strings.HasPrefix(dir, "http") {
|
|
if len(pathSegs) > 1 || len(dirSegs) > 1 {
|
|
cleanedPath = fmt.Sprintf("%s/%s", strings.Join(pathSegs, "/"), strings.Join(dirSegs, "/"))
|
|
}
|
|
} else {
|
|
cleanedPath = strings.Join(dirSegs, "/")
|
|
}
|
|
}
|
|
var p string
|
|
if baseURL.Scheme != "" && !strings.HasPrefix(dir, "http") {
|
|
p = fmt.Sprintf("%s://%s%s", baseURL.Scheme, baseURL.Host, cleanedPath)
|
|
} else {
|
|
p = cleanedPath
|
|
}
|
|
if strings.HasSuffix(p, "/") {
|
|
p = p[:len(p)-1]
|
|
}
|
|
return p
|
|
|
|
}
|
|
|
|
|