From 6bac51ac08021729afc4f184547852d64f2c0f86 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 --- .gitignore | 3 +- document.go | 107 +++++++++++++------------- document_examples_test.go | 143 ++++++++++++++++++++--------------- document_test.go | 134 +++++++++++++++++++------------- error.go | 77 +++++++++++++++++++ go.mod | 2 +- index/find_component_test.go | 3 +- 7 files changed, 299 insertions(+), 170 deletions(-) create mode 100644 error.go diff --git a/.gitignore b/.gitignore index 7f91d0d2..6e6d1241 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -test-operation.yaml \ No newline at end of file +test-operation.yaml +.idea/ diff --git a/document.go b/document.go index f58faeea..a1b2b258 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. @@ -155,41 +154,41 @@ func (d *document) Serialize() ([]byte, error) { return utils.ConvertYAMLtoJSON(yamlData) } } +//func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], []error) { +// return nil, nil, nil, nil +//} -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") } newBytes, err := d.highOpenAPI3Model.Model.Render() if err != nil { - return newBytes, nil, nil, []error{err} + return newBytes, nil, nil, wrapErr(err) } 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 @@ -200,16 +199,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) @@ -217,22 +216,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 @@ -243,16 +239,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) @@ -260,7 +256,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 @@ -269,37 +265,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 7c44bb6e..653bddf9 100644 --- a/document_examples_test.go +++ b/document_examples_test.go @@ -4,8 +4,10 @@ package libopenapi import ( + "errors" "fmt" "github.com/pb33f/libopenapi/datamodel" + "github.com/stretchr/testify/require" "io/ioutil" "net/url" "os" @@ -37,14 +39,18 @@ 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) { + for _, e := range errs.Unwrap() { + fmt.Printf("error: %e\n", e) + } + 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. @@ -79,10 +85,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 @@ -116,10 +122,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") @@ -143,14 +149,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. @@ -176,36 +184,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 @@ -237,14 +243,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. @@ -306,14 +314,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. @@ -353,14 +363,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. @@ -427,10 +439,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. @@ -476,12 +494,10 @@ 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() + fmt.Printf("%#v\n", err) + assert.NoError(t, err) } // If you're using complex types with OpenAPI Extensions, it's simple to unpack extensions into complex @@ -619,14 +635,17 @@ 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) { + for _, e := range errs.Unwrap() { + fmt.Printf("error: %e\n", e) + } + 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. @@ -651,11 +670,15 @@ func ExampleNewDocument_modifyAndReRender() { // renderedPathItem, _ := yaml.Marshal(newPath) // render the document back to bytes and reload the model. - rawBytes, _, newModel, errs := doc.RenderAndReload() + rawBytes, _, newModel, err := 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))) + 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'") } // capture new number of paths after re-rendering diff --git a/document_test.go b/document_test.go index 1f305ac5..45ccbb32 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. @@ -585,9 +613,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..3ed7ddb5 --- /dev/null +++ b/error.go @@ -0,0 +1,77 @@ +package libopenapi + +import ( + "errors" + "fmt" + "strings" +) + +func errorMsg(msg string) *MultiError { + return &MultiError{[]error{errors.New(msg)}} +} + +func errorMsgf(msg string, a ...any) *MultiError { + return &MultiError{[]error{fmt.Errorf(msg, a...)}} +} + +func wrapErr(err error) error { + if err != nil { + return nil + } + return &MultiError{[]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) { + 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 i > 0 { + b.WriteByte('\n') + } + b.WriteString(fmt.Sprintf("[%d] %s",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 _, err := range e.errs { + fmt.Printf("error: %e\n", err) + } +} 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/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)