mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
Adding docs and examples to code.
This commit is contained in:
71
document.go
71
document.go
@@ -1,7 +1,17 @@
|
|||||||
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
||||||
// SPDX-License-Identifier: MIT
|
// 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,35 +24,74 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"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
|
version string
|
||||||
info *datamodel.SpecInfo
|
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 {
|
type DocumentModel[T v2high.Swagger | v3high.Document] struct {
|
||||||
Model T
|
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)
|
info, err := datamodel.ExtractSpecInfo(specByteArray)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
d := new(Document)
|
d := new(document)
|
||||||
d.version = info.Version
|
d.version = info.Version
|
||||||
d.info = info
|
d.info = info
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) GetVersion() string {
|
func (d *document) GetVersion() string {
|
||||||
return d.version
|
return d.version
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) GetSpecInfo() *datamodel.SpecInfo {
|
func (d *document) GetSpecInfo() *datamodel.SpecInfo {
|
||||||
return d.info
|
return d.info
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) Serialize() ([]byte, error) {
|
func (d *document) Serialize() ([]byte, error) {
|
||||||
if d.info == nil {
|
if d.info == nil {
|
||||||
return nil, fmt.Errorf("unable to serialize, document has not yet been initialized")
|
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
|
var errors []error
|
||||||
if d.info == nil {
|
if d.info == nil {
|
||||||
errors = append(errors, fmt.Errorf("unable to build swagger document, no specification has been loaded"))
|
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 {
|
if d.info.SpecFormat != datamodel.OAS2 {
|
||||||
errors = append(errors, fmt.Errorf("unable to build swagger document, "+
|
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
|
return nil, errors
|
||||||
}
|
}
|
||||||
lowDoc, err := v2low.CreateDocument(d.info)
|
lowDoc, err := v2low.CreateDocument(d.info)
|
||||||
@@ -75,7 +124,7 @@ func (d *Document) BuildV2Document() (*DocumentModel[v2high.Swagger], []error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) BuildV3Document() (*DocumentModel[v3high.Document], []error) {
|
func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) {
|
||||||
var errors []error
|
var errors []error
|
||||||
if d.info == nil {
|
if d.info == nil {
|
||||||
errors = append(errors, fmt.Errorf("unable to build document, no specification has been loaded"))
|
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 {
|
if d.info.SpecFormat != datamodel.OAS3 {
|
||||||
errors = append(errors, fmt.Errorf("unable to build openapi document, "+
|
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
|
return nil, errors
|
||||||
}
|
}
|
||||||
lowDoc, err := v3low.CreateDocument(d.info)
|
lowDoc, err := v3low.CreateDocument(d.info)
|
||||||
|
|||||||
147
document_test.go
147
document_test.go
@@ -1,11 +1,13 @@
|
|||||||
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package main
|
package libopenapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/pb33f/libopenapi/utils"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ func TestLoadDocument_Simple_V2(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "2.0.1", doc.GetVersion())
|
assert.Equal(t, "2.0.1", doc.GetVersion())
|
||||||
|
|
||||||
v2Doc, docErr := doc.BuildV2Document()
|
v2Doc, docErr := doc.BuildV2Model()
|
||||||
assert.Len(t, docErr, 0)
|
assert.Len(t, docErr, 0)
|
||||||
assert.NotNil(t, v2Doc)
|
assert.NotNil(t, v2Doc)
|
||||||
assert.NotNil(t, doc.GetSpecInfo())
|
assert.NotNil(t, doc.GetSpecInfo())
|
||||||
@@ -31,7 +33,7 @@ func TestLoadDocument_Simple_V2_Error(t *testing.T) {
|
|||||||
doc, err := NewDocument([]byte(yml))
|
doc, err := NewDocument([]byte(yml))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
v2Doc, docErr := doc.BuildV3Document()
|
v2Doc, docErr := doc.BuildV3Model()
|
||||||
assert.Len(t, docErr, 1)
|
assert.Len(t, docErr, 1)
|
||||||
assert.Nil(t, v2Doc)
|
assert.Nil(t, v2Doc)
|
||||||
}
|
}
|
||||||
@@ -45,7 +47,7 @@ definitions:
|
|||||||
doc, err := NewDocument([]byte(yml))
|
doc, err := NewDocument([]byte(yml))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
v2Doc, docErr := doc.BuildV2Document()
|
v2Doc, docErr := doc.BuildV2Model()
|
||||||
assert.Len(t, docErr, 1)
|
assert.Len(t, docErr, 1)
|
||||||
assert.Nil(t, v2Doc)
|
assert.Nil(t, v2Doc)
|
||||||
}
|
}
|
||||||
@@ -56,22 +58,22 @@ func TestLoadDocument_Simple_V3_Error(t *testing.T) {
|
|||||||
doc, err := NewDocument([]byte(yml))
|
doc, err := NewDocument([]byte(yml))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
v2Doc, docErr := doc.BuildV2Document()
|
v2Doc, docErr := doc.BuildV2Model()
|
||||||
assert.Len(t, docErr, 1)
|
assert.Len(t, docErr, 1)
|
||||||
assert.Nil(t, v2Doc)
|
assert.Nil(t, v2Doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDocument_Error_V2NoSpec(t *testing.T) {
|
func TestLoadDocument_Error_V2NoSpec(t *testing.T) {
|
||||||
|
|
||||||
doc := new(Document) // not how this should be instantiated.
|
doc := new(document) // not how this should be instantiated.
|
||||||
_, err := doc.BuildV2Document()
|
_, err := doc.BuildV2Model()
|
||||||
assert.Len(t, err, 1)
|
assert.Len(t, err, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDocument_Error_V3NoSpec(t *testing.T) {
|
func TestLoadDocument_Error_V3NoSpec(t *testing.T) {
|
||||||
|
|
||||||
doc := new(Document) // not how this should be instantiated.
|
doc := new(document) // not how this should be instantiated.
|
||||||
_, err := doc.BuildV3Document()
|
_, err := doc.BuildV3Model()
|
||||||
assert.Len(t, err, 1)
|
assert.Len(t, err, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +90,7 @@ func TestLoadDocument_Simple_V3(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "3.0.1", doc.GetVersion())
|
assert.Equal(t, "3.0.1", doc.GetVersion())
|
||||||
|
|
||||||
v3Doc, docErr := doc.BuildV3Document()
|
v3Doc, docErr := doc.BuildV3Model()
|
||||||
assert.Len(t, docErr, 0)
|
assert.Len(t, docErr, 0)
|
||||||
assert.NotNil(t, v3Doc)
|
assert.NotNil(t, v3Doc)
|
||||||
}
|
}
|
||||||
@@ -102,13 +104,13 @@ paths:
|
|||||||
doc, err := NewDocument([]byte(yml))
|
doc, err := NewDocument([]byte(yml))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
v3Doc, docErr := doc.BuildV3Document()
|
v3Doc, docErr := doc.BuildV3Model()
|
||||||
assert.Len(t, docErr, 1)
|
assert.Len(t, docErr, 1)
|
||||||
assert.Nil(t, v3Doc)
|
assert.Nil(t, v3Doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDocument_Serialize_Error(t *testing.T) {
|
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()
|
_, err := doc.Serialize()
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,7 @@ info:
|
|||||||
`
|
`
|
||||||
doc, _ := NewDocument([]byte(yml))
|
doc, _ := NewDocument([]byte(yml))
|
||||||
|
|
||||||
v3Doc, _ := doc.BuildV3Document()
|
v3Doc, _ := doc.BuildV3Model()
|
||||||
|
|
||||||
v3Doc.Model.Info.GoLow().Title.Mutate("The magic API - but now, altered!")
|
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"}`
|
jsonModified := `{"info":{"title":"The magic API - but now, altered!"},"openapi":"3.0"}`
|
||||||
doc, _ := NewDocument([]byte(json))
|
doc, _ := NewDocument([]byte(json))
|
||||||
|
|
||||||
v3Doc, _ := doc.BuildV3Document()
|
v3Doc, _ := doc.BuildV3Model()
|
||||||
|
|
||||||
// eventually this will be encapsulated up high.
|
// eventually this will be encapsulated up high.
|
||||||
// mutation does not replace low model, eventually pointers will be used.
|
// 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.NoError(t, err)
|
||||||
assert.Equal(t, jsonModified, string(serial))
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user