Documentation ¶
Overview ¶
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.
Index ¶
Examples ¶
- NewDocument (FromOpenAPI3Document)
- NewDocument (FromSwaggerDocument)
- NewDocument (FromUnknownVersion)
- NewDocument (FromWithDocumentConfigurationFailure)
- NewDocument (FromWithDocumentConfigurationSuccess)
- NewDocument (Infinite_circular_references)
- NewDocument (ModifyAndReRender)
- NewDocument (MutateValuesAndSerialize)
- NewDocument (Unpacking_extensions)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CompareDocuments ¶ added in v0.2.0
func CompareDocuments(original, updated Document) (*model.DocumentChanges, []error)
CompareDocuments will accept a left and right Document implementing struct, build a model for the correct version and then compare model documents for changes.
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.
Types ¶
type Document ¶
type Document interface { // GetVersion will return the exact version of the OpenAPI specification set for the document. GetVersion() string // GetRolodex will return the Rolodex instance that was used to load the document. GetRolodex() *index.Rolodex // GetSpecInfo will return the *datamodel.SpecInfo instance that contains all specification information. GetSpecInfo() *datamodel.SpecInfo // SetConfiguration will set the configuration for the document. This allows for finer grained control over // allowing remote or local references, as well as a BaseURL to allow for relative file references. SetConfiguration(configuration *datamodel.DocumentConfiguration) // GetConfiguration will return the configuration for the document. This allows for finer grained control over // allowing remote or local references, as well as a BaseURL to allow for relative file references. GetConfiguration() *datamodel.DocumentConfiguration // 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) // 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 // extracted from the model that was re-rendered. This is useful if you want to make changes to the high level model // and then 'reload' the model into memory, so that line numbers and column numbers are correct and all update // according to the changes made. // // The method returns the raw YAML bytes that were rendered, and any errors that occurred during rebuilding of the model. // This is a destructive operation, and will re-build the entire model from scratch using the new bytes, so any // references to the old model will be lost. The second return is the new Document that was created, and the third // return is any errors hit trying to re-render. // // **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) // Render will render the high level model as it currently exists (including any mutations, additions // and removals to and from any object in the tree). Unlike RenderAndReload, Render will simply print the state // of the model as it currently exists, and will not re-load the model into memory. It means that the low-level and // the high-level models will be out of sync, and the index will only be useful for the original document. // // Why use this instead of RenderAndReload? // // The simple answer is that RenderAndReload is a destructive operation, and will re-build the entire model from // scratch using the new bytes, which is desirable if you want to make changes to the high level model and then // 'reload' the model into memory, so that line numbers and column numbers are correct and the index is accurate. // However, if you don't care about the low-level model, and you're not using the index, and you just want to // print the state of the model as it currently exists, then Render() is the method to use. // **IMPORTANT** This method only supports OpenAPI Documents. Render() ([]byte, 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. // Deprecated: This method is deprecated and will be removed in a future release. Use RenderAndReload() instead. // This method does not support mutations correctly. Serialize() ([]byte, error) }
Document Represents an OpenAPI specification that can then be rendered into a model or serialized back into a string document after being manipulated.
func NewDocument ¶
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.
This function will NOT automatically follow (meaning load) any file or remote references that are found.
If this isn't the behavior you want, then you can use the NewDocumentWithConfiguration() function instead, which allows you to set a configuration that will allow you to control if file or remote references are allowed. In particular the `AllowFileReferences` and `FollowRemoteReferences` properties.
Example (FromOpenAPI3Document) ¶
// How to read in an OpenAPI 3 Specification, into a Document. // load an OpenAPI 3 specification from bytes petstore, _ := os.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 := orderedmap.Len(v3Model.Model.Paths.PathItems) schemas := orderedmap.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
Example (FromSwaggerDocument) ¶
// How to read in a Swagger / OpenAPI 2 Specification, into a Document. // load a Swagger specification from bytes petstore, _ := os.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 := orderedmap.Len(v2Model.Model.Paths.PathItems) schemas := orderedmap.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
Example (FromUnknownVersion) ¶
// load an unknown version of an OpenAPI spec petstore, _ := os.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 = orderedmap.Len(v3Model.Model.Paths.PathItems) schemas = orderedmap.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 = orderedmap.Len(v2Model.Model.Paths.PathItems) schemas = orderedmap.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
Example (FromWithDocumentConfigurationFailure) ¶
// This example shows how to create a document that prevents the loading of external references/ // from files or the network // load in the Digital Ocean OpenAPI specification digitalOcean, _ := os.ReadFile("test_specs/digitalocean.yaml") // create a DocumentConfiguration that prevents loading file and remote references config := datamodel.NewDocumentConfiguration() // create a new structured logger to capture error logs that will be spewed out by the rolodex // when it tries to load external references. We're going to create a byte buffer to capture the logs // and then look at them after the document is built. var logs []byte buf := bytes.NewBuffer(logs) logger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{ Level: slog.LevelError, })) config.Logger = logger // set the config logger to our new logger. // Do not set any baseURL, as this will allow the rolodex to resolve relative references. // without a baseURL (for remote references, or a basePath for local references) the rolodex // will consider the reference to be local, and will not attempt to load it from the network. // create a new document from specification bytes doc, err := NewDocumentWithConfiguration(digitalOcean, config) // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } // only errors will be thrown, so just capture them and print the number of errors. _, errors := doc.BuildV3Model() // there should be 475 errors logs logItems := strings.Split(buf.String(), "\n") fmt.Printf("There are %d errors logged\n", len(logItems)) // if anything went wrong when building the v3 model, a slice of errors will be returned if len(errors) > 0 { fmt.Println("Error building Digital Ocean spec errors reported") }
Output: There are 475 errors logged Error building Digital Ocean spec errors reported
Example (FromWithDocumentConfigurationSuccess) ¶
// load in the Digital Ocean OpenAPI specification digitalOcean, _ := os.ReadFile("test_specs/digitalocean.yaml") // Digital Ocean needs a baseURL to be set, so we can resolve relative references. //baseURL, _ := url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification") // locked this in to a release, because the spec is throwing 404's occasionally. baseURL, _ := url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/9b763029ba805c3e0e56927fd6685d73bc27f3e8/specification") // create a DocumentConfiguration that allows loading file and remote references, and sets the baseURL // to somewhere that can resolve the relative references. config := datamodel.DocumentConfiguration{ BaseURL: baseURL, Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelError, })), } // create a new document from specification bytes doc, err := NewDocumentWithConfiguration(digitalOcean, &config) // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } m, errors := doc.BuildV3Model() // if anything went wrong when building the v3 model, a slice of errors will be returned if len(errors) > 0 { fmt.Println("Error building Digital Ocean spec errors reported") } else { fmt.Println("Digital Ocean spec built successfully") } // running this through a change detection, will render out the entire model and // any stage two rendering for the model will be caught. what_changed.CompareOpenAPIDocuments(m.Model.GoLow(), m.Model.GoLow())
Output: Digital Ocean spec built successfully
Example (Infinite_circular_references) ¶
If you want to know more about circular references that have been found during the parsing/indexing/building of a document, you can capture the []errors thrown which are pointers to *resolver.ResolvingError
// create a specification with an obvious and deliberate circular reference spec := `openapi: "3.1" components: schemas: One: description: "test one" properties: things: "$ref": "#/components/schemas/Two" required: - things Two: description: "test two" properties: testThing: "$ref": "#/components/schemas/One" required: - testThing ` // create a new document from specification bytes doc, err := NewDocument([]byte(spec)) // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } _, errs := doc.BuildV3Model() // extract resolving error resolvingError := errs[0] // resolving error is a pointer to *resolver.ResolvingError // which provides access to rich details about the error. circularReference := resolvingError.(*index.ResolvingError).CircularReference // capture the journey with all details var buf strings.Builder for n := range circularReference.Journey { // add the full definition name to the journey. buf.WriteString(circularReference.Journey[n].Definition) if n < len(circularReference.Journey)-1 { buf.WriteString(" -> ") } } // print out the journey and the loop point. fmt.Printf("Journey: %s\n", buf.String()) fmt.Printf("Loop Point: %s", circularReference.LoopPoint.Definition)
Output: Journey: #/components/schemas/Two -> #/components/schemas/One -> #/components/schemas/Two Loop Point: #/components/schemas/Two
Example (ModifyAndReRender) ¶
// How to read in an OpenAPI 3 Specification, into a Document, // modify the document and then re-render it back to YAML bytes. // load an OpenAPI 3 specification from bytes petstore, _ := os.ReadFile("test_specs/petstorev3.json") // create a new document from specification bytes doc, 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 := 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]) } panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) } // create a new path item and operation. newPath := &v3high.PathItem{ Description: "this is a new path item", Get: &v3high.Operation{ Description: "this is a get operation", OperationId: "getNewThing", RequestBody: &v3high.RequestBody{ Description: "this is a new request body", }, }, } // capture original number of paths originalPaths := orderedmap.Len(v3Model.Model.Paths.PathItems) // add the path to the document v3Model.Model.Paths.PathItems.Set("/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))) } // capture new number of paths after re-rendering newPaths := orderedmap.Len(newModel.Model.Paths.PathItems) // 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 new spec has %d bytes\n", len(rawBytes))
Output: There were 13 original paths. There are now 14 paths in the document The new spec has 31213 bytes
Example (MutateValuesAndSerialize) ¶
// How to mutate values in an OpenAPI Specification, without re-ordering original content. // create very small, and useless spec that does nothing useful, except showcase this feature. spec := ` openapi: 3.1.0 info: title: This is a title contact: name: Some Person email: some@emailaddress.com license: url: https://some-place-on-the-internet.com/license ` // create a new document from specification bytes document, err := NewDocument([]byte(spec)) // 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))) } // mutate the title, to do this we currently need to drop down to the low-level API. v3Model.Model.GoLow().Info.Value.Title.Mutate("A new title for a useless spec") // mutate the email address in the contact object. v3Model.Model.GoLow().Info.Value.Contact.Value.Email.Mutate("buckaroo@pb33f.io") // mutate the name in the contact object. v3Model.Model.GoLow().Info.Value.Contact.Value.Name.Mutate("Buckaroo") // mutate the URL for the license object. v3Model.Model.GoLow().Info.Value.License.Value.URL.Mutate("https://pb33f.io/license") // serialize the document back into the original YAML or JSON mutatedSpec, serialError := document.Serialize() // if something went wrong serializing if serialError != nil { panic(fmt.Sprintf("cannot serialize document: %e", serialError)) } // print our modified spec! fmt.Println(string(mutatedSpec))
Output: openapi: 3.1.0 info: title: A new title for a useless spec contact: name: Buckaroo email: buckaroo@pb33f.io license: url: https://pb33f.io/license
Example (Unpacking_extensions) ¶
If you're using complex types with OpenAPI Extensions, it's simple to unpack extensions into complex types using `high.UnpackExtensions()`. libopenapi retains the original raw data in the low model (not the high) which means unpacking them can be a little complex.
This example demonstrates how to use the `UnpackExtensions` with custom OpenAPI extensions.
// define an example struct representing a cake type cake struct { Candles int `yaml:"candles"` Frosting string `yaml:"frosting"` Some_Strange_Var_Name string `yaml:"someStrangeVarName"` } // define a struct that holds a map of cake pointers. type cakes struct { Description string Cakes map[string]*cake } // define a struct representing a burger type burger struct { Sauce string Patty string } // define a struct that holds a map of cake pointers type burgers struct { Description string Burgers map[string]*burger } // create a specification with a schema and parameter that use complex custom cakes and burgers extensions. spec := `openapi: "3.1" components: schemas: SchemaOne: description: "Some schema with custom complex extensions" x-custom-cakes: description: some cakes cakes: someCake: candles: 10 frosting: blue someStrangeVarName: something anotherCake: candles: 1 frosting: green parameters: ParameterOne: description: "Some parameter also using complex extensions" x-custom-burgers: description: some burgers burgers: someBurger: sauce: ketchup patty: meat anotherBurger: sauce: mayo patty: lamb` // create a new document from specification bytes doc, err := NewDocument([]byte(spec)) // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } // build a v3 model. docModel, errs := doc.BuildV3Model() // if anything went wrong building, indexing and resolving the model, an error is thrown if errs != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } // get a reference to SchemaOne and ParameterOne schemaOne := docModel.Model.Components.Schemas.GetOrZero("SchemaOne").Schema() parameterOne := docModel.Model.Components.Parameters.GetOrZero("ParameterOne") // unpack schemaOne extensions into complex `cakes` type schemaOneExtensions, schemaUnpackErrors := high.UnpackExtensions[cakes, *low.Schema](schemaOne) if schemaUnpackErrors != nil { panic(fmt.Sprintf("cannot unpack schema extensions: %e", err)) } // unpack parameterOne into complex `burgers` type parameterOneExtensions, paramUnpackErrors := high.UnpackExtensions[burgers, *v3.Parameter](parameterOne) if paramUnpackErrors != nil { panic(fmt.Sprintf("cannot unpack parameter extensions: %e", err)) } // extract extension by name for schemaOne customCakes := schemaOneExtensions.GetOrZero("x-custom-cakes") // extract extension by name for schemaOne customBurgers := parameterOneExtensions.GetOrZero("x-custom-burgers") // print out schemaOne complex extension details. fmt.Printf("schemaOne 'x-custom-cakes' (%s) has %d cakes, 'someCake' has %d candles and %s frosting\n", customCakes.Description, len(customCakes.Cakes), customCakes.Cakes["someCake"].Candles, customCakes.Cakes["someCake"].Frosting, ) // print out parameterOne complex extension details. fmt.Printf("parameterOne 'x-custom-burgers' (%s) has %d burgers, 'anotherBurger' has %s sauce and a %s patty\n", customBurgers.Description, len(customBurgers.Burgers), customBurgers.Burgers["anotherBurger"].Sauce, customBurgers.Burgers["anotherBurger"].Patty, )
Output: schemaOne 'x-custom-cakes' (some cakes) has 2 cakes, 'someCake' has 10 candles and blue frosting parameterOne 'x-custom-burgers' (some burgers) has 2 burgers, 'anotherBurger' has mayo sauce and a lamb patty
func NewDocumentWithConfiguration ¶ added in v0.6.0
func NewDocumentWithConfiguration(specByteArray []byte, configuration *datamodel.DocumentConfiguration) (Document, error)
NewDocumentWithConfiguration is the same as NewDocument, except it's a convenience function that calls NewDocument under the hood and then calls SetConfiguration() on the returned Document.
Directories ¶
Path | Synopsis |
---|---|
Package datamodel contains two sets of models, high and low.
|
Package datamodel contains two sets of models, high and low. |
high
Package high contains a set of high-level models that represent OpenAPI 2 and 3 documents.
|
Package high contains a set of high-level models that represent OpenAPI 2 and 3 documents. |
high/base
Package base contains shared high-level models that are used between both versions 2 and 3 of OpenAPI.
|
Package base contains shared high-level models that are used between both versions 2 and 3 of OpenAPI. |
high/v2
Package v2 represents all Swagger / OpenAPI 2 high-level models.
|
Package v2 represents all Swagger / OpenAPI 2 high-level models. |
high/v3
Package v3 represents all OpenAPI 3+ high-level models.
|
Package v3 represents all OpenAPI 3+ high-level models. |
low
Package low contains a set of low-level models that represent OpenAPI 2 and 3 documents.
|
Package low contains a set of low-level models that represent OpenAPI 2 and 3 documents. |
low/base
Package base contains shared low-level models that are used between both versions 2 and 3 of OpenAPI.
|
Package base contains shared low-level models that are used between both versions 2 and 3 of OpenAPI. |
low/v2
Package v2 represents all Swagger / OpenAPI 2 low-level models.
|
Package v2 represents all Swagger / OpenAPI 2 low-level models. |
low/v3
Package v3 represents all OpenAPI 3+ low-level models.
|
Package v3 represents all OpenAPI 3+ low-level models. |
Package index contains an OpenAPI indexer that will very quickly scan through an OpenAPI specification (all versions) and extract references to all the important nodes you might want to look up, as well as counts on total objects.
|
Package index contains an OpenAPI indexer that will very quickly scan through an OpenAPI specification (all versions) and extract references to all the important nodes you might want to look up, as well as counts on total objects. |
Package what_changed
|
Package what_changed |
model
Package model
|
Package model |