Files
libopenapi/index/find_component.go
quobix 43bf162e0c Added new Render method to top level document.
Like `RenderAndReload` this method will render / print out the document, however it won’t read it back in and reload the model from the ground up. This is a non-destructive method that does not reload the model from the rendered document.

If you don’t use the low model or you don’t care that the high level model and low-level models are out of sync? No problem. Otherwise keep this in mind that the low level model will only represent the original un-mutated document, as will the index.

Signed-off-by: quobix <dave@quobix.com>
2023-09-14 07:30:03 -04:00

458 lines
13 KiB
Go

// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
package index
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/pb33f/libopenapi/utils"
"github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath"
"gopkg.in/yaml.v3"
)
// FindComponent will locate a component by its reference, returns nil if nothing is found.
// This method will recurse through remote, local and file references. For each new external reference
// a new index will be created. These indexes can then be traversed recursively.
func (index *SpecIndex) FindComponent(componentId string, parent *yaml.Node) *Reference {
if index.root == nil {
return nil
}
remoteLookup := func(id string) (*yaml.Node, *yaml.Node, error) {
if index.config.AllowRemoteLookup {
return index.lookupRemoteReference(id)
} else {
return nil, nil, fmt.Errorf("remote lookups are not permitted, " +
"please set AllowRemoteLookup to true in the configuration")
}
}
fileLookup := func(id string) (*yaml.Node, *yaml.Node, error) {
if index.config.AllowFileLookup {
return index.lookupFileReference(id)
} else {
return nil, nil, fmt.Errorf("local lookups are not permitted, " +
"please set AllowFileLookup to true in the configuration")
}
}
switch DetermineReferenceResolveType(componentId) {
case LocalResolve: // ideally, every single ref in every single spec is local. however, this is not the case.
return index.FindComponentInRoot(componentId)
case HttpResolve:
uri := strings.Split(componentId, "#")
if len(uri) >= 2 {
return index.performExternalLookup(uri, componentId, remoteLookup, parent)
}
if len(uri) == 1 {
// if there is no reference, second segment is empty / has no name
// this means there is no component to look-up and the entire file should be pulled in.
// to stop all the other code from breaking (that is expecting a component), let's just post-pend
// a hash to the end of the componentId and ensure the uri slice is as expected.
// described in https://github.com/pb33f/libopenapi/issues/37
componentId = fmt.Sprintf("%s#", componentId)
uri = append(uri, "")
return index.performExternalLookup(uri, componentId, remoteLookup, parent)
}
case FileResolve:
uri := strings.Split(componentId, "#")
if len(uri) == 2 {
return index.performExternalLookup(uri, componentId, fileLookup, parent)
}
if len(uri) == 1 {
// if there is no reference, second segment is empty / has no name
// this means there is no component to look-up and the entire file should be pulled in.
// to stop all the other code from breaking (that is expecting a component), let's just post-pend
// a hash to the end of the componentId and ensure the uri slice is as expected.
// described in https://github.com/pb33f/libopenapi/issues/37
//
// ^^ this same issue was re-reported in file based lookups in vacuum.
// more info here: https://github.com/daveshanley/vacuum/issues/225
componentId = fmt.Sprintf("%s#", componentId)
uri = append(uri, "")
return index.performExternalLookup(uri, componentId, fileLookup, parent)
}
}
return nil
}
var httpClient = &http.Client{Timeout: time.Duration(60) * time.Second}
type RemoteURLHandler = func(url string) (*http.Response, error)
func getRemoteDoc(g RemoteURLHandler, u string, d chan []byte, e chan error) {
resp, err := g(u)
if err != nil {
e <- err
close(e)
close(d)
return
}
var body []byte
body, _ = io.ReadAll(resp.Body)
d <- body
close(e)
close(d)
}
func (index *SpecIndex) lookupRemoteReference(ref string) (*yaml.Node, *yaml.Node, error) {
// split string to remove file reference
uri := strings.Split(ref, "#")
// have we already seen this remote source?
var parsedRemoteDocument *yaml.Node
alreadySeen, foundDocument := index.CheckForSeenRemoteSource(uri[0])
if alreadySeen {
parsedRemoteDocument = foundDocument
} else {
d := make(chan bool)
var body []byte
var err error
go func(uri string) {
bc := make(chan []byte)
ec := make(chan error)
var getter RemoteURLHandler = httpClient.Get
if index.config != nil && index.config.RemoteURLHandler != nil {
getter = index.config.RemoteURLHandler
}
// if we have a remote handler, use it instead of the default.
if index.config != nil && index.config.FSHandler != nil {
go func() {
remoteFS := index.config.FSHandler
remoteFile, rErr := remoteFS.Open(uri)
if rErr != nil {
e := fmt.Errorf("unable to open remote file: %s", rErr)
ec <- e
return
}
b, ioErr := io.ReadAll(remoteFile)
if ioErr != nil {
e := fmt.Errorf("unable to read remote file bytes: %s", ioErr)
ec <- e
return
}
bc <- b
}()
} else {
go getRemoteDoc(getter, uri, bc, ec)
}
select {
case v := <-bc:
body = v
break
case er := <-ec:
err = er
break
}
if len(body) > 0 {
var remoteDoc yaml.Node
er := yaml.Unmarshal(body, &remoteDoc)
if er != nil {
err = er
d <- true
return
}
parsedRemoteDocument = &remoteDoc
if index.config != nil {
index.config.seenRemoteSources.Store(uri, &remoteDoc)
}
}
d <- true
}(uri[0])
// wait for double go fun.
<-d
if err != nil {
// no bueno.
return nil, nil, err
}
}
// lookup item from reference by using a path query.
var query string
if len(uri) >= 2 {
query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", "."))
} else {
query = "$"
}
query, err := url.PathUnescape(query)
if err != nil {
return nil, nil, err
}
// remove any URL encoding
query = strings.Replace(query, "~1", "./", 1)
query = strings.ReplaceAll(query, "~1", "/")
path, err := yamlpath.NewPath(query)
if err != nil {
return nil, nil, err
}
result, _ := path.Find(parsedRemoteDocument)
if len(result) == 1 {
return result[0], parsedRemoteDocument, nil
}
return nil, nil, nil
}
func (index *SpecIndex) lookupFileReference(ref string) (*yaml.Node, *yaml.Node, error) {
// split string to remove file reference
uri := strings.Split(ref, "#")
file := strings.ReplaceAll(uri[0], "file:", "")
filePath := filepath.Dir(file)
fileName := filepath.Base(file)
var parsedRemoteDocument *yaml.Node
if index.seenRemoteSources[file] != nil {
parsedRemoteDocument = index.seenRemoteSources[file]
} else {
base := index.config.BasePath
fileToRead := filepath.Join(base, filePath, fileName)
var body []byte
var err error
// if we have an FS handler, use it instead of the default behavior
if index.config != nil && index.config.FSHandler != nil {
remoteFS := index.config.FSHandler
remoteFile, rErr := remoteFS.Open(fileToRead)
if rErr != nil {
e := fmt.Errorf("unable to open file: %s", rErr)
return nil, nil, e
}
body, err = io.ReadAll(remoteFile)
if err != nil {
e := fmt.Errorf("unable to read file bytes: %s", err)
return nil, nil, e
}
} else {
// try and read the file off the local file system, if it fails
// check for a baseURL and then ask our remote lookup function to go try and get it.
body, err = os.ReadFile(fileToRead)
if err != nil {
// if we have a baseURL, then we can try and get the file from there.
if index.config != nil && index.config.BaseURL != nil {
u := index.config.BaseURL
remoteRef := GenerateCleanSpecConfigBaseURL(u, ref, true)
a, b, e := index.lookupRemoteReference(remoteRef)
if e != nil {
// give up, we can't find the file, not locally, not remotely. It's toast.
return nil, nil, e
}
return a, b, nil
} else {
// no baseURL? then we can't do anything, give up.
return nil, nil, err
}
}
}
var remoteDoc yaml.Node
err = yaml.Unmarshal(body, &remoteDoc)
if err != nil {
return nil, nil, err
}
parsedRemoteDocument = &remoteDoc
if index.seenLocalSources != nil {
index.sourceLock.Lock()
index.seenLocalSources[file] = &remoteDoc
index.sourceLock.Unlock()
}
}
// lookup item from reference by using a path query.
var query string
if len(uri) >= 2 {
query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", "."))
} else {
query = "$"
}
query, err := url.PathUnescape(query)
if err != nil {
return nil, nil, err
}
// remove any URL encoding
query = strings.Replace(query, "~1", "./", 1)
query = strings.ReplaceAll(query, "~1", "/")
path, err := yamlpath.NewPath(query)
if err != nil {
return nil, nil, err
}
result, _ := path.Find(parsedRemoteDocument)
if len(result) == 1 {
return result[0], parsedRemoteDocument, nil
}
return nil, parsedRemoteDocument, nil
}
func (index *SpecIndex) FindComponentInRoot(componentId string) *Reference {
if index.root != nil {
// check component for url encoding.
if strings.Contains(componentId, "%") {
// decode the url.
componentId, _ = url.QueryUnescape(componentId)
}
name, friendlySearch := utils.ConvertComponentIdIntoFriendlyPathSearch(componentId)
path, err := yamlpath.NewPath(friendlySearch)
if path == nil || err != nil {
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: resNode,
Path: friendlySearch,
RequiredRefProperties: index.extractDefinitionRequiredRefProperties(res[0], map[string][]string{}),
}
return ref
}
}
return nil
}
func (index *SpecIndex) performExternalLookup(uri []string, componentId string,
lookupFunction ExternalLookupFunction, parent *yaml.Node) *Reference {
if len(uri) > 0 {
index.externalLock.RLock()
externalSpecIndex := index.externalSpecIndex[uri[0]]
index.externalLock.RUnlock()
if externalSpecIndex == nil {
_, newRoot, err := lookupFunction(componentId)
if err != nil {
indexError := &IndexingError{
Err: err,
Node: parent,
Path: componentId,
}
index.errorLock.Lock()
index.refErrors = append(index.refErrors, indexError)
index.errorLock.Unlock()
return nil
}
// cool, cool, lets index this spec also. This is a recursive action and will keep going
// until all remote references have been found.
var bp *url.URL
var bd string
if index.config.BaseURL != nil {
bp = index.config.BaseURL
}
if index.config.BasePath != "" {
bd = index.config.BasePath
}
var path, newBasePath string
var newUrl *url.URL
if bp != nil {
path = GenerateCleanSpecConfigBaseURL(bp, uri[0], false)
newUrl, _ = url.Parse(path)
newBasePath = filepath.Dir(filepath.Join(index.config.BasePath, filepath.Dir(newUrl.Path)))
}
if bd != "" {
if len(uri[0]) > 0 {
// if there is no base url defined, but we can know we have been requested remotely,
// set the base url to the remote url base path.
// first check if the first param is actually a URL
io, er := url.ParseRequestURI(uri[0])
if er != nil {
newBasePath = filepath.Dir(filepath.Join(bd, uri[0]))
} else {
if newUrl == nil || newUrl.String() != io.String() {
newUrl, _ = url.Parse(fmt.Sprintf("%s://%s%s", io.Scheme, io.Host, filepath.Dir(io.Path)))
}
newBasePath = filepath.Dir(filepath.Join(bd, uri[1]))
}
} else {
newBasePath = filepath.Dir(filepath.Join(bd, uri[0]))
}
}
if newUrl != nil || newBasePath != "" {
newConfig := &SpecIndexConfig{
BaseURL: newUrl,
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
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
}
}
}
if externalSpecIndex != nil {
foundRef := externalSpecIndex.FindComponentInRoot(uri[1])
if foundRef != nil {
nameSegs := strings.Split(uri[1], "/")
ref := &Reference{
Definition: componentId,
Name: nameSegs[len(nameSegs)-1],
Node: foundRef.Node,
IsRemote: true,
RemoteLocation: componentId,
Path: foundRef.Path,
}
return ref
}
}
}
return nil
}