mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
first round of a number I am sure, lots to clean. Signed-off-by: quobix <dave@quobix.com>
607 lines
14 KiB
Go
607 lines
14 KiB
Go
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package index
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/pb33f/libopenapi/datamodel"
|
|
"gopkg.in/yaml.v3"
|
|
"io"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type HasIndex interface {
|
|
GetIndex() *SpecIndex
|
|
}
|
|
|
|
type CanBeIndexed interface {
|
|
Index(config *SpecIndexConfig) (*SpecIndex, error)
|
|
}
|
|
|
|
type RolodexFile interface {
|
|
GetContent() string
|
|
GetFileExtension() FileExtension
|
|
GetFullPath() string
|
|
GetErrors() []error
|
|
GetContentAsYAMLNode() (*yaml.Node, error)
|
|
GetIndex() *SpecIndex
|
|
Name() string
|
|
ModTime() time.Time
|
|
IsDir() bool
|
|
Sys() any
|
|
Size() int64
|
|
Mode() os.FileMode
|
|
}
|
|
|
|
var logger *slog.Logger
|
|
|
|
func init() {
|
|
logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
}))
|
|
|
|
}
|
|
|
|
type RolodexFS interface {
|
|
Open(name string) (fs.File, error)
|
|
GetFiles() map[string]RolodexFile
|
|
}
|
|
|
|
type Rolodex struct {
|
|
localFS map[string]fs.FS
|
|
remoteFS map[string]fs.FS
|
|
indexed bool
|
|
built bool
|
|
resolved bool
|
|
circChecked bool
|
|
indexConfig *SpecIndexConfig
|
|
indexingDuration time.Duration
|
|
indexes []*SpecIndex
|
|
rootIndex *SpecIndex
|
|
rootNode *yaml.Node
|
|
caughtErrors []error
|
|
ignoredCircularReferences []*CircularReferenceResult
|
|
}
|
|
|
|
type rolodexFile struct {
|
|
location string
|
|
rolodex *Rolodex
|
|
index *SpecIndex
|
|
localFile *LocalFile
|
|
remoteFile *RemoteFile
|
|
}
|
|
|
|
func (rf *rolodexFile) Name() string {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.filename
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.filename
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (rf *rolodexFile) GetIndex() *SpecIndex {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.GetIndex()
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.GetIndex()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (rf *rolodexFile) Index(config *SpecIndexConfig) (*SpecIndex, error) {
|
|
if rf.index != nil {
|
|
return rf.index, nil
|
|
}
|
|
var content []byte
|
|
if rf.localFile != nil {
|
|
content = rf.localFile.data
|
|
}
|
|
if rf.remoteFile != nil {
|
|
content = rf.remoteFile.data
|
|
}
|
|
|
|
// first, we must parse the content of the file
|
|
info, err := datamodel.ExtractSpecInfo(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// create a new index for this file and link it to this rolodex.
|
|
config.Rolodex = rf.rolodex
|
|
index := NewSpecIndexWithConfig(info.RootNode, config)
|
|
rf.index = index
|
|
return index, nil
|
|
|
|
}
|
|
|
|
func (rf *rolodexFile) GetContent() string {
|
|
if rf.localFile != nil {
|
|
return string(rf.localFile.data)
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return string(rf.remoteFile.data)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (rf *rolodexFile) GetContentAsYAMLNode() (*yaml.Node, error) {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.GetContentAsYAMLNode()
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.GetContentAsYAMLNode()
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (rf *rolodexFile) GetFileExtension() FileExtension {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.extension
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.extension
|
|
}
|
|
return UNSUPPORTED
|
|
}
|
|
func (rf *rolodexFile) GetFullPath() string {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.fullPath
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.fullPath
|
|
}
|
|
return ""
|
|
}
|
|
func (rf *rolodexFile) ModTime() time.Time {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.lastModified
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.lastModified
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
func (rf *rolodexFile) Size() int64 {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.Size()
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.Size()
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (rf *rolodexFile) IsDir() bool {
|
|
return false
|
|
}
|
|
|
|
func (rf *rolodexFile) Sys() interface{} {
|
|
return nil
|
|
}
|
|
|
|
func (rf *rolodexFile) Mode() os.FileMode {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.Mode()
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.Mode()
|
|
}
|
|
return os.FileMode(0)
|
|
}
|
|
|
|
func (rf *rolodexFile) GetErrors() []error {
|
|
if rf.localFile != nil {
|
|
return rf.localFile.readingErrors
|
|
}
|
|
if rf.remoteFile != nil {
|
|
return rf.remoteFile.seekingErrors
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewRolodex(indexConfig *SpecIndexConfig) *Rolodex {
|
|
r := &Rolodex{
|
|
indexConfig: indexConfig,
|
|
localFS: make(map[string]fs.FS),
|
|
remoteFS: make(map[string]fs.FS),
|
|
}
|
|
indexConfig.Rolodex = r
|
|
return r
|
|
}
|
|
|
|
func (r *Rolodex) GetIgnoredCircularReferences() []*CircularReferenceResult {
|
|
return r.ignoredCircularReferences
|
|
}
|
|
|
|
func (r *Rolodex) GetIndexingDuration() time.Duration {
|
|
return r.indexingDuration
|
|
}
|
|
|
|
func (r *Rolodex) GetRootIndex() *SpecIndex {
|
|
return r.rootIndex
|
|
}
|
|
|
|
func (r *Rolodex) GetIndexes() []*SpecIndex {
|
|
return r.indexes
|
|
}
|
|
|
|
func (r *Rolodex) GetCaughtErrors() []error {
|
|
return r.caughtErrors
|
|
}
|
|
|
|
func (r *Rolodex) AddLocalFS(baseDir string, fileSystem fs.FS) {
|
|
absBaseDir, _ := filepath.Abs(baseDir)
|
|
r.localFS[absBaseDir] = fileSystem
|
|
}
|
|
|
|
func (r *Rolodex) SetRootNode(node *yaml.Node) {
|
|
r.rootNode = node
|
|
}
|
|
|
|
func (r *Rolodex) AddRemoteFS(baseURL string, fileSystem fs.FS) {
|
|
r.remoteFS[baseURL] = fileSystem
|
|
}
|
|
|
|
func (r *Rolodex) IndexTheRolodex() error {
|
|
if r.indexed {
|
|
return nil
|
|
}
|
|
|
|
var caughtErrors []error
|
|
|
|
var indexBuildQueue []*SpecIndex
|
|
|
|
indexRolodexFile := func(
|
|
location string, fs fs.FS,
|
|
doneChan chan bool,
|
|
errChan chan error,
|
|
indexChan chan *SpecIndex) {
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
indexFileFunc := func(idxFile CanBeIndexed, fullPath string) {
|
|
defer wg.Done()
|
|
|
|
// copy config and set the
|
|
copiedConfig := *r.indexConfig
|
|
copiedConfig.SpecAbsolutePath = fullPath
|
|
copiedConfig.AvoidBuildIndex = true // we will build out everything in two steps.
|
|
idx, err := idxFile.Index(&copiedConfig)
|
|
|
|
// for each index, we need a resolver
|
|
resolver := NewResolver(idx)
|
|
|
|
// check if the config has been set to ignore circular references in arrays and polymorphic schemas
|
|
if copiedConfig.IgnoreArrayCircularReferences {
|
|
resolver.IgnoreArrayCircularReferences()
|
|
}
|
|
if copiedConfig.IgnorePolymorphicCircularReferences {
|
|
resolver.IgnorePolymorphicCircularReferences()
|
|
}
|
|
|
|
if err != nil {
|
|
errChan <- err
|
|
}
|
|
indexChan <- idx
|
|
}
|
|
|
|
if lfs, ok := fs.(RolodexFS); ok {
|
|
wait := false
|
|
for _, f := range lfs.GetFiles() {
|
|
if idxFile, ko := f.(CanBeIndexed); ko {
|
|
wg.Add(1)
|
|
wait = true
|
|
go indexFileFunc(idxFile, f.GetFullPath())
|
|
}
|
|
}
|
|
if wait {
|
|
wg.Wait()
|
|
}
|
|
doneChan <- true
|
|
return
|
|
} else {
|
|
errChan <- errors.New("rolodex file system is not a RolodexFS")
|
|
doneChan <- true
|
|
}
|
|
}
|
|
|
|
indexingCompleted := 0
|
|
totalToIndex := len(r.localFS) + len(r.remoteFS)
|
|
doneChan := make(chan bool)
|
|
errChan := make(chan error)
|
|
indexChan := make(chan *SpecIndex)
|
|
|
|
// run through every file system and index every file, fan out as many goroutines as possible.
|
|
started := time.Now()
|
|
for k, v := range r.localFS {
|
|
go indexRolodexFile(k, v, doneChan, errChan, indexChan)
|
|
}
|
|
for k, v := range r.remoteFS {
|
|
go indexRolodexFile(k, v, doneChan, errChan, indexChan)
|
|
}
|
|
|
|
for indexingCompleted < totalToIndex {
|
|
select {
|
|
case <-doneChan:
|
|
indexingCompleted++
|
|
case err := <-errChan:
|
|
indexingCompleted++
|
|
caughtErrors = append(caughtErrors, err)
|
|
case idx := <-indexChan:
|
|
indexBuildQueue = append(indexBuildQueue, idx)
|
|
}
|
|
}
|
|
|
|
// now that we have indexed all the files, we can build the index.
|
|
r.indexes = indexBuildQueue
|
|
//if !r.indexConfig.AvoidBuildIndex {
|
|
for _, idx := range indexBuildQueue {
|
|
idx.BuildIndex()
|
|
if r.indexConfig.AvoidCircularReferenceCheck {
|
|
continue
|
|
}
|
|
errs := idx.resolver.CheckForCircularReferences()
|
|
for e := range errs {
|
|
caughtErrors = append(caughtErrors, errs[e])
|
|
}
|
|
}
|
|
|
|
// indexed and built every supporting file, we can build the root index (our entry point)
|
|
if r.rootNode != nil {
|
|
|
|
// if there is a base path, then we need to set the root spec config to point to a theoretical root.yaml
|
|
// which does not exist, but is used to formulate the absolute path to root references correctly.
|
|
if r.indexConfig.BasePath != "" && r.indexConfig.BaseURL == nil {
|
|
|
|
basePath := r.indexConfig.BasePath
|
|
if !filepath.IsAbs(basePath) {
|
|
basePath, _ = filepath.Abs(basePath)
|
|
}
|
|
|
|
if len(r.localFS) > 0 || len(r.remoteFS) > 0 {
|
|
r.indexConfig.SpecAbsolutePath = filepath.Join(basePath, "root.yaml")
|
|
}
|
|
}
|
|
|
|
// todo: variation with no base path, but a base URL.
|
|
|
|
index := NewSpecIndexWithConfig(r.rootNode, r.indexConfig)
|
|
resolver := NewResolver(index)
|
|
|
|
if r.indexConfig.IgnoreArrayCircularReferences {
|
|
resolver.IgnoreArrayCircularReferences()
|
|
}
|
|
if r.indexConfig.IgnorePolymorphicCircularReferences {
|
|
resolver.IgnorePolymorphicCircularReferences()
|
|
}
|
|
|
|
index.BuildIndex()
|
|
|
|
if !r.indexConfig.AvoidCircularReferenceCheck {
|
|
resolvingErrors := resolver.CheckForCircularReferences()
|
|
r.circChecked = true
|
|
for e := range resolvingErrors {
|
|
caughtErrors = append(caughtErrors, resolvingErrors[e])
|
|
}
|
|
}
|
|
r.rootIndex = index
|
|
if len(index.refErrors) > 0 {
|
|
caughtErrors = append(caughtErrors, index.refErrors...)
|
|
}
|
|
}
|
|
r.indexingDuration = time.Since(started)
|
|
r.indexed = true
|
|
r.caughtErrors = caughtErrors
|
|
r.built = true
|
|
return errors.Join(caughtErrors...)
|
|
|
|
}
|
|
|
|
func (r *Rolodex) CheckForCircularReferences() {
|
|
if !r.circChecked {
|
|
if r.rootIndex != nil && r.rootIndex.resolver != nil {
|
|
resolvingErrors := r.rootIndex.resolver.CheckForCircularReferences()
|
|
for e := range resolvingErrors {
|
|
r.caughtErrors = append(r.caughtErrors, resolvingErrors[e])
|
|
}
|
|
if len(r.rootIndex.resolver.ignoredPolyReferences) > 0 {
|
|
r.ignoredCircularReferences = append(r.ignoredCircularReferences, r.rootIndex.resolver.ignoredPolyReferences...)
|
|
}
|
|
if len(r.rootIndex.resolver.ignoredArrayReferences) > 0 {
|
|
r.ignoredCircularReferences = append(r.ignoredCircularReferences, r.rootIndex.resolver.ignoredArrayReferences...)
|
|
}
|
|
}
|
|
r.circChecked = true
|
|
}
|
|
}
|
|
|
|
func (r *Rolodex) Resolve() {
|
|
if r.rootIndex != nil && r.rootIndex.resolver != nil {
|
|
resolvingErrors := r.rootIndex.resolver.Resolve()
|
|
for e := range resolvingErrors {
|
|
r.caughtErrors = append(r.caughtErrors, resolvingErrors[e])
|
|
}
|
|
if len(r.rootIndex.resolver.ignoredPolyReferences) > 0 {
|
|
r.ignoredCircularReferences = append(r.ignoredCircularReferences, r.rootIndex.resolver.ignoredPolyReferences...)
|
|
}
|
|
if len(r.rootIndex.resolver.ignoredArrayReferences) > 0 {
|
|
r.ignoredCircularReferences = append(r.ignoredCircularReferences, r.rootIndex.resolver.ignoredArrayReferences...)
|
|
}
|
|
}
|
|
r.resolved = true
|
|
}
|
|
|
|
func (r *Rolodex) BuildIndexes() {
|
|
if r.built {
|
|
return
|
|
}
|
|
for _, idx := range r.indexes {
|
|
idx.BuildIndex()
|
|
}
|
|
if r.rootIndex != nil {
|
|
r.rootIndex.BuildIndex()
|
|
}
|
|
r.built = true
|
|
}
|
|
|
|
func (r *Rolodex) Open(location string) (RolodexFile, error) {
|
|
|
|
var errorStack []error
|
|
|
|
var localFile *LocalFile
|
|
var remoteFile *RemoteFile
|
|
|
|
if r == nil || r.localFS == nil && r.remoteFS == nil {
|
|
return nil, fmt.Errorf("rolodex has no file systems configured, cannot open '%s'", location)
|
|
}
|
|
|
|
fileLookup := location
|
|
isUrl := false
|
|
u, _ := url.Parse(location)
|
|
if u != nil && u.Scheme != "" {
|
|
isUrl = true
|
|
}
|
|
|
|
if !isUrl {
|
|
|
|
for k, v := range r.localFS {
|
|
|
|
// check if this is a URL or an abs/rel reference.
|
|
|
|
if !filepath.IsAbs(location) {
|
|
fileLookup, _ = filepath.Abs(filepath.Join(k, location))
|
|
}
|
|
|
|
f, err := v.Open(fileLookup)
|
|
if err != nil {
|
|
|
|
// try a lookup that is not absolute, but relative
|
|
f, err = v.Open(location)
|
|
if err != nil {
|
|
errorStack = append(errorStack, err)
|
|
continue
|
|
}
|
|
}
|
|
// check if this is a native rolodex FS, then the work is done.
|
|
if lrf, ok := interface{}(f).(*localRolodexFile); ok {
|
|
|
|
if lf, ko := interface{}(lrf.f).(*LocalFile); ko {
|
|
localFile = lf
|
|
break
|
|
}
|
|
} else {
|
|
// not a native FS, so we need to read the file and create a local file.
|
|
bytes, rErr := io.ReadAll(f)
|
|
if rErr != nil {
|
|
errorStack = append(errorStack, rErr)
|
|
continue
|
|
}
|
|
s, sErr := f.Stat()
|
|
if sErr != nil {
|
|
errorStack = append(errorStack, sErr)
|
|
continue
|
|
}
|
|
if len(bytes) > 0 {
|
|
localFile = &LocalFile{
|
|
filename: filepath.Base(fileLookup),
|
|
name: filepath.Base(fileLookup),
|
|
extension: ExtractFileType(fileLookup),
|
|
data: bytes,
|
|
fullPath: fileLookup,
|
|
lastModified: s.ModTime(),
|
|
index: r.rootIndex,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if localFile == nil {
|
|
|
|
// if there was no file found locally, then search the remote FS.
|
|
for _, v := range r.remoteFS {
|
|
f, err := v.Open(location)
|
|
if err != nil {
|
|
errorStack = append(errorStack, err)
|
|
continue
|
|
}
|
|
if f != nil {
|
|
return f.(*RemoteFile), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
if !r.indexConfig.AllowRemoteLookup {
|
|
return nil, fmt.Errorf("remote lookup for '%s' not allowed, please set the index configuration to "+
|
|
"AllowRemoteLookup to true", fileLookup)
|
|
}
|
|
|
|
for _, v := range r.remoteFS {
|
|
f, err := v.Open(fileLookup)
|
|
if err == nil {
|
|
|
|
if rf, ok := interface{}(f).(*RemoteFile); ok {
|
|
remoteFile = rf
|
|
break
|
|
} else {
|
|
|
|
bytes, rErr := io.ReadAll(f)
|
|
if rErr != nil {
|
|
errorStack = append(errorStack, rErr)
|
|
continue
|
|
}
|
|
s, sErr := f.Stat()
|
|
if sErr != nil {
|
|
errorStack = append(errorStack, sErr)
|
|
continue
|
|
}
|
|
if len(bytes) > 0 {
|
|
remoteFile = &RemoteFile{
|
|
filename: filepath.Base(fileLookup),
|
|
name: filepath.Base(fileLookup),
|
|
extension: ExtractFileType(fileLookup),
|
|
data: bytes,
|
|
fullPath: fileLookup,
|
|
lastModified: s.ModTime(),
|
|
index: r.rootIndex,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if localFile != nil {
|
|
return &rolodexFile{
|
|
rolodex: r,
|
|
location: localFile.fullPath,
|
|
localFile: localFile,
|
|
}, errors.Join(errorStack...)
|
|
}
|
|
|
|
if remoteFile != nil {
|
|
return &rolodexFile{
|
|
rolodex: r,
|
|
location: remoteFile.fullPath,
|
|
remoteFile: remoteFile,
|
|
}, errors.Join(errorStack...)
|
|
}
|
|
|
|
return nil, errors.Join(errorStack...)
|
|
}
|