From 7c8dcb01d787d58bcd625d4324d7f68e7cd42f99 Mon Sep 17 00:00:00 2001 From: "Derrick J. Wippler" Date: Tue, 30 May 2023 15:41:37 -0500 Subject: [PATCH] feat: Public methods now return MultiError instead of []error --- document.go | 102 ++++++++++++------------ document_examples_test.go | 146 ++++++++++++++++++++--------------- document_test.go | 134 +++++++++++++++++++------------- error.go | 85 ++++++++++++++++++++ error_test.go | 57 ++++++++++++++ go.mod | 2 +- go.sum | 4 - index/find_component_test.go | 3 +- 8 files changed, 357 insertions(+), 176 deletions(-) create mode 100644 error.go create mode 100644 error_test.go diff --git a/document.go b/document.go index acdca950..9f908d35 100644 --- a/document.go +++ b/document.go @@ -14,7 +14,6 @@ package libopenapi import ( - "errors" "fmt" "github.com/pb33f/libopenapi/index" @@ -48,13 +47,13 @@ type Document interface { // If there are any issues, then no model will be returned, instead a slice of errors will explain all the // problems that occurred. This method will only support version 2 specifications and will throw an error for // any other types. - BuildV2Model() (*DocumentModel[v2high.Swagger], []error) + BuildV2Model() (*DocumentModel[v2high.Swagger], error) // BuildV3Model will build out an OpenAPI (version 3+) model from the specification used to create the document // If there are any issues, then no model will be returned, instead a slice of errors will explain all the // problems that occurred. This method will only support version 3 specifications and will throw an error for // any other types. - BuildV3Model() (*DocumentModel[v3high.Document], []error) + BuildV3Model() (*DocumentModel[v3high.Document], error) // RenderAndReload will render the high level model as it currently exists (including any mutations, additions // and removals to and from any object in the tree). It will then reload the low level model with the new bytes @@ -70,7 +69,7 @@ type Document interface { // **IMPORTANT** This method only supports OpenAPI Documents. The Swagger model will not support mutations correctly // and will not update when called. This choice has been made because we don't want to continue supporting Swagger, // it's too old, so it should be motivation to upgrade to OpenAPI 3. - RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], []error) + RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], error) // Serialize will re-render a Document back into a []byte slice. If any modifications have been made to the // underlying data model using low level APIs, then those changes will be reflected in the serialized output. @@ -156,9 +155,9 @@ func (d *document) Serialize() ([]byte, error) { } } -func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], []error) { +func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], error) { if d.highSwaggerModel != nil && d.highOpenAPI3Model == nil { - return nil, nil, nil, []error{errors.New("this method only supports OpenAPI 3 documents, not Swagger")} + return nil, nil, nil, errorMsg("this method only supports OpenAPI 3 documents, not Swagger") } var newBytes []byte @@ -181,30 +180,27 @@ func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Do newDoc, err := NewDocumentWithConfiguration(newBytes, d.config) if err != nil { - return newBytes, newDoc, nil, []error{err} + return newBytes, newDoc, nil, wrapErr(err) } // build the model. - model, errs := newDoc.BuildV3Model() - if errs != nil { - return newBytes, newDoc, model, errs + model, err := newDoc.BuildV3Model() + if err != nil { + return newBytes, newDoc, model, wrapErr(err) } // this document is now dead, long live the new document! return newBytes, newDoc, model, nil } -func (d *document) BuildV2Model() (*DocumentModel[v2high.Swagger], []error) { +func (d *document) BuildV2Model() (*DocumentModel[v2high.Swagger], error) { if d.highSwaggerModel != nil { return d.highSwaggerModel, nil } - var errors []error if d.info == nil { - errors = append(errors, fmt.Errorf("unable to build swagger document, no specification has been loaded")) - return nil, errors + return nil, errorMsg("unable to build swagger document, no specification has been loaded") } if d.info.SpecFormat != datamodel.OAS2 { - errors = append(errors, fmt.Errorf("unable to build swagger document, "+ - "supplied spec is a different version (%v). Try 'BuildV3Model()'", d.info.SpecFormat)) - return nil, errors + return nil, errorMsgf("unable to build swagger document, "+ + "supplied spec is a different version (%v). Try 'BuildV3Model()'", d.info.SpecFormat) } var lowDoc *v2low.Swagger @@ -215,16 +211,16 @@ func (d *document) BuildV2Model() (*DocumentModel[v2high.Swagger], []error) { } } - lowDoc, errors = v2low.CreateDocumentFromConfig(d.info, d.config) + lowDoc, errs := v2low.CreateDocumentFromConfig(d.info, d.config) // Do not short-circuit on circular reference errors, so the client // has the option of ignoring them. - for _, err := range errors { + for _, err := range errs { if refErr, ok := err.(*resolver.ResolvingError); ok { if refErr.CircularReference == nil { - return nil, errors + return nil, wrapErrs(errs) } } else { - return nil, errors + return nil, wrapErrs(errs) } } highDoc := v2high.NewSwaggerDocument(lowDoc) @@ -232,22 +228,19 @@ func (d *document) BuildV2Model() (*DocumentModel[v2high.Swagger], []error) { Model: *highDoc, Index: lowDoc.Index, } - return d.highSwaggerModel, errors + return d.highSwaggerModel, wrapErrs(errs) } -func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) { +func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], error) { if d.highOpenAPI3Model != nil { return d.highOpenAPI3Model, nil } - var errors []error if d.info == nil { - errors = append(errors, fmt.Errorf("unable to build document, no specification has been loaded")) - return nil, errors + return nil, errorMsg("unable to build document, no specification has been loaded") } if d.info.SpecFormat != datamodel.OAS3 { - errors = append(errors, fmt.Errorf("unable to build openapi document, "+ - "supplied spec is a different version (%v). Try 'BuildV2Model()'", d.info.SpecFormat)) - return nil, errors + return nil, errorMsgf("unable to build openapi document, "+ + "supplied spec is a different version (%v). Try 'BuildV2Model()'", d.info.SpecFormat) } var lowDoc *v3low.Document @@ -258,16 +251,16 @@ func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) { } } - lowDoc, errors = v3low.CreateDocumentFromConfig(d.info, d.config) + lowDoc, errs := v3low.CreateDocumentFromConfig(d.info, d.config) // Do not short-circuit on circular reference errors, so the client // has the option of ignoring them. - for _, err := range errors { + for _, err := range errs { if refErr, ok := err.(*resolver.ResolvingError); ok { if refErr.CircularReference == nil { - return nil, errors + return nil, wrapErrs(errs) } } else { - return nil, errors + return nil, wrapErrs(errs) } } highDoc := v3high.NewDocument(lowDoc) @@ -275,7 +268,7 @@ func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) { Model: *highDoc, Index: lowDoc.Index, } - return d.highOpenAPI3Model, errors + return d.highOpenAPI3Model, wrapErrs(errs) } // CompareDocuments will accept a left and right Document implementing struct, build a model for the correct @@ -284,37 +277,40 @@ func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) { // If there are any errors when building the models, those errors are returned with a nil pointer for the // model.DocumentChanges. If there are any changes found however between either Document, then a pointer to // model.DocumentChanges is returned containing every single change, broken down, model by model. -func CompareDocuments(original, updated Document) (*model.DocumentChanges, []error) { - var errors []error +func CompareDocuments(original, updated Document) (*model.DocumentChanges, error) { + errs := &MultiError{} if original.GetSpecInfo().SpecType == utils.OpenApi3 && updated.GetSpecInfo().SpecType == utils.OpenApi3 { - v3ModelLeft, errs := original.BuildV3Model() - if len(errs) > 0 { - errors = errs + v3ModelLeft, err := original.BuildV3Model() + if err != nil { + errs.Append(err) } - v3ModelRight, errs := updated.BuildV3Model() - if len(errs) > 0 { - errors = append(errors, errs...) + v3ModelRight, err := updated.BuildV3Model() + if err != nil { + errs.Append(err) } if v3ModelLeft != nil && v3ModelRight != nil { - return what_changed.CompareOpenAPIDocuments(v3ModelLeft.Model.GoLow(), v3ModelRight.Model.GoLow()), errors + return what_changed.CompareOpenAPIDocuments(v3ModelLeft.Model.GoLow(), v3ModelRight.Model.GoLow()), + errs.OrNil() } else { - return nil, errors + return nil, errs.OrNil() } } if original.GetSpecInfo().SpecType == utils.OpenApi2 && updated.GetSpecInfo().SpecType == utils.OpenApi2 { - v2ModelLeft, errs := original.BuildV2Model() - if len(errs) > 0 { - errors = errs + errs := &MultiError{} + v2ModelLeft, err := original.BuildV2Model() + if err != nil { + errs.Append(err) } - v2ModelRight, errs := updated.BuildV2Model() - if len(errs) > 0 { - errors = append(errors, errs...) + v2ModelRight, err := updated.BuildV2Model() + if err != nil { + errs.Append(err) } if v2ModelLeft != nil && v2ModelRight != nil { - return what_changed.CompareSwaggerDocuments(v2ModelLeft.Model.GoLow(), v2ModelRight.Model.GoLow()), errors + return what_changed.CompareSwaggerDocuments(v2ModelLeft.Model.GoLow(), v2ModelRight.Model.GoLow()), + errs.OrNil() } else { - return nil, errors + return nil, errs.OrNil() } } - return nil, []error{fmt.Errorf("unable to compare documents, one or both documents are not of the same version")} + return nil, errorMsg("unable to compare documents, one or both documents are not of the same version") } diff --git a/document_examples_test.go b/document_examples_test.go index 4c3600a2..1ea4b805 100644 --- a/document_examples_test.go +++ b/document_examples_test.go @@ -4,13 +4,14 @@ package libopenapi import ( + "errors" "fmt" - "github.com/pb33f/libopenapi/datamodel" "net/url" "os" "strings" "testing" + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high" v3high "github.com/pb33f/libopenapi/datamodel/high/v3" low "github.com/pb33f/libopenapi/datamodel/low/base" @@ -18,6 +19,7 @@ import ( "github.com/pb33f/libopenapi/resolver" "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func ExampleNewDocument_fromOpenAPI3Document() { @@ -36,14 +38,16 @@ func ExampleNewDocument_fromOpenAPI3Document() { } // because we know this is a v3 spec, we can build a ready to go model from it. - v3Model, errors := document.BuildV3Model() + v3Model, err := document.BuildV3Model() // if anything went wrong when building the v3 model, a slice of errors will be returned - if len(errors) > 0 { - for i := range errors { - fmt.Printf("error: %e\n", errors[i]) + if err != nil { + var errs *MultiError + if errors.As(err, &errs) { + errs.Print() + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", errs.Count())) } - panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + panic("error returned was not of type '*MultiError'") } // get a count of the number of paths and schemas. @@ -78,10 +82,10 @@ func ExampleNewDocument_fromWithDocumentConfigurationFailure() { } // only errors will be thrown, so just capture them and print the number of errors. - _, errors := doc.BuildV3Model() + _, err = doc.BuildV3Model() // if anything went wrong when building the v3 model, a slice of errors will be returned - if len(errors) > 0 { + if err != nil { fmt.Println("Error building Digital Ocean spec errors reported") } // Output: Error building Digital Ocean spec errors reported @@ -115,10 +119,10 @@ func ExampleNewDocument_fromWithDocumentConfigurationSuccess() { } // only errors will be thrown, so just capture them and print the number of errors. - _, errors := doc.BuildV3Model() + _, err = doc.BuildV3Model() // if anything went wrong when building the v3 model, a slice of errors will be returned - if len(errors) > 0 { + if err != nil { fmt.Println("Error building Digital Ocean spec errors reported") } else { fmt.Println("Digital Ocean spec built successfully") @@ -142,14 +146,16 @@ func ExampleNewDocument_fromSwaggerDocument() { } // because we know this is a v2 spec, we can build a ready to go model from it. - v2Model, errors := document.BuildV2Model() + v2Model, err := document.BuildV2Model() // if anything went wrong when building the v3 model, a slice of errors will be returned - if len(errors) > 0 { - for i := range errors { - fmt.Printf("error: %e\n", errors[i]) + if err != nil { + var errs *MultiError + if errors.As(err, &errs) { + errs.Print() + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", errs.Count())) } - panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + panic("error returned was not of type '*MultiError'") } // get a count of the number of paths and schemas. @@ -175,36 +181,34 @@ func ExampleNewDocument_fromUnknownVersion() { } var paths, schemas int - var errors []error + errs := MultiError{} // We don't know which type of document this is, so we can use the spec info to inform us if document.GetSpecInfo().SpecType == utils.OpenApi3 { - v3Model, errs := document.BuildV3Model() - if len(errs) > 0 { - errors = errs + v3Model, err := document.BuildV3Model() + if err != nil { + errs.Append(err) } - if len(errors) <= 0 { + if errs.Count() <= 0 { paths = len(v3Model.Model.Paths.PathItems) schemas = len(v3Model.Model.Components.Schemas) } } if document.GetSpecInfo().SpecType == utils.OpenApi2 { - v2Model, errs := document.BuildV2Model() - if len(errs) > 0 { - errors = errs + v2Model, err := document.BuildV2Model() + if err != nil { + errs.Append(err) } - if len(errors) <= 0 { + if errs.Count() <= 0 { paths = len(v2Model.Model.Paths.PathItems) schemas = len(v2Model.Model.Definitions.Definitions) } } // if anything went wrong when building the model, report errors. - if len(errors) > 0 { - for i := range errors { - fmt.Printf("error: %e\n", errors[i]) - } - panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + if errs.Count() > 0 { + errs.Print() + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", errs.Count())) } // print the number of paths and schemas in the document @@ -236,14 +240,16 @@ info: } // because we know this is a v3 spec, we can build a ready to go model from it. - v3Model, errors := document.BuildV3Model() + v3Model, err := document.BuildV3Model() // if anything went wrong when building the v3 model, a slice of errors will be returned - if len(errors) > 0 { - for i := range errors { - fmt.Printf("error: %e\n", errors[i]) + if err != nil { + var errs *MultiError + if errors.As(err, &errs) { + errs.Print() + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", errs.Count())) } - panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + panic("error returned was not of type '*MultiError'") } // mutate the title, to do this we currently need to drop down to the low-level API. @@ -305,14 +311,16 @@ func ExampleCompareDocuments_openAPI() { } // Compare documents for all changes made - documentChanges, errs := CompareDocuments(originalDoc, updatedDoc) + documentChanges, err := CompareDocuments(originalDoc, updatedDoc) // If anything went wrong when building models for documents. - if len(errs) > 0 { - for i := range errs { - fmt.Printf("error: %e\n", errs[i]) + if err != nil { + var errs *MultiError + if errors.As(err, &errs) { + errs.Print() + panic(fmt.Sprintf("cannot compare documents: %d errors reported", errs.Count())) } - panic(fmt.Sprintf("cannot compare documents: %d errors reported", len(errs))) + panic("error returned was not of type '*MultiError'") } // Extract SchemaChanges from components changes. @@ -352,14 +360,16 @@ func ExampleCompareDocuments_swagger() { } // Compare documents for all changes made - documentChanges, errs := CompareDocuments(originalDoc, updatedDoc) + documentChanges, err := CompareDocuments(originalDoc, updatedDoc) // If anything went wrong when building models for documents. - if len(errs) > 0 { - for i := range errs { - fmt.Printf("error: %e\n", errs[i]) + if err != nil { + var errs *MultiError + if errors.As(err, &errs) { + errs.Print() + panic(fmt.Sprintf("cannot compare documents: %d errors reported", errs.Count())) } - panic(fmt.Sprintf("cannot compare documents: %d errors reported", len(errs))) + panic("error returned was not of type '*MultiError'") } // Extract SchemaChanges from components changes. @@ -426,10 +436,16 @@ components: if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } - _, errs := doc.BuildV3Model() + _, err = doc.BuildV3Model() // extract resolving error - resolvingError := errs[0] + var errs *MultiError + var resolvingError error + if errors.As(err, &errs) { + resolvingError = errs.Unwrap()[0] + } else { + panic("error returned was not of type '*MultiError'") + } // resolving error is a pointer to *resolver.ResolvingError // which provides access to rich details about the error. @@ -475,12 +491,9 @@ components: 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() - - assert.Len(t, errs, 0) + require.NoError(t, err) + _, err = doc.BuildV3Model() + assert.NoError(t, err) } // If you're using complex types with OpenAPI Extensions, it's simple to unpack extensions into complex @@ -618,14 +631,15 @@ func ExampleNewDocument_modifyAndReRender() { } // because we know this is a v3 spec, we can build a ready to go model from it. - v3Model, errors := doc.BuildV3Model() + v3Model, err := doc.BuildV3Model() - // if anything went wrong when building the v3 model, a slice of errors will be returned - if len(errors) > 0 { - for i := range errors { - fmt.Printf("error: %e\n", errors[i]) + if err != nil { + var errs *MultiError + if errors.As(err, &errs) { + errs.Print() + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", errs.Count())) } - panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + panic("error returned was not of type '*MultiError'") } // create a new path item and operation. @@ -647,16 +661,20 @@ func ExampleNewDocument_modifyAndReRender() { v3Model.Model.Paths.PathItems["/new/path"] = newPath // render the document back to bytes and reload the model. - rawBytes, _, newModel, errs := doc.RenderAndReload() - - // if anything went wrong when re-rendering the v3 model, a slice of errors will be returned - if len(errors) > 0 { - panic(fmt.Sprintf("cannot re-render document: %d errors reported", len(errs))) - } + rawBytes, _, newModel, err := doc.RenderAndReload() // capture new number of paths after re-rendering newPaths := len(newModel.Model.Paths.PathItems) + // if anything went wrong when re-rendering the v3 model, a slice of errors will be returned + if err != nil { + var errs *MultiError + if errors.As(err, &errs) { + panic(fmt.Sprintf("cannot re-render document: %d errors reported", errs.Count())) + } + panic("error returned was not of type '*MultiError'") + } + // print the number of paths and schemas in the document fmt.Printf("There were %d original paths. There are now %d paths in the document\n", originalPaths, newPaths) fmt.Printf("The original spec had %d bytes, the new one has %d\n", len(petstore), len(rawBytes)) diff --git a/document_test.go b/document_test.go index f2f66208..43c3bc89 100644 --- a/document_test.go +++ b/document_test.go @@ -3,6 +3,7 @@ package libopenapi import ( + "errors" "fmt" "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high/base" @@ -22,7 +23,7 @@ func TestLoadDocument_Simple_V2(t *testing.T) { assert.Equal(t, "2.0.1", doc.GetVersion()) v2Doc, docErr := doc.BuildV2Model() - assert.Len(t, docErr, 0) + assert.NoError(t, docErr) assert.NotNil(t, v2Doc) assert.NotNil(t, doc.GetSpecInfo()) @@ -37,7 +38,9 @@ func TestLoadDocument_Simple_V2_Error(t *testing.T) { assert.NoError(t, err) v2Doc, docErr := doc.BuildV3Model() - assert.Len(t, docErr, 1) + var errs *MultiError + assert.True(t, errors.As(docErr, &errs)) + assert.Equal(t, errs.Count(), 1) assert.Nil(t, v2Doc) } @@ -51,7 +54,9 @@ definitions: assert.NoError(t, err) v2Doc, docErr := doc.BuildV2Model() - assert.Len(t, docErr, 2) + var errs *MultiError + assert.True(t, errors.As(docErr, &errs)) + assert.Equal(t, errs.Count(), 2) assert.Nil(t, v2Doc) } @@ -62,7 +67,9 @@ func TestLoadDocument_Simple_V3_Error(t *testing.T) { assert.NoError(t, err) v2Doc, docErr := doc.BuildV2Model() - assert.Len(t, docErr, 1) + var errs *MultiError + assert.True(t, errors.As(docErr, &errs)) + assert.Equal(t, errs.Count(), 1) assert.Nil(t, v2Doc) } @@ -70,14 +77,18 @@ func TestLoadDocument_Error_V2NoSpec(t *testing.T) { doc := new(document) // not how this should be instantiated. _, err := doc.BuildV2Model() - assert.Len(t, err, 1) + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 1) } func TestLoadDocument_Error_V3NoSpec(t *testing.T) { doc := new(document) // not how this should be instantiated. _, err := doc.BuildV3Model() - assert.Len(t, err, 1) + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 1) } func TestLoadDocument_Empty(t *testing.T) { @@ -94,7 +105,7 @@ func TestLoadDocument_Simple_V3(t *testing.T) { assert.Equal(t, "3.0.1", doc.GetVersion()) v3Doc, docErr := doc.BuildV3Model() - assert.Len(t, docErr, 0) + assert.NoError(t, docErr) assert.NotNil(t, v3Doc) } @@ -108,7 +119,9 @@ paths: assert.NoError(t, err) v3Doc, docErr := doc.BuildV3Model() - assert.Len(t, docErr, 2) + var errs *MultiError + assert.True(t, errors.As(docErr, &errs)) + assert.Equal(t, errs.Count(), 2) assert.Nil(t, v3Doc) } @@ -161,10 +174,10 @@ func TestDocument_RenderAndReload_ChangeCheck_Burgershop(t *testing.T) { rend, newDoc, _, _ := doc.RenderAndReload() // compare documents - compReport, errs := CompareDocuments(doc, newDoc) + compReport, err := CompareDocuments(doc, newDoc) - // should noth be nil. - assert.Nil(t, errs) + // should both be nil. + assert.NoError(t, err) assert.NotNil(t, rend) assert.Nil(t, compReport) @@ -179,7 +192,7 @@ func TestDocument_RenderAndReload_ChangeCheck_Stripe(t *testing.T) { _, newDoc, _, _ := doc.RenderAndReload() // compare documents - compReport, errs := CompareDocuments(doc, newDoc) + compReport, err := CompareDocuments(doc, newDoc) // get flat list of changes. flatChanges := compReport.GetAllChanges() @@ -192,7 +205,7 @@ func TestDocument_RenderAndReload_ChangeCheck_Stripe(t *testing.T) { } } - assert.Nil(t, errs) + assert.NoError(t, err) tc := compReport.TotalChanges() bc := compReport.TotalBreakingChanges() assert.Equal(t, 0, bc) @@ -213,12 +226,12 @@ func TestDocument_RenderAndReload_ChangeCheck_Asana(t *testing.T) { assert.NotNil(t, dat) // compare documents - compReport, errs := CompareDocuments(doc, newDoc) + compReport, err := CompareDocuments(doc, newDoc) // get flat list of changes. flatChanges := compReport.GetAllChanges() - assert.Nil(t, errs) + assert.Nil(t, err) tc := compReport.TotalChanges() assert.Equal(t, 21, tc) @@ -283,9 +296,11 @@ func TestDocument_RenderAndReload_Swagger(t *testing.T) { doc, _ := NewDocument(petstore) doc.BuildV2Model() doc.BuildV2Model() - _, _, _, e := doc.RenderAndReload() - assert.Len(t, e, 1) - assert.Equal(t, "this method only supports OpenAPI 3 documents, not Swagger", e[0].Error()) + _, _, _, err := doc.RenderAndReload() + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 1) + assert.Equal(t, "this method only supports OpenAPI 3 documents, not Swagger", errs.Unwrap()[0].Error()) } @@ -294,24 +309,28 @@ func TestDocument_BuildModelPreBuild(t *testing.T) { doc, _ := NewDocument(petstore) doc.BuildV3Model() doc.BuildV3Model() - _, _, _, e := doc.RenderAndReload() - assert.Len(t, e, 0) + _, _, _, err := doc.RenderAndReload() + assert.NoError(t, err) } func TestDocument_BuildModelCircular(t *testing.T) { petstore, _ := ioutil.ReadFile("test_specs/circular-tests.yaml") doc, _ := NewDocument(petstore) - m, e := doc.BuildV3Model() + m, err := doc.BuildV3Model() assert.NotNil(t, m) - assert.Len(t, e, 3) + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 3) } func TestDocument_BuildModelBad(t *testing.T) { petstore, _ := ioutil.ReadFile("test_specs/badref-burgershop.openapi.yaml") doc, _ := NewDocument(petstore) - m, e := doc.BuildV3Model() + m, err := doc.BuildV3Model() assert.Nil(t, m) - assert.Len(t, e, 9) + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 9) } func TestDocument_Serialize_JSON_Modified(t *testing.T) { @@ -357,9 +376,9 @@ paths: panic(err) } - result, errs := doc.BuildV3Model() - if len(errs) > 0 { - panic(errs) + result, err := doc.BuildV3Model() + if err != nil { + panic(err) } // extract operation. @@ -376,8 +395,10 @@ func TestDocument_BuildModel_CompareDocsV3_LeftError(t *testing.T) { burgerShopUpdated, _ := ioutil.ReadFile("test_specs/burgershop.openapi-modified.yaml") originalDoc, _ := NewDocument(burgerShopOriginal) updatedDoc, _ := NewDocument(burgerShopUpdated) - changes, errors := CompareDocuments(originalDoc, updatedDoc) - assert.Len(t, errors, 9) + changes, err := CompareDocuments(originalDoc, updatedDoc) + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 9) assert.Nil(t, changes) } @@ -387,8 +408,10 @@ func TestDocument_BuildModel_CompareDocsV3_RightError(t *testing.T) { burgerShopUpdated, _ := ioutil.ReadFile("test_specs/burgershop.openapi-modified.yaml") originalDoc, _ := NewDocument(burgerShopOriginal) updatedDoc, _ := NewDocument(burgerShopUpdated) - changes, errors := CompareDocuments(updatedDoc, originalDoc) - assert.Len(t, errors, 9) + changes, err := CompareDocuments(updatedDoc, originalDoc) + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 9) assert.Nil(t, changes) } @@ -399,8 +422,10 @@ func TestDocument_BuildModel_CompareDocsV2_Error(t *testing.T) { burgerShopUpdated, _ := ioutil.ReadFile("test_specs/petstorev2-badref.json") originalDoc, _ := NewDocument(burgerShopOriginal) updatedDoc, _ := NewDocument(burgerShopUpdated) - changes, errors := CompareDocuments(updatedDoc, originalDoc) - assert.Len(t, errors, 2) + changes, err := CompareDocuments(updatedDoc, originalDoc) + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 2) assert.Nil(t, changes) } @@ -411,8 +436,10 @@ func TestDocument_BuildModel_CompareDocsV2V3Mix_Error(t *testing.T) { burgerShopUpdated, _ := ioutil.ReadFile("test_specs/petstorev3.json") originalDoc, _ := NewDocument(burgerShopOriginal) updatedDoc, _ := NewDocument(burgerShopUpdated) - changes, errors := CompareDocuments(updatedDoc, originalDoc) - assert.Len(t, errors, 1) + changes, err := CompareDocuments(updatedDoc, originalDoc) + var errs *MultiError + assert.True(t, errors.As(err, &errs)) + assert.Equal(t, errs.Count(), 1) assert.Nil(t, changes) } @@ -429,14 +456,15 @@ func TestSchemaRefIsFollowed(t *testing.T) { } // because we know this is a v3 spec, we can build a ready to go model from it. - v3Model, errors := document.BuildV3Model() + v3Model, err := document.BuildV3Model() - // if anything went wrong when building the v3 model, a slice of errors will be returned - if len(errors) > 0 { - for i := range errors { - fmt.Printf("error: %e\n", errors[i]) + // if anything went wrong when building the v3 model, a MultiError will be returned + if err != nil { + var errs *MultiError + if errors.As(err, &errs) { + errs.Print() } - panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", errs.Count())) } // get a count of the number of paths and schemas. @@ -494,9 +522,9 @@ paths: panic(err) } - result, errs := doc.BuildV3Model() - if len(errs) > 0 { - panic(errs) + result, err := doc.BuildV3Model() + if err != nil { + panic(err) } // render the document. @@ -525,9 +553,9 @@ paths: panic(err) } - result, errs := doc.BuildV3Model() - if len(errs) > 0 { - panic(errs) + result, err := doc.BuildV3Model() + if err != nil { + panic(err) } assert.Equal(t, "crs", result.Model.Paths.PathItems["/test"].Get.Parameters[0].Name) @@ -555,9 +583,9 @@ components: panic(err) } - result, errs := doc.BuildV3Model() - if len(errs) > 0 { - panic(errs) + result, err := doc.BuildV3Model() + if err != nil { + panic(err) } // render the document. @@ -586,9 +614,9 @@ paths: panic(err) } - result, errs := doc.BuildV3Model() - if len(errs) > 0 { - panic(errs) + result, err := doc.BuildV3Model() + if err != nil { + panic(err) } // render the document. diff --git a/error.go b/error.go new file mode 100644 index 00000000..f8cfbd82 --- /dev/null +++ b/error.go @@ -0,0 +1,85 @@ +// Copyright 2023 Princess B33f Heavy Industries +// SPDX-License-Identifier: MIT + +package libopenapi + +import ( + "errors" + "fmt" + "strings" +) + +func errorMsg(msg string) *MultiError { + return &MultiError{errs: []error{errors.New(msg)}} +} + +func errorMsgf(msg string, a ...any) *MultiError { + return &MultiError{errs: []error{fmt.Errorf(msg, a...)}} +} + +func wrapErr(err error) error { + if err == nil { + return nil + } + return &MultiError{errs: []error{err}} +} + +func wrapErrs(err []error) error { + if len(err) == 0 { + return nil + } + return &MultiError{err} +} + +type MultiError struct { + errs []error +} + +func (e *MultiError) Append(err error) { + if err == nil { + return + } + + var m *MultiError + if errors.As(err, &m) { + e.errs = append(e.errs, m.errs...) + return + } + e.errs = append(e.errs, err) +} + +func (e *MultiError) Count() int { + return len(e.errs) +} + +func (e *MultiError) Error() string { + var b strings.Builder + for i, err := range e.errs { + if err == nil { + b.WriteString(fmt.Sprintf("[%d] nil\n", i)) + continue + } + b.WriteString(fmt.Sprintf("[%d] %s\n", i, err.Error())) + } + return b.String() +} + +func (e *MultiError) Unwrap() []error { + return e.errs +} + +// OrNil returns this instance of *MultiError or nil if there are no errors +// This is useful because returning a &MultiError{} even if it's empty is +// still considered an error. +func (e *MultiError) OrNil() error { + if len(e.errs) == 0 { + return nil + } + return e +} + +func (e *MultiError) Print() { + for i, err := range e.errs { + fmt.Printf("[%d] %s\n", i, err.Error()) + } +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 00000000..50d5c636 --- /dev/null +++ b/error_test.go @@ -0,0 +1,57 @@ +// Copyright 2023 Princess B33f Heavy Industries +// SPDX-License-Identifier: MIT + +package libopenapi + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMultiError(t *testing.T) { + err := &MultiError{} + err.Append(errors.New("error 1")) + err.Append(errors.New("error 2")) + err.Append(wrapErr(errors.New("error 3"))) + assert.Equal(t, "[0] error 1\n[1] error 2\n[2] error 3\n", err.Error()) +} + +func TestMultiError_OrNil(t *testing.T) { + err := &MultiError{} + err.Append(errors.New("error 1")) + err.Append(errors.New("error 2")) + + // Append does not add nil errors + nilErr := &MultiError{} + err.Append(wrapErr(nilErr.OrNil())) + + assert.Equal(t, "[0] error 1\n[1] error 2\n", err.Error()) +} + +func TestMultiError_NilError(t *testing.T) { + // When nil error added to the list. + err := &MultiError{errs: []error{ + errors.New("error 1"), + nil, + errors.New("error 2"), + }} + + // Should output as 'nil' + assert.Equal(t, "[0] error 1\n[1] nil\n[2] error 2\n", err.Error()) +} + +func ExampleMultiError_Print() { + err := &MultiError{} + err.Append(errors.New("error 1")) + err.Append(errors.New("error 2")) + err.Append(errors.New("error 3")) + + err.Print() + + // Output: + // [0] error 1 + // [1] error 2 + // [2] error 3 +} diff --git a/go.mod b/go.mod index ae7eecff..ba8ef189 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/pb33f/libopenapi -go 1.18 +go 1.20 require ( github.com/stretchr/testify v1.8.0 diff --git a/go.sum b/go.sum index 026d1375..67087946 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -80,8 +78,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/index/find_component_test.go b/index/find_component_test.go index 048b1041..1d40047c 100644 --- a/index/find_component_test.go +++ b/index/find_component_test.go @@ -5,6 +5,7 @@ package index import ( "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" "os" "testing" @@ -137,7 +138,7 @@ paths: // extract crs param from index crsParam := index.GetMappedReferences()["https://schemas.opengis.net/ogcapi/features/part2/1.0/openapi/ogcapi-features-2.yaml#/components/parameters/crs"] - assert.NotNil(t, crsParam) + require.NotNil(t, crsParam) assert.True(t, crsParam.IsRemote) assert.Equal(t, "crs", crsParam.Node.Content[1].Value) assert.Equal(t, "query", crsParam.Node.Content[3].Value)