mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
Docs, examples and new logo!
This commit is contained in:
318
README.md
318
README.md
@@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# libopenapi - enterprise grade OpenAPI tools for golang.
|
||||
|
||||

|
||||
@@ -49,10 +51,11 @@ Want a lightning fast way to look up any element in an OpenAPI swagger spec? **l
|
||||
Need a way to 'resolve' OpenAPI documents that are exploded out across multiple files, remotely or locally?
|
||||
**libopenapi has you covered**
|
||||
|
||||
> **Read the full docs at [https://pkg.go.dev](https://pkg.go.dev/github.com/pb33f/libopenapi)**
|
||||
|
||||
---
|
||||
|
||||
## Some examples to get you started.
|
||||
|
||||
## Installing
|
||||
Grab the latest release of **libopenapi**
|
||||
|
||||
```
|
||||
@@ -62,34 +65,40 @@ go get github.com/pb33f/libopenapi
|
||||
### Load an OpenAPI 3+ spec into a model
|
||||
|
||||
```go
|
||||
// load an OpenAPI 3 specification from bytes
|
||||
petstore, _ := ioutil.ReadFile("test_specs/petstorev3.json")
|
||||
// import the library
|
||||
import "github.com/pb33f/libopenapi"
|
||||
|
||||
// create a new document from specification bytes
|
||||
document, err := NewDocument(petstore)
|
||||
func readSpec() {
|
||||
|
||||
// if anything went wrong, an error is thrown
|
||||
if err != nil {
|
||||
// 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()
|
||||
// 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 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
```
|
||||
|
||||
This will print:
|
||||
@@ -101,34 +110,40 @@ There are 13 paths and 8 schemas in the document
|
||||
|
||||
### Load a Swagger (OpenAPI 2) spec into a model
|
||||
```go
|
||||
// load a Swagger specification from bytes
|
||||
petstore, _ := ioutil.ReadFile("test_specs/petstorev2.json")
|
||||
// import the library
|
||||
import "github.com/pb33f/libopenapi"
|
||||
|
||||
// create a new document from specification bytes
|
||||
document, err := NewDocument(petstore)
|
||||
func readSpec() {
|
||||
|
||||
// if anything went wrong, an error is thrown
|
||||
if err != nil {
|
||||
// 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()
|
||||
// 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 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
```
|
||||
|
||||
This will print:
|
||||
@@ -170,14 +185,9 @@ fmt.Printf("value is %s, the value is on line %d, " +
|
||||
lowReqBody.Description.KeyNode.Line,
|
||||
lowReqBody.KeyNode.Column)
|
||||
```
|
||||
|
||||
The library heavily depends on the fantastic (yet hard to get used to) [yaml.Node API](https://pkg.go.dev/gopkg.in/yaml.v3#Node).
|
||||
This is what is exposed by the `GoLow` API. It does not matter if the input material is JSON or YAML, the yaml.Node API
|
||||
creates a great way to navigate the AST of the document.
|
||||
|
||||
---
|
||||
|
||||
## But wait, there's more!
|
||||
## But wait, there's more - Mutating the model
|
||||
|
||||
Having a read-only model is great, but what about when we want to modify the model and mutate values, or even add new
|
||||
content to the model? What if we also want to save that output as an updated specification - but we don't want to jumble up
|
||||
@@ -199,8 +209,8 @@ tree in-tact. It allows us to make changes to values in place, and serialize bac
|
||||
other content order.
|
||||
|
||||
```go
|
||||
// create very small, and useless spec that does nothing useful, except showcase this feature.
|
||||
spec := `
|
||||
// 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
|
||||
@@ -210,48 +220,47 @@ info:
|
||||
license:
|
||||
url: http://some-place-on-the-internet.com/license
|
||||
`
|
||||
// create a new document from specification bytes
|
||||
document, err := NewDocument([]byte(spec))
|
||||
// create a new document from specification bytes
|
||||
document, err := NewDocument([]byte(spec))
|
||||
|
||||
// if anything went wrong, an error is thrown
|
||||
if err != nil {
|
||||
// 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()
|
||||
// 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 {
|
||||
// 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 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 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 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")
|
||||
// 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()
|
||||
// serialize the document back into the original YAML or JSON
|
||||
mutatedSpec, serialError := document.Serialize()
|
||||
|
||||
// if something went wrong serializing
|
||||
if serialError != nil {
|
||||
// if something went wrong serializing
|
||||
if serialError != nil {
|
||||
panic(fmt.Sprintf("cannot serialize document: %e", serialError))
|
||||
}
|
||||
|
||||
// print our modified spec!
|
||||
fmt.Println(string(mutatedSpec))
|
||||
}
|
||||
|
||||
// print our modified spec!
|
||||
fmt.Println(string(mutatedSpec))
|
||||
```
|
||||
|
||||
Which will output:
|
||||
@@ -267,7 +276,168 @@ info:
|
||||
url: https://pb33f.io/license
|
||||
|
||||
```
|
||||
> It's worth noting that the original line numbers and column numbers **won't be respected** when calling `Serialize()`,
|
||||
> A new `Document` needs to be created from that raw YAML to continue processing after serialization.
|
||||
|
||||
## Creating an index of an OpenAPI Specification
|
||||
|
||||
An index is really useful when a map of an OpenAPI spec is needed. Knowing where all the references are and where
|
||||
they point, is very useful when resolving specifications, or just looking things up.
|
||||
|
||||
### Creating an index from the Stripe OpenAPI Spec
|
||||
|
||||
```go
|
||||
// define a rootNode to hold our raw stripe spec AST.
|
||||
var rootNode yaml.Node
|
||||
|
||||
// load in the stripe OpenAPI specification into bytes (it's pretty meaty)
|
||||
stripeSpec, _ := ioutil.ReadFile("test_specs/stripe.yaml")
|
||||
|
||||
// unmarshal spec into our rootNode
|
||||
yaml.Unmarshal(stripeSpec, &rootNode)
|
||||
|
||||
// create a new specification index.
|
||||
index := NewSpecIndex(&rootNode)
|
||||
|
||||
// print out some statistics
|
||||
fmt.Printf("There are %d references\n"+
|
||||
"%d paths\n"+
|
||||
"%d operations\n"+
|
||||
"%d schemas\n"+
|
||||
"%d enums\n"+
|
||||
"%d polymorphic references",
|
||||
len(index.GetAllCombinedReferences()),
|
||||
len(index.GetAllPaths()),
|
||||
index.GetOperationCount(),
|
||||
len(index.GetAllSchemas()),
|
||||
len(index.GetAllEnums()),
|
||||
len(index.GetPolyOneOfReferences())+len(index.GetPolyAnyOfReferences()))
|
||||
```
|
||||
|
||||
## Resolving an OpenAPI Specification
|
||||
|
||||
When creating an index, the raw AST that uses [yaml.Node](https://pkg.go.dev/gopkg.in/yaml.v3#Node) is preserved
|
||||
when looking up local, file-based and remote references. This means that if required, the spec can be 'resolved'
|
||||
and all the reference nodes will be replaced with the actual data they reference.
|
||||
|
||||
What this looks like from a spec perspective.
|
||||
|
||||
If the specification looks like this:
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
"/some/path/to/a/thing":
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
$ref: '#/components/schemas/MySchema'
|
||||
components:
|
||||
schemas:
|
||||
MySchema:
|
||||
type: string
|
||||
description: This is my schema that is great!
|
||||
```
|
||||
|
||||
Will become this (as represented by the root [yaml.Node](https://pkg.go.dev/gopkg.in/yaml.v3#Node)
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
"/some/path/to/a/thing":
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
type: string
|
||||
description: This is my schema that is great!
|
||||
components:
|
||||
schemas:
|
||||
MySchema:
|
||||
type: string
|
||||
description: This is my schema that is great!
|
||||
```
|
||||
> This is not a valid spec, it's just design to illustrate how resolving works.
|
||||
|
||||
The reference has been 'resolved', so when reading the raw AST, there is no lookup required anymore.
|
||||
|
||||
### Resolving Example:
|
||||
|
||||
Using the Stripe API as an example, we can resolve all references, and then count how many circular reference issues
|
||||
were found.
|
||||
|
||||
```go
|
||||
// create a yaml.Node reference as a root node.
|
||||
var rootNode yaml.Node
|
||||
|
||||
// load in the Stripe OpenAPI spec (lots of polymorphic complexity in here)
|
||||
stripeBytes, _ := ioutil.ReadFile("../test_specs/stripe.yaml")
|
||||
|
||||
// unmarshal bytes into our rootNode.
|
||||
_ = yaml.Unmarshal(stripeBytes, &rootNode)
|
||||
|
||||
// create a new spec index (resolver depends on it)
|
||||
index := index.NewSpecIndex(&rootNode)
|
||||
|
||||
// create a new resolver using the index.
|
||||
resolver := NewResolver(index)
|
||||
|
||||
// resolve the document, if there are circular reference errors, they are returned/
|
||||
// WARNING: this is a destructive action and the rootNode will be PERMANENTLY altered and cannot be unresolved
|
||||
circularErrors := resolver.Resolve()
|
||||
|
||||
// The Stripe API has a bunch of circular reference problems, mainly from polymorphism.
|
||||
// So let's print them out.
|
||||
fmt.Printf("There are %d circular reference errors, %d of them are polymorphic errors, %d are not",
|
||||
len(circularErrors), len(resolver.GetPolymorphicCircularErrors()), len(resolver.GetNonPolymorphicCircularErrors()))
|
||||
```
|
||||
|
||||
This will output:
|
||||
|
||||
`There are 21 circular reference errors, 19 of them are polymorphic errors, 2 are not`
|
||||
|
||||
> Important to remember: Resolving is **destructive** and will permanently change the tree, it cannot be un-resolved.
|
||||
|
||||
### Checking for circular errors without resolving
|
||||
|
||||
Resolving is destructive, the original reference nodes are gone and all replaced, so how do we check for circular references
|
||||
in a non-destructive way? Instead of calling `Resolve()`, we can call `CheckForCircularReferences()` instead.
|
||||
|
||||
The same code as `Resolve()` executes, except the tree is **not actually resolved**, _nothing_ changes and _no destruction_
|
||||
occurs. A handy way to perform circular reference analysis on the specification, without permanently altering it.
|
||||
|
||||
```go
|
||||
// create a yaml.Node reference as a root node.
|
||||
var rootNode yaml.Node
|
||||
|
||||
// load in the Stripe OpenAPI spec (lots of polymorphic complexity in here)
|
||||
stripeBytes, _ := ioutil.ReadFile("../test_specs/stripe.yaml")
|
||||
|
||||
// unmarshal bytes into our rootNode.
|
||||
_ = yaml.Unmarshal(stripeBytes, &rootNode)
|
||||
|
||||
// create a new spec index (resolver depends on it)
|
||||
index := index.NewSpecIndex(&rootNode)
|
||||
|
||||
// create a new resolver using the index.
|
||||
resolver := NewResolver(index)
|
||||
|
||||
// extract circular reference errors without any changes to the original tree.
|
||||
circularErrors := resolver.CheckForCircularReferences()
|
||||
|
||||
// The Stripe API has a bunch of circular reference problems, mainly from polymorphism.
|
||||
// So let's print them out.
|
||||
fmt.Printf("There are %d circular reference errors, %d of them are polymorphic errors, %d are not",
|
||||
len(circularErrors), len(resolver.GetPolymorphicCircularErrors()), len(resolver.GetNonPolymorphicCircularErrors()))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **Read the full docs at [https://pkg.go.dev](https://pkg.go.dev/github.com/pb33f/libopenapi)**
|
||||
|
||||
---
|
||||
|
||||
The library heavily depends on the fantastic (yet hard to get used to) [yaml.Node API](https://pkg.go.dev/gopkg.in/yaml.v3#Node).
|
||||
This is what is exposed by the `GoLow` API. It does not matter if the input material is JSON or YAML, the yaml.Node API
|
||||
creates a great way to navigate the AST of the document.
|
||||
This is what is exposed by the `GoLow` API.
|
||||
|
||||
> It does not matter if the input material is JSON or YAML, the [yaml.Node API](https://pkg.go.dev/gopkg.in/yaml.v3#Node) supports both and
|
||||
> creates a great way to navigate the AST of the document.
|
||||
|
||||
Logo gopher is modified, originally from [egonelbre](https://github.com/egonelbre/gophers)
|
||||
@@ -1,6 +1,15 @@
|
||||
// Copyright 2022 Dave Shanley / Quobix
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// 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.
|
||||
//
|
||||
// When extracting references, the index can determine if the reference is local to the file (recommended) or the
|
||||
// reference is located in another local file, or a remote file. The index will then attempt to load in those remote
|
||||
// files and look up the references there, or continue following the chain.
|
||||
//
|
||||
// When the index loads in a local or remote file, it will also index that remote spec as well. This means everything
|
||||
// is indexed and stored as a tree, depending on how deep the remote references go.
|
||||
package index
|
||||
|
||||
import (
|
||||
@@ -15,10 +24,11 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Constants used to determine if resolving is local, file based or remote file based.
|
||||
const (
|
||||
LocalResolve int = 0
|
||||
HttpResolve int = 1
|
||||
FileResolve int = 2
|
||||
LocalResolve = iota
|
||||
HttpResolve
|
||||
FileResolve
|
||||
)
|
||||
|
||||
// Reference is a wrapper around *yaml.Node results to make things more manageable when performing
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io/ioutil"
|
||||
@@ -572,3 +573,39 @@ func TestSpecIndex_lookupFileReference(t *testing.T) {
|
||||
assert.NotNil(t, k)
|
||||
|
||||
}
|
||||
|
||||
// Example of how to load in an OpenAPI Specification and index it.
|
||||
func ExampleNewSpecIndex() {
|
||||
|
||||
// define a rootNode to hold our raw spec AST.
|
||||
var rootNode yaml.Node
|
||||
|
||||
// load in the stripe OpenAPI specification into bytes (it's pretty meaty)
|
||||
stripeSpec, _ := ioutil.ReadFile("../test_specs/stripe.yaml")
|
||||
|
||||
// unmarshal spec into our rootNode
|
||||
yaml.Unmarshal(stripeSpec, &rootNode)
|
||||
|
||||
// create a new specification index.
|
||||
index := NewSpecIndex(&rootNode)
|
||||
|
||||
// print out some statistics
|
||||
fmt.Printf("There are %d references\n"+
|
||||
"%d paths\n"+
|
||||
"%d operations\n"+
|
||||
"%d schemas\n"+
|
||||
"%d enums\n"+
|
||||
"%d polymorphic references",
|
||||
len(index.GetAllCombinedReferences()),
|
||||
len(index.GetAllPaths()),
|
||||
index.GetOperationCount(),
|
||||
len(index.GetAllSchemas()),
|
||||
len(index.GetAllEnums()),
|
||||
len(index.GetPolyOneOfReferences())+len(index.GetPolyAnyOfReferences()))
|
||||
// Output: There are 537 references
|
||||
// 246 paths
|
||||
// 402 operations
|
||||
// 537 schemas
|
||||
// 1516 enums
|
||||
// 828 polymorphic references
|
||||
}
|
||||
|
||||
BIN
libopenapi-logo.png
Normal file
BIN
libopenapi-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@@ -1,6 +1,7 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pb33f/libopenapi/index"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -173,3 +174,34 @@ func TestResolver_ResolveComponents_k8s(t *testing.T) {
|
||||
circ := resolver.Resolve()
|
||||
assert.Len(t, circ, 1)
|
||||
}
|
||||
|
||||
// Example of how to resolve the Stripe OpenAPI specification, and check for circular reference errors
|
||||
func ExampleNewResolver() {
|
||||
|
||||
// create a yaml.Node reference as a root node.
|
||||
var rootNode yaml.Node
|
||||
|
||||
// load in the Stripe OpenAPI spec (lots of polymorphic complexity in here)
|
||||
stripeBytes, _ := ioutil.ReadFile("../test_specs/stripe.yaml")
|
||||
|
||||
// unmarshal bytes into our rootNode.
|
||||
_ = yaml.Unmarshal(stripeBytes, &rootNode)
|
||||
|
||||
// create a new spec index (resolver depends on it)
|
||||
index := index.NewSpecIndex(&rootNode)
|
||||
|
||||
// create a new resolver using the index.
|
||||
resolver := NewResolver(index)
|
||||
|
||||
// resolve the document, if there are circular reference errors, they are returned/
|
||||
// WARNING: this is a destructive action and the rootNode will be PERMANENTLY altered and cannot be unresolved
|
||||
circularErrors := resolver.Resolve()
|
||||
|
||||
// The Stripe API has a bunch of circular reference problems, mainly from polymorphism.
|
||||
// So let's print them out.
|
||||
//
|
||||
fmt.Printf("There are %d circular reference errors, %d of them are polymorphic errors, %d are not",
|
||||
len(circularErrors), len(resolver.GetPolymorphicCircularErrors()), len(resolver.GetNonPolymorphicCircularErrors()))
|
||||
// Output: There are 21 circular reference errors, 19 of them are polymorphic errors, 2 are not
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user