Files
libopenapi/index/utility_methods.go
quobix 8717b3cd33 An enormous amount of surgery on the low level model.
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>
2023-10-23 15:04:34 -04:00

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, "/")
}