Docs, examples and new logo!

This commit is contained in:
Dave Shanley
2022-09-26 10:52:09 -04:00
parent 6214babaec
commit 57622b26e5
5 changed files with 1744 additions and 1495 deletions

318
README.md
View File

@@ -1,3 +1,5 @@
![libopenapi](libopenapi-logo.png)
# libopenapi - enterprise grade OpenAPI tools for golang.
![Pipeline](https://github.com/pb33f/libopenapi/workflows/Build/badge.svg)
@@ -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)

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -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
}