// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package libopenapi import ( "fmt" "github.com/pb33f/libopenapi/datamodel" "net/url" "os" "strings" "testing" "github.com/pb33f/libopenapi/datamodel/high" v3high "github.com/pb33f/libopenapi/datamodel/high/v3" low "github.com/pb33f/libopenapi/datamodel/low/base" v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/libopenapi/resolver" "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" ) func ExampleNewDocument_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 := 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_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.DocumentConfiguration{ AllowFileReferences: false, AllowRemoteReferences: false, } // 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() // 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: Error building Digital Ocean spec errors reported } func ExampleNewDocument_fromWithDocumentConfigurationSuccess() { // 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") // 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") // 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{ AllowFileReferences: true, AllowRemoteReferences: true, BaseURL: baseURL, } // 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() // 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") } // Output: Digital Ocean spec built successfully } func ExampleNewDocument_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 := 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, _ := 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 = 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 } func ExampleNewDocument_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 } func ExampleCompareDocuments_openAPI() { // How to compare two different OpenAPI specifications. // load an original OpenAPI 3 specification from bytes burgerShopOriginal, _ := os.ReadFile("test_specs/burgershop.openapi.yaml") // load an **updated** OpenAPI 3 specification from bytes burgerShopUpdated, _ := os.ReadFile("test_specs/burgershop.openapi-modified.yaml") // create a new document from original specification bytes originalDoc, err := NewDocument(burgerShopOriginal) // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } // create a new document from updated specification bytes updatedDoc, err := NewDocument(burgerShopUpdated) // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } // Compare documents for all changes made documentChanges, errs := 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]) } panic(fmt.Sprintf("cannot compare documents: %d errors reported", len(errs))) } // Extract SchemaChanges from components changes. schemaChanges := documentChanges.ComponentsChanges.SchemaChanges // Print out some interesting stats about the OpenAPI document changes. fmt.Printf("There are %d changes, of which %d are breaking. %v schemas have changes.", documentChanges.TotalChanges(), documentChanges.TotalBreakingChanges(), len(schemaChanges)) //Output: There are 72 changes, of which 17 are breaking. 5 schemas have changes. } func ExampleCompareDocuments_swagger() { // How to compare two different Swagger specifications. // load an original OpenAPI 3 specification from bytes petstoreOriginal, _ := os.ReadFile("test_specs/petstorev2-complete.yaml") // load an **updated** OpenAPI 3 specification from bytes petstoreUpdated, _ := os.ReadFile("test_specs/petstorev2-complete-modified.yaml") // create a new document from original specification bytes originalDoc, err := NewDocument(petstoreOriginal) // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } // create a new document from updated specification bytes updatedDoc, err := NewDocument(petstoreUpdated) // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) } // Compare documents for all changes made documentChanges, errs := 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]) } panic(fmt.Sprintf("cannot compare documents: %d errors reported", len(errs))) } // Extract SchemaChanges from components changes. schemaChanges := documentChanges.ComponentsChanges.SchemaChanges // Print out some interesting stats about the Swagger document changes. fmt.Printf("There are %d changes, of which %d are breaking. %v schemas have changes.", documentChanges.TotalChanges(), documentChanges.TotalBreakingChanges(), len(schemaChanges)) //Output: There are 52 changes, of which 27 are breaking. 5 schemas have changes. } func TestDocument_Paths_As_Array(t *testing.T) { // paths can now be wrapped in an array. spec := `{ "openapi": "3.1.0", "paths": [ "/": { "get": {} } ] } ` // 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)) } v3Model, _ := doc.BuildV3Model() assert.NotNil(t, v3Model) } // 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 func ExampleNewDocument_infinite_circular_references() { // 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.(*resolver.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 } // This tests checks that circular references which are _not_ marked as required pass correctly func TestNewDocument_terminable_circular_references(t *testing.T) { // 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" Two: description: "test two" properties: testThing: "$ref": "#/components/schemas/One" ` // 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() assert.Len(t, errs, 0) } // 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. func ExampleNewDocument_unpacking_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["SchemaOne"].Schema() parameterOne := docModel.Model.Components.Parameters["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["x-custom-cakes"] // extract extension by name for schemaOne customBurgers := parameterOneExtensions["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 ExampleNewDocument_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 := len(v3Model.Model.Paths.PathItems) // add the path to the document 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))) } // capture new number of paths after re-rendering newPaths := 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 original spec had %d bytes, the new one has %d\n", len(petstore), len(rawBytes)) // Output: There were 13 original paths. There are now 14 paths in the document //The original spec had 31143 bytes, the new one has 31027 }