diff --git a/document.go b/document.go index 28f297d..edd380f 100644 --- a/document.go +++ b/document.go @@ -1,7 +1,17 @@ // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT -package main +// Package libopenapi is a library containing tools for reading and in and manipulating Swagger (OpenAPI 2) and OpenAPI 3+ +// specifications into strongly typed documents. These documents have two APIs, a high level (porcelain) and a +// low level (plumbing). +// +// Every single type has a 'GoLow()' method that drops down from the high API to the low API. Once in the low API, +// the entire original document data is available, including all comments, line and column numbers for keys and values. +// +// There are two steps to creating a using Document. First, create a new Document using the NewDocument() method +// and pass in a specification []byte array that contains the OpenAPI Specification. It doesn't matter if YAML or JSON +// are used. +package libopenapi import ( "fmt" @@ -14,35 +24,74 @@ import ( "gopkg.in/yaml.v3" ) -type Document struct { +// Document Represents an OpenAPI specification that can then be rendered into a model or serialized back into +// a string document after being manipulated. +type Document interface { + + // GetVersion will return the exact version of the OpenAPI specification set for the document. + GetVersion() string + + // GetSpecInfo will return the *datamodel.SpecInfo instance that contains all specification information. + GetSpecInfo() *datamodel.SpecInfo + + // BuildV2Model will build out a Swagger (version 2) 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 2 specifications and will throw an error for + // any other types. + 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) + + // 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. + // + // It's important to know that this should not be used if the resolver has been used on a specification to + // for anything other than checking for circular references. If the resolver is used to resolve the spec, then this + // method may spin out forever if the specification backing the model has circular references. + Serialize() ([]byte, error) +} + +type document struct { version string info *datamodel.SpecInfo } +// DocumentModel represents either a Swagger document (version 2) or an OpenAPI document (version 3) that is +// built from a parent Document. type DocumentModel[T v2high.Swagger | v3high.Document] struct { Model T } -func NewDocument(specByteArray []byte) (*Document, error) { +// NewDocument will create a new OpenAPI instance from an OpenAPI specification []byte array. If anything goes +// wrong when parsing, reading or processing the OpenAPI specification, there will be no document returned, instead +// a slice of errors will be returned that explain everything that failed. +// +// After creating a Document, the option to build a model becomes available, in either V2 or V3 flavors. The models +// are about 70% different between Swagger and OpenAPI 3, which is why two different models are available. +func NewDocument(specByteArray []byte) (Document, error) { info, err := datamodel.ExtractSpecInfo(specByteArray) if err != nil { return nil, err } - d := new(Document) + d := new(document) d.version = info.Version d.info = info return d, nil } -func (d *Document) GetVersion() string { +func (d *document) GetVersion() string { return d.version } -func (d *Document) GetSpecInfo() *datamodel.SpecInfo { +func (d *document) GetSpecInfo() *datamodel.SpecInfo { return d.info } -func (d *Document) Serialize() ([]byte, error) { +func (d *document) Serialize() ([]byte, error) { if d.info == nil { return nil, fmt.Errorf("unable to serialize, document has not yet been initialized") } @@ -54,7 +103,7 @@ func (d *Document) Serialize() ([]byte, error) { } } -func (d *Document) BuildV2Document() (*DocumentModel[v2high.Swagger], []error) { +func (d *document) BuildV2Model() (*DocumentModel[v2high.Swagger], []error) { var errors []error if d.info == nil { errors = append(errors, fmt.Errorf("unable to build swagger document, no specification has been loaded")) @@ -62,7 +111,7 @@ func (d *Document) BuildV2Document() (*DocumentModel[v2high.Swagger], []error) { } if d.info.SpecFormat != datamodel.OAS2 { errors = append(errors, fmt.Errorf("unable to build swagger document, "+ - "supplied spec is a different version (%v). Try 'BuildV3Document()'", d.info.SpecFormat)) + "supplied spec is a different version (%v). Try 'BuildV3Model()'", d.info.SpecFormat)) return nil, errors } lowDoc, err := v2low.CreateDocument(d.info) @@ -75,7 +124,7 @@ func (d *Document) BuildV2Document() (*DocumentModel[v2high.Swagger], []error) { }, nil } -func (d *Document) BuildV3Document() (*DocumentModel[v3high.Document], []error) { +func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) { var errors []error if d.info == nil { errors = append(errors, fmt.Errorf("unable to build document, no specification has been loaded")) @@ -83,7 +132,7 @@ func (d *Document) BuildV3Document() (*DocumentModel[v3high.Document], []error) } if d.info.SpecFormat != datamodel.OAS3 { errors = append(errors, fmt.Errorf("unable to build openapi document, "+ - "supplied spec is a different version (%v). Try 'BuildV2Document()'", d.info.SpecFormat)) + "supplied spec is a different version (%v). Try 'BuildV2Model()'", d.info.SpecFormat)) return nil, errors } lowDoc, err := v3low.CreateDocument(d.info) diff --git a/document_test.go b/document_test.go index c8e78a3..e8bbd0a 100644 --- a/document_test.go +++ b/document_test.go @@ -1,11 +1,13 @@ // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT -package main +package libopenapi import ( "fmt" + "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" + "io/ioutil" "testing" ) @@ -16,7 +18,7 @@ func TestLoadDocument_Simple_V2(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "2.0.1", doc.GetVersion()) - v2Doc, docErr := doc.BuildV2Document() + v2Doc, docErr := doc.BuildV2Model() assert.Len(t, docErr, 0) assert.NotNil(t, v2Doc) assert.NotNil(t, doc.GetSpecInfo()) @@ -31,7 +33,7 @@ func TestLoadDocument_Simple_V2_Error(t *testing.T) { doc, err := NewDocument([]byte(yml)) assert.NoError(t, err) - v2Doc, docErr := doc.BuildV3Document() + v2Doc, docErr := doc.BuildV3Model() assert.Len(t, docErr, 1) assert.Nil(t, v2Doc) } @@ -45,7 +47,7 @@ definitions: doc, err := NewDocument([]byte(yml)) assert.NoError(t, err) - v2Doc, docErr := doc.BuildV2Document() + v2Doc, docErr := doc.BuildV2Model() assert.Len(t, docErr, 1) assert.Nil(t, v2Doc) } @@ -56,22 +58,22 @@ func TestLoadDocument_Simple_V3_Error(t *testing.T) { doc, err := NewDocument([]byte(yml)) assert.NoError(t, err) - v2Doc, docErr := doc.BuildV2Document() + v2Doc, docErr := doc.BuildV2Model() assert.Len(t, docErr, 1) assert.Nil(t, v2Doc) } func TestLoadDocument_Error_V2NoSpec(t *testing.T) { - doc := new(Document) // not how this should be instantiated. - _, err := doc.BuildV2Document() + doc := new(document) // not how this should be instantiated. + _, err := doc.BuildV2Model() assert.Len(t, err, 1) } func TestLoadDocument_Error_V3NoSpec(t *testing.T) { - doc := new(Document) // not how this should be instantiated. - _, err := doc.BuildV3Document() + doc := new(document) // not how this should be instantiated. + _, err := doc.BuildV3Model() assert.Len(t, err, 1) } @@ -88,7 +90,7 @@ func TestLoadDocument_Simple_V3(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "3.0.1", doc.GetVersion()) - v3Doc, docErr := doc.BuildV3Document() + v3Doc, docErr := doc.BuildV3Model() assert.Len(t, docErr, 0) assert.NotNil(t, v3Doc) } @@ -102,13 +104,13 @@ paths: doc, err := NewDocument([]byte(yml)) assert.NoError(t, err) - v3Doc, docErr := doc.BuildV3Document() + v3Doc, docErr := doc.BuildV3Model() assert.Len(t, docErr, 1) assert.Nil(t, v3Doc) } func TestDocument_Serialize_Error(t *testing.T) { - doc := new(Document) // not how this should be instantiated. + doc := new(document) // not how this should be instantiated. _, err := doc.Serialize() assert.Error(t, err) } @@ -138,7 +140,7 @@ info: ` doc, _ := NewDocument([]byte(yml)) - v3Doc, _ := doc.BuildV3Document() + v3Doc, _ := doc.BuildV3Model() v3Doc.Model.Info.GoLow().Title.Mutate("The magic API - but now, altered!") @@ -158,7 +160,7 @@ func TestDocument_Serialize_JSON_Modified(t *testing.T) { jsonModified := `{"info":{"title":"The magic API - but now, altered!"},"openapi":"3.0"}` doc, _ := NewDocument([]byte(json)) - v3Doc, _ := doc.BuildV3Document() + v3Doc, _ := doc.BuildV3Model() // eventually this will be encapsulated up high. // mutation does not replace low model, eventually pointers will be used. @@ -171,3 +173,120 @@ func TestDocument_Serialize_JSON_Modified(t *testing.T) { assert.NoError(t, err) assert.Equal(t, jsonModified, string(serial)) } + +func ExampleNewDocument_fromOpenAPI3Document() { + + // load an OpenAPI 3 specification from bytes + petstore, _ := ioutil.ReadFile("test_specs/petstorev3.json") + + // create a new document from specification bytes + document, err := NewDocument(petstore) + + // if anything went wrong, an error is thrown + if err != nil { + panic(fmt.Sprintf("cannot create new document: %e", err)) + } + + // because we know this is a v3 spec, we can build a ready to go model from it. + v3Model, errors := 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]) + } + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + } + + // get a count of the number of paths and schemas. + paths := len(v3Model.Model.Paths.PathItems) + schemas := len(v3Model.Model.Components.Schemas) + + // print the number of paths and schemas in the document + fmt.Printf("There are %d paths and %d schemas in the document", paths, schemas) + // Output: There are 13 paths and 8 schemas in the document +} + +func ExampleNewDocument_fromSwaggerDocument() { + + // load a Swagger specification from bytes + petstore, _ := ioutil.ReadFile("test_specs/petstorev2.json") + + // create a new document from specification bytes + document, err := NewDocument(petstore) + + // if anything went wrong, an error is thrown + if err != nil { + panic(fmt.Sprintf("cannot create new document: %e", err)) + } + + // because we know this is a v2 spec, we can build a ready to go model from it. + v2Model, errors := 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]) + } + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + } + + // get a count of the number of paths and schemas. + paths := len(v2Model.Model.Paths.PathItems) + schemas := len(v2Model.Model.Definitions.Definitions) + + // print the number of paths and schemas in the document + fmt.Printf("There are %d paths and %d schemas in the document", paths, schemas) + // Output: There are 14 paths and 6 schemas in the document +} + +func ExampleNewDocument_fromUnknownVersion() { + + // load an unknown version of an OpenAPI spec + petstore, _ := ioutil.ReadFile("test_specs/burgershop.openapi.yaml") + + // create a new document from specification bytes + document, err := NewDocument(petstore) + + // if anything went wrong, an error is thrown + if err != nil { + panic(fmt.Sprintf("cannot create new document: %e", err)) + } + + var paths, schemas int + var errors []error + + // 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 + } + if len(errors) <= 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 + } + if len(errors) <= 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))) + } + + // print the number of paths and schemas in the document + fmt.Printf("There are %d paths and %d schemas in the document", paths, schemas) + // Output: There are 5 paths and 6 schemas in the document +}