Files
libopenapi/resolver/resolver.go
Dave Shanley 6c2de6c151 The resolver just got an upgrade, polymorphic circular reference checking.
Building out models that have circular references is tricky. Particularly when they are polymorphic ones. The resolver now searches the polymorphic references for circular references as well (which is returning much more valuable results now). The low level model is now sensitive to this and will avoid building out anything that is circular.
2022-08-22 09:24:23 -04:00

264 lines
7.7 KiB
Go

// 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"
)
// 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 []*index.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() []*index.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, true)
}
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, true)
}
}
// 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
}
// CheckForCircularReferences Check for circular references, without resolving.
func (resolver *Resolver) CheckForCircularReferences() []*ResolvingError {
mapped := resolver.specIndex.GetMappedReferencesSequenced()
mappedIndex := resolver.specIndex.GetMappedReferences()
for _, ref := range mapped {
seenReferences := make(map[string]bool)
var journey []*index.Reference
resolver.VisitReference(ref.Reference, seenReferences, journey, false)
}
schemas := resolver.specIndex.GetAllSchemas()
for s, schemaRef := range schemas {
if mappedIndex[s] == nil {
seenReferences := make(map[string]bool)
var journey []*index.Reference
resolver.VisitReference(schemaRef, seenReferences, journey, false)
}
}
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(),
})
}
// update our index with any circular refs we found.
resolver.specIndex.SetCircularReferences(resolver.circularReferences)
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, resolve bool) []*yaml.Node {
if ref.Resolved || ref.Seen {
return ref.Node.Content
}
journey = append(journey, ref)
relatives := resolver.extractRelatives(ref.Node, seen, journey, resolve)
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 *index.CircularReferenceResult
if !foundDup.Circular {
loop := append(journey, foundDup)
circRef = &index.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, resolve)
if resolve {
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, resolve bool) []*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, resolve)...)
}
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" {
// if this is a polymorphic link, we want to follow it and see if it becomes circular
if utils.IsNodeMap(node.Content[i+1]) { // check for nested items
// check if items is present, to indicate an array
if _, v := utils.FindKeyNodeTop("items", node.Content[i+1].Content); v != nil {
if utils.IsNodeMap(v) {
items := resolver.extractRelatives(v, foundRelatives, journey, resolve)
for j := range items {
resolver.VisitReference(items[j], foundRelatives, journey, resolve)
}
}
}
}
// for array based polymorphic items
if utils.IsNodeArray(node.Content[i+1]) { // check for nested items
// check if items is present, to indicate an array
for q := range node.Content[i+1].Content {
v := node.Content[i+1].Content[q]
if utils.IsNodeMap(v) {
items := resolver.extractRelatives(v, foundRelatives, journey, resolve)
for j := range items {
resolver.VisitReference(items[j], foundRelatives, journey, resolve)
}
}
}
}
break
}
}
}
}
return found
}