mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-09 12:37:49 +00:00
Added resolver, models and model utils.
This commit is contained in:
229
resolver/resolver.go
Normal file
229
resolver/resolver.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright 2022 Dave Shanley / Quobix
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pb33f/libopenapi/index"
|
||||
"github.com/pb33f/libopenapi/utils"
|
||||
"gopkg.in/yaml.v3"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CircularReferenceResult contains a circular reference found when traversing the graph.
|
||||
type CircularReferenceResult struct {
|
||||
Journey []*index.Reference
|
||||
Start *index.Reference
|
||||
LoopIndex int
|
||||
LoopPoint *index.Reference
|
||||
}
|
||||
|
||||
func (c *CircularReferenceResult) GenerateJourneyPath() string {
|
||||
buf := strings.Builder{}
|
||||
for i, ref := range c.Journey {
|
||||
buf.WriteString(ref.Name)
|
||||
if i+1 < len(c.Journey) {
|
||||
buf.WriteString(" -> ")
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ResolvingError represents an issue the resolver had trying to stitch the tree together.
|
||||
type ResolvingError struct {
|
||||
Error error
|
||||
Node *yaml.Node
|
||||
Path string
|
||||
}
|
||||
|
||||
// Resolver will use a *index.SpecIndex to stitch together a resolved root tree using all the discovered
|
||||
// references in the doc.
|
||||
type Resolver struct {
|
||||
specIndex *index.SpecIndex
|
||||
resolvedRoot *yaml.Node
|
||||
resolvingErrors []*ResolvingError
|
||||
circularReferences []*CircularReferenceResult
|
||||
}
|
||||
|
||||
// NewResolver will create a new resolver from a *index.SpecIndex
|
||||
func NewResolver(index *index.SpecIndex) *Resolver {
|
||||
if index == nil {
|
||||
return nil
|
||||
}
|
||||
return &Resolver{
|
||||
specIndex: index,
|
||||
resolvedRoot: index.GetRootNode(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetResolvingErrors returns all errors found during resolving
|
||||
func (resolver *Resolver) GetResolvingErrors() []*ResolvingError {
|
||||
return resolver.resolvingErrors
|
||||
}
|
||||
|
||||
// GetCircularErrors returns all errors found during resolving
|
||||
func (resolver *Resolver) GetCircularErrors() []*CircularReferenceResult {
|
||||
return resolver.circularReferences
|
||||
}
|
||||
|
||||
// Resolve will resolve the specification, everything that is not polymorphic and not circular, will be resolved.
|
||||
// this data can get big, it results in a massive duplication of data.
|
||||
func (resolver *Resolver) Resolve() []*ResolvingError {
|
||||
|
||||
mapped := resolver.specIndex.GetMappedReferencesSequenced()
|
||||
mappedIndex := resolver.specIndex.GetMappedReferences()
|
||||
|
||||
for _, ref := range mapped {
|
||||
seenReferences := make(map[string]bool)
|
||||
var journey []*index.Reference
|
||||
ref.Reference.Node.Content = resolver.VisitReference(ref.Reference, seenReferences, journey)
|
||||
}
|
||||
|
||||
schemas := resolver.specIndex.GetAllSchemas()
|
||||
|
||||
for s, schemaRef := range schemas {
|
||||
if mappedIndex[s] == nil {
|
||||
seenReferences := make(map[string]bool)
|
||||
var journey []*index.Reference
|
||||
schemaRef.Node.Content = resolver.VisitReference(schemaRef, seenReferences, journey)
|
||||
}
|
||||
}
|
||||
|
||||
// map everything
|
||||
for _, sequenced := range resolver.specIndex.GetAllSequencedReferences() {
|
||||
locatedDef := mappedIndex[sequenced.Definition]
|
||||
if locatedDef != nil {
|
||||
if !locatedDef.Circular && locatedDef.Seen {
|
||||
sequenced.Node.Content = locatedDef.Node.Content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, circRef := range resolver.circularReferences {
|
||||
resolver.resolvingErrors = append(resolver.resolvingErrors, &ResolvingError{
|
||||
Error: fmt.Errorf("Circular reference detected: %s", circRef.Start.Name),
|
||||
Node: circRef.LoopPoint.Node,
|
||||
Path: circRef.GenerateJourneyPath(),
|
||||
})
|
||||
}
|
||||
|
||||
return resolver.resolvingErrors
|
||||
}
|
||||
|
||||
// VisitReference will visit a reference as part of a journey and will return resolved nodes.
|
||||
func (resolver *Resolver) VisitReference(ref *index.Reference, seen map[string]bool, journey []*index.Reference) []*yaml.Node {
|
||||
|
||||
if ref.Resolved || ref.Seen {
|
||||
return ref.Node.Content
|
||||
}
|
||||
|
||||
journey = append(journey, ref)
|
||||
relatives := resolver.extractRelatives(ref.Node, seen, journey)
|
||||
|
||||
seen = make(map[string]bool)
|
||||
|
||||
seen[ref.Definition] = true
|
||||
for _, r := range relatives {
|
||||
|
||||
// check if we have seen this on the journey before, if so! it's circular
|
||||
skip := false
|
||||
for i, j := range journey {
|
||||
if j.Definition == r.Definition {
|
||||
|
||||
foundDup := resolver.specIndex.GetMappedReferences()[r.Definition]
|
||||
|
||||
var circRef *CircularReferenceResult
|
||||
if !foundDup.Circular {
|
||||
|
||||
loop := append(journey, foundDup)
|
||||
circRef = &CircularReferenceResult{
|
||||
Journey: loop,
|
||||
Start: foundDup,
|
||||
LoopIndex: i,
|
||||
LoopPoint: foundDup,
|
||||
}
|
||||
|
||||
foundDup.Seen = true
|
||||
foundDup.Circular = true
|
||||
resolver.circularReferences = append(resolver.circularReferences, circRef)
|
||||
|
||||
}
|
||||
skip = true
|
||||
|
||||
}
|
||||
}
|
||||
if !skip {
|
||||
original := resolver.specIndex.GetMappedReferences()[r.Definition]
|
||||
resolved := resolver.VisitReference(original, seen, journey)
|
||||
r.Node.Content = resolved // this is where we perform the actual resolving.
|
||||
r.Seen = true
|
||||
ref.Seen = true
|
||||
}
|
||||
}
|
||||
ref.Resolved = true
|
||||
ref.Seen = true
|
||||
|
||||
return ref.Node.Content
|
||||
}
|
||||
|
||||
func (resolver *Resolver) extractRelatives(node *yaml.Node,
|
||||
foundRelatives map[string]bool,
|
||||
journey []*index.Reference) []*index.Reference {
|
||||
|
||||
var found []*index.Reference
|
||||
if len(node.Content) > 0 {
|
||||
for i, n := range node.Content {
|
||||
if utils.IsNodeMap(n) || utils.IsNodeArray(n) {
|
||||
found = append(found, resolver.extractRelatives(n, foundRelatives, journey)...)
|
||||
}
|
||||
|
||||
if i%2 == 0 && n.Value == "$ref" {
|
||||
|
||||
if !utils.IsNodeStringValue(node.Content[i+1]) {
|
||||
continue
|
||||
}
|
||||
|
||||
value := node.Content[i+1].Value
|
||||
ref := resolver.specIndex.GetMappedReferences()[value]
|
||||
|
||||
if ref == nil {
|
||||
// TODO handle error, missing ref, can't resolve.
|
||||
_, path := utils.ConvertComponentIdIntoFriendlyPathSearch(value)
|
||||
err := &ResolvingError{
|
||||
Error: fmt.Errorf("cannot resolve reference `%s`, it's missing", value),
|
||||
Node: n,
|
||||
Path: path,
|
||||
}
|
||||
resolver.resolvingErrors = append(resolver.resolvingErrors, err)
|
||||
continue
|
||||
}
|
||||
|
||||
r := &index.Reference{
|
||||
Definition: value,
|
||||
Name: value,
|
||||
Node: node,
|
||||
}
|
||||
|
||||
found = append(found, r)
|
||||
|
||||
foundRelatives[value] = true
|
||||
}
|
||||
|
||||
if i%2 == 0 && n.Value != "$ref" && n.Value != "" {
|
||||
|
||||
if n.Value == "allOf" ||
|
||||
n.Value == "oneOf" ||
|
||||
n.Value == "anyOf" {
|
||||
|
||||
// TODO: track this.
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
Reference in New Issue
Block a user