mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
Every `Build()` method now requires a `context.Context`. This is so the rolodex knows where to resolve from when locating relative links. Without knowing where we are, there is no way to resolve anything. This new mechanism allows the model to recurse across as many files as required to locate references, without loosing track of where we are in the process. Signed-off-by: quobix <dave@quobix.com>
520 lines
14 KiB
Go
520 lines
14 KiB
Go
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package index
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/pb33f/libopenapi/utils"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
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)
|
|
fullDef := fmt.Sprintf("%s%s", index.specAbsolutePath, def)
|
|
|
|
ref := &Reference{
|
|
FullDefinition: fullDef,
|
|
Definition: def,
|
|
Name: name,
|
|
Node: schema,
|
|
Path: fmt.Sprintf("$.components.schemas.%s", name),
|
|
ParentNode: schemasNode,
|
|
RequiredRefProperties: extractDefinitionRequiredRefProperties(schemasNode, map[string][]string{}, fullDef),
|
|
}
|
|
index.allComponentSchemaDefinitions[def] = ref
|
|
}
|
|
}
|
|
|
|
// extractDefinitionRequiredRefProperties goes through the direct properties of a schema and extracts the map of required definitions from within it
|
|
func extractDefinitionRequiredRefProperties(schemaNode *yaml.Node, reqRefProps map[string][]string, fulldef 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 = extractDefinitionRequiredRefProperties(param, reqRefProps, fulldef)
|
|
}
|
|
|
|
// 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 = extractRequiredReferenceProperties(fulldef, 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 = extractRequiredReferenceProperties(fulldef, 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 extractRequiredReferenceProperties(fulldef string, 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
|
|
}
|
|
|
|
// explode defpath
|
|
exp := strings.Split(defPath, "#/")
|
|
if len(exp) == 2 {
|
|
if exp[0] != "" {
|
|
if !strings.HasPrefix(exp[0], "http") {
|
|
|
|
if !filepath.IsAbs(exp[0]) {
|
|
|
|
if strings.HasPrefix(fulldef, "http") {
|
|
|
|
u, _ := url.Parse(fulldef)
|
|
p := filepath.Dir(u.Path)
|
|
abs, _ := filepath.Abs(filepath.Join(p, exp[0]))
|
|
u.Path = abs
|
|
defPath = fmt.Sprintf("%s#/%s", u.String(), exp[1])
|
|
|
|
} else {
|
|
|
|
abs, _ := filepath.Abs(filepath.Join(filepath.Dir(fulldef), exp[0]))
|
|
defPath = fmt.Sprintf("%s#/%s", abs, exp[1])
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
} else {
|
|
|
|
if strings.HasPrefix(exp[0], "http") {
|
|
|
|
defPath = exp[0]
|
|
|
|
} else {
|
|
|
|
// file shit again
|
|
|
|
if filepath.IsAbs(exp[0]) {
|
|
|
|
defPath = exp[0]
|
|
|
|
} else {
|
|
|
|
// check full def and decide what to do next.
|
|
if strings.HasPrefix(fulldef, "http") {
|
|
|
|
u, _ := url.Parse(fulldef)
|
|
p := filepath.Dir(u.Path)
|
|
abs, _ := filepath.Abs(filepath.Join(p, exp[0]))
|
|
u.Path = abs
|
|
defPath = u.String()
|
|
|
|
} else {
|
|
|
|
defPath, _ = filepath.Abs(filepath.Join(filepath.Dir(fulldef), exp[0]))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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.paramInlineDuplicateNames {
|
|
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 paramRef == nil {
|
|
// could be in the rolodex
|
|
ref, _ := index.SearchIndexForReference(paramRefName)
|
|
if ref != nil {
|
|
paramRef = ref
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if paramRef != nil {
|
|
index.paramOpRefs[pathItemNode.Value][method][paramRefName] =
|
|
append(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 name, check if the `in` type is also the same, if so, it's a duplicate.
|
|
if len(index.paramOpRefs[pathItemNode.Value][method][ref.Name]) > 0 {
|
|
|
|
currentNode := ref.Node
|
|
checkNodes := index.paramOpRefs[pathItemNode.Value][method][ref.Name]
|
|
_, currentIn := utils.FindKeyNodeTop("in", currentNode.Content)
|
|
|
|
for _, checkNode := range checkNodes {
|
|
|
|
_, checkIn := utils.FindKeyNodeTop("in", checkNode.Node.Content)
|
|
|
|
if currentIn != nil && checkIn != nil && currentIn.Value == checkIn.Value {
|
|
|
|
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` and `in` type", method, pathItemNode.Value, i, vn.Value),
|
|
Node: param,
|
|
Path: path,
|
|
})
|
|
} else {
|
|
index.paramOpRefs[pathItemNode.Value][method][ref.Name] =
|
|
append(index.paramOpRefs[pathItemNode.Value][method][ref.Name], ref)
|
|
}
|
|
}
|
|
} else {
|
|
index.paramOpRefs[pathItemNode.Value][method][ref.Name] =
|
|
append(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 {
|
|
if !strings.Contains(cleanedPath, "/") {
|
|
p = ""
|
|
} else {
|
|
p = cleanedPath
|
|
}
|
|
|
|
}
|
|
return strings.TrimSuffix(p, "/")
|
|
}
|