(feat): Upgraded circular error handling experience #27

Thinking more about how to know what to resolve and what not to means depending on circular reference knowledge. So using an idea from @TristanSpeakEasy, the `*resolver.ResolvingError`  now contains a pointer to the original circular error, and it also implements the `Error` interface correctly which allows it simple passage up the chain, without losing fidelity.

Added new documentation example as well

Signed-off-by: Dave Shanley <dave@quobix.com>
This commit is contained in:
Dave Shanley
2022-12-01 11:43:08 -05:00
parent ee93996ff2
commit 52a5b61de2
4 changed files with 85 additions and 20 deletions

View File

@@ -12,7 +12,6 @@
package v2
import (
"fmt"
"github.com/pb33f/libopenapi/datamodel"
"github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/datamodel/low/base"
@@ -146,9 +145,7 @@ func CreateDocument(info *datamodel.SpecInfo) (*Swagger, []error) {
if len(resolvingErrors) > 0 {
for r := range resolvingErrors {
errors = append(errors,
fmt.Errorf("%s (%s) [%d:%d]", resolvingErrors[r].Error.Error(),
resolvingErrors[r].Path, resolvingErrors[r].Node.Line, resolvingErrors[r].Node.Column))
errors = append(errors, resolvingErrors[r])
}
}

View File

@@ -2,7 +2,6 @@ package v3
import (
"errors"
"fmt"
"github.com/pb33f/libopenapi/datamodel"
"github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/datamodel/low/base"
@@ -34,9 +33,7 @@ func CreateDocument(info *datamodel.SpecInfo) (*Document, []error) {
if len(resolvingErrors) > 0 {
for r := range resolvingErrors {
errors = append(errors,
fmt.Errorf("%s: %s [%d:%d]", resolvingErrors[r].Error.Error(),
resolvingErrors[r].Path, resolvingErrors[r].Node.Line, resolvingErrors[r].Node.Column))
errors = append(errors, resolvingErrors[r])
}
}

View File

@@ -5,9 +5,11 @@ package libopenapi
import (
"fmt"
"github.com/pb33f/libopenapi/resolver"
"github.com/pb33f/libopenapi/utils"
"github.com/stretchr/testify/assert"
"io/ioutil"
"strings"
"testing"
)
@@ -477,3 +479,58 @@ func TestDocument_Paths_As_Array(t *testing.T) {
v3Model, _ := doc.BuildV3Model()
assert.NotNil(t, v3Model)
}
// If you want to know more about circular references that have been found
// during the parsing/indexing/building of a document, you can capture the
// []errors thrown which are pointers to *resolver.ResolvingError
func ExampleNewDocument_circular_references() {
// create a specification with an obvious and deliberate circular reference
spec := `openapi: "3.1"
components:
schemas:
One:
description: "test one"
properties:
things:
"$ref": "#/components/schemas/Two"
Two:
description: "test two"
properties:
testThing:
"$ref": "#/components/schemas/One"
`
// create a new document from specification bytes
doc, err := NewDocument([]byte(spec))
// if anything went wrong, an error is thrown
if err != nil {
panic(fmt.Sprintf("cannot create new document: %e", err))
}
_, errs := doc.BuildV3Model()
// extract resolving error
resolvingError := errs[0]
// resolving error is a pointer to *resolver.ResolvingError
// which provides access to rich details about the error.
circularReference := resolvingError.(*resolver.ResolvingError).CircularReference
// capture the journey with all details
var buf strings.Builder
for n := range circularReference.Journey {
// add the full definition name to the journey.
buf.WriteString(circularReference.Journey[n].Definition)
if n < len(circularReference.Journey)-1 {
buf.WriteString(" -> ")
}
}
// print out the journey and the loop point.
fmt.Printf("Journey: %s\n", buf.String())
fmt.Printf("Loop Point: %s", circularReference.LoopPoint.Definition)
// Output: Journey: #/components/schemas/Two -> #/components/schemas/One -> #/components/schemas/Two
// Loop Point: #/components/schemas/Two
}

View File

@@ -12,9 +12,22 @@ import (
// ResolvingError represents an issue the resolver had trying to stitch the tree together.
type ResolvingError struct {
Error error
// ErrorRef is the error thrown by the resolver
ErrorRef error
// Node is the *yaml.Node reference that contains the resolving error
Node *yaml.Node
// Path is the shortened journey taken by the resolver
Path string
// CircularReference is set if the error is a reference to the circular reference.
CircularReference *index.CircularReferenceResult
}
func (r *ResolvingError) Error() string {
return fmt.Sprintf("%s: %s [%d:%d]", r.ErrorRef.Error(),
r.Path, r.Node.Line, r.Node.Column)
}
// Resolver will use a *index.SpecIndex to stitch together a resolved root tree using all the discovered
@@ -106,7 +119,7 @@ func (resolver *Resolver) Resolve() []*ResolvingError {
for _, circRef := range resolver.circularReferences {
resolver.resolvingErrors = append(resolver.resolvingErrors, &ResolvingError{
Error: fmt.Errorf("Circular reference detected: %s", circRef.Start.Name),
ErrorRef: fmt.Errorf("Circular reference detected: %s", circRef.Start.Name),
Node: circRef.LoopPoint.Node,
Path: circRef.GenerateJourneyPath(),
})
@@ -135,9 +148,10 @@ func (resolver *Resolver) CheckForCircularReferences() []*ResolvingError {
}
for _, circRef := range resolver.circularReferences {
resolver.resolvingErrors = append(resolver.resolvingErrors, &ResolvingError{
Error: fmt.Errorf("Circular reference detected: %s", circRef.Start.Name),
ErrorRef: fmt.Errorf("Circular reference detected: %s", circRef.Start.Name),
Node: circRef.LoopPoint.Node,
Path: circRef.GenerateJourneyPath(),
CircularReference: circRef,
})
}
// update our index with any circular refs we found.
@@ -231,7 +245,7 @@ func (resolver *Resolver) extractRelatives(node *yaml.Node,
// 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),
ErrorRef: fmt.Errorf("cannot resolve reference `%s`, it's missing", value),
Node: n,
Path: path,
}