(feat): Unpack extensions into complex function types #8

The more in-depth we use extensions, the more likely is is that we need custom extensions. New `UnpackExtensions` function in the high package makes this easy. Low level models have been updated to support feature fully and docs added in README and examples as well.

Signed-off-by: Dave Shanley <dave@quobix.com>
This commit is contained in:
Dave Shanley
2022-12-02 11:15:44 -05:00
parent 52a5b61de2
commit c08dd591b2
61 changed files with 756 additions and 89 deletions

387
README.md
View File

@@ -5,8 +5,27 @@
![Pipeline](https://github.com/pb33f/libopenapi/workflows/Build/badge.svg)
[![GoReportCard](https://goreportcard.com/badge/github.com/pb33f/libopenapi)](https://goreportcard.com/report/github.com/pb33f/libopenapi)
[![codecov](https://codecov.io/gh/pb33f/libopenapi/branch/main/graph/badge.svg?)](https://codecov.io/gh/pb33f/libopenapi)
[![Docs](https://img.shields.io/badge/godoc-reference-5fafd7)](https://pkg.go.dev/github.com/pb33f/libopenapi)
libopenapi has full support for Swagger (OpenAPI 2), OpenAPI 3, and OpenAPI 3.1.
libopenapi has full support for Swagger (OpenAPI 2), OpenAPI 3, and OpenAPI 3.1. It can handle the largest and most
complex specifications you can think of.
## Contents
- [Installing libopenapi](#installing)
- [Load an OpenAPI 3.1 or 3.0 specification into a model](#load-an-openapi-spec-into-a-model)
- [Load a Swagger Spec into a model](#load-a-swagger-spec-into-a-model)
- [Discover what changed](#discover-what-changed)
- [Loading complex types using extensions](#loading-extensions-using-complex-types)
- [Creating an index of an OpenAPI specification](#creating-an-index-of-an-openapi-specification)
- [Resolving an OpenAPI specification](#resolving-an-openapi-specification)
- [Checking for circular errors without resolving](#checking-for-circular-errors-without-resolving)
- [Extracting circular refs and resolving errors when building a document](#extracting-circular-refs-and-resolving-errors-when-building-a-document)
- [Mutating the model](#mutating-the-model)
> **Read the docs at [https://pkg.go.dev/github.com/pb33f/libopenapi](https://pkg.go.dev/github.com/pb33f/libopenapi)**
>
---
## Introduction - Why?
@@ -77,7 +96,7 @@ Grab the latest release of **libopenapi**
go get github.com/pb33f/libopenapi
```
### Load an OpenAPI 3+ spec into a model
## Load an OpenAPI spec into a model
```go
// import the library
@@ -124,7 +143,7 @@ There are 13 paths and 8 schemas in the document
```
### Load a Swagger (OpenAPI 2) spec into a model
## Load a Swagger spec into a model
```go
// import the library
import "github.com/pb33f/libopenapi"
@@ -135,7 +154,7 @@ func readSpec() {
petstore, _ := ioutil.ReadFile("test_specs/petstorev2.json")
// create a new document from specification bytes
document, err := libeopnapi.NewDocument(petstore)
document, err := libopenapi.NewDocument(petstore)
// if anything went wrong, an error is thrown
if err != nil {
@@ -205,7 +224,7 @@ fmt.Printf("value is %s, the value is on line %d, " +
```
---
## What changed?
## Discover what changed
libopenapi comes with a complete **diff engine**
@@ -260,97 +279,143 @@ Every change can be explored and navigated just like you would use the high or l
---
## But wait, there's more - Mutating the model
## Loading extensions using complex types
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 the original ordering of the source.
If you're using extensions with complex types (rather that just simple strings and primitives), then there is a good
chance you're going to want some simple way to marshal extensions into those structs.
### marshaling and unmarshalling to and from structs into JSON/YAML is not ideal.
Since version v0.4.0 There is a new method to make this simple available in the `high` package within the `datamodel`
package.
When we straight up use `json.Marshal` or `yaml.Marshal` to send structs to be rendered into the desired format, there
is no guarantee as to the order in which each component will be rendered. This works great if...
- We don't care about the spec being randomly ordered.
- We don't care about code-reviews.
- We don't actually care about this very much.
### But if we do care...
Then libopenpi provides a way to mutate the model, that keeps the original [yaml.Node API](https://pkg.go.dev/gopkg.in/yaml.v3#Node)
tree in-tact. It allows us to make changes to values in place, and serialize back to JSON or YAML without any changes to
other content order.
Here is an example of complex types being extracted easily from OpenAPI extensions.
```go
// 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: http://some-place-on-the-internet.com/license
`
// create a new document from specification bytes
document, err := libopenapi.NewDocument([]byte(spec))
// if anything went wrong, an error is thrown
if err != nil {
panic(fmt.Sprintf("cannot create new document: %e", err))
import (
"github.com/pb33f/libopenapi"
high "github.com/pb33f/libopenapi/datamodel/high"
lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3"
)
// define an example struct representing a cake
type cake struct {
Candles int
Frosting string
}
// because we know this is a v3 spec, we can build a ready to go model from it.
v3Model, errors := document.BuildV3Model()
// define a struct that holds a map of cake pointers.
type cakes struct {
Description string
Cakes map[string]*cake
}
// 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])
// 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
}
func main() {
// 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
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 := libopenapi.NewDocument([]byte(spec))
// if anything went wrong, an error is thrown
if err != nil {
panic(fmt.Sprintf("cannot create new document: %e", err))
}
panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors)))
// 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, *lowbase.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, *lowv3.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,
)
}
// 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))
```
Which will output:
```yaml
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
This will output:
```text
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
```
> 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
@@ -518,8 +583,168 @@ fmt.Printf("There are %d circular reference errors, " +
len(resolver.GetNonPolymorphicCircularErrors()))
```
### Extracting circular refs and resolving errors when building a document
To avoid having to create an index and a resolver each time you want to both create
a document and resolve it / check for errors, don't worry, circular references are checked
automatically and are available in the returned `[]errors` which building a document.
The errors returned by the slice are pointers to `*resolver.ResolvingError` which contains
rich details about the issue, where it was found and the journey it took to get there.
Here is an example:
```go
// 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()
// 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)
```
Will output:
```text
Journey: #/components/schemas/Two -> #/components/schemas/One -> #/components/schemas/Two
Loop Point: #/components/schemas/Two
```
---
## 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 the original ordering of the source.
### marshaling and unmarshalling to and from structs into JSON/YAML is not ideal.
When we straight up use `json.Marshal` or `yaml.Marshal` to send structs to be rendered into the desired format, there
is no guarantee as to the order in which each component will be rendered. This works great if...
- We don't care about the spec being randomly ordered.
- We don't care about code-reviews.
- We don't actually care about this very much.
### But if we do care...
Then libopenpi provides a way to mutate the model, that keeps the original [yaml.Node API](https://pkg.go.dev/gopkg.in/yaml.v3#Node)
tree in-tact. It allows us to make changes to values in place, and serialize back to JSON or YAML without any changes to
other content order.
```go
// 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: http://some-place-on-the-internet.com/license
`
// create a new document from specification bytes
document, err := libopenapi.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))
```
Which will output:
```yaml
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
```
> 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.
> **Read the full docs at [https://pkg.go.dev](https://pkg.go.dev/github.com/pb33f/libopenapi)**
---

View File

@@ -38,3 +38,7 @@ func NewExternalDoc(extDoc *low.ExternalDoc) *ExternalDoc {
func (e *ExternalDoc) GoLow() *low.ExternalDoc {
return e.low
}
func (e *ExternalDoc) GetExtensions() map[string]any {
return e.Extensions
}

View File

@@ -35,6 +35,7 @@ x-hack: code`
wentLow := highExt.GoLow()
assert.Equal(t, 2, wentLow.URL.ValueNode.Line)
assert.Len(t, highExt.GetExtensions(), 1)
}
func ExampleNewExternalDoc() {

View File

@@ -33,3 +33,34 @@ func ExtractExtensions(extensions map[low.KeyReference[string]]low.ValueReferenc
}
return extracted
}
// UnpackExtensions is a convenience function that makes it easy and simple to unpack an objects extensions
// into a complex type, provided as a generic. This function is for high-level models that implement `GoesLow()`
// and for low-level models that support extensions via `HasExtensions`.
//
// This feature will be upgraded at some point to hold a registry of types and extension mappings to make this
// functionality available a little more automatically.
// You can read more about the discussion here: https://github.com/pb33f/libopenapi/issues/8
//
// `T` represents the Type you want to unpack into
// `R` represents the LOW type of the object that contains the extensions (not the high)
// `low` represents the HIGH type of the object that contains the extensions.
//
// to use:
// schema := schemaProxy.Schema() // any high-level object that has extensions
// extensions, err := UnpackExtensions[MyComplexType, low.Schema](schema)
func UnpackExtensions[T any, R low.HasExtensions[T]](low GoesLow[R]) (map[string]*T, error) {
m := make(map[string]*T)
ext := low.GoLow().GetExtensions()
for i := range ext {
key := i.Value
g := new(T)
valueNode := ext[i].ValueNode
err := valueNode.Decode(g)
if err != nil {
return nil, err
}
m[key] = g
}
return m, nil
}

View File

@@ -6,6 +6,7 @@ package high
import (
"github.com/pb33f/libopenapi/datamodel/low"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
"testing"
)
@@ -18,4 +19,112 @@ func TestExtractExtensions(t *testing.T) {
}
ext := ExtractExtensions(n)
assert.Equal(t, "new cowboy in town", ext["pb33f"])
}
type textExtension struct {
Cowboy string
Power int
}
type parent struct {
low *child
}
func (p *parent) GoLow() *child {
return p.low
}
type child struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
func (c *child) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return c.Extensions
}
func TestUnpackExtensions(t *testing.T) {
var resultA, resultB yaml.Node
ymlA := `
cowboy: buckaroo
power: 100`
ymlB := `
cowboy: frogman
power: 2`
err := yaml.Unmarshal([]byte(ymlA), &resultA)
assert.NoError(t, err)
err = yaml.Unmarshal([]byte(ymlB), &resultB)
assert.NoError(t, err)
n := make(map[low.KeyReference[string]]low.ValueReference[any])
n[low.KeyReference[string]{
Value: "x-rancher-a",
}] = low.ValueReference[any]{
ValueNode: resultA.Content[0],
}
n[low.KeyReference[string]{
Value: "x-rancher-b",
}] = low.ValueReference[any]{
ValueNode: resultB.Content[0],
}
c := new(child)
c.Extensions = n
p := new(parent)
p.low = c
res, err := UnpackExtensions[textExtension, *child](p)
assert.NoError(t, err)
assert.NotEmpty(t, res)
assert.Equal(t, "buckaroo", res["x-rancher-a"].Cowboy)
assert.Equal(t, 100, res["x-rancher-a"].Power)
assert.Equal(t, "frogman", res["x-rancher-b"].Cowboy)
assert.Equal(t, 2, res["x-rancher-b"].Power)
}
func TestUnpackExtensions_Fail(t *testing.T) {
var resultA, resultB yaml.Node
ymlA := `
cowboy: buckaroo
power: 100`
// this is incorrect types, unpacking will fail.
ymlB := `
cowboy: 0
power: hello`
err := yaml.Unmarshal([]byte(ymlA), &resultA)
assert.NoError(t, err)
err = yaml.Unmarshal([]byte(ymlB), &resultB)
assert.NoError(t, err)
n := make(map[low.KeyReference[string]]low.ValueReference[any])
n[low.KeyReference[string]{
Value: "x-rancher-a",
}] = low.ValueReference[any]{
ValueNode: resultA.Content[0],
}
n[low.KeyReference[string]{
Value: "x-rancher-b",
}] = low.ValueReference[any]{
ValueNode: resultB.Content[0],
}
c := new(child)
c.Extensions = n
p := new(parent)
p.low = c
res, er := UnpackExtensions[textExtension, *child](p)
assert.Error(t, er)
assert.Empty(t, res)
}

View File

@@ -35,6 +35,11 @@ func (i *Info) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap(ext, i.Extensions)
}
// GetExtensions returns all extensions for Info
func (i *Info) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return i.Extensions
}
// Build will extract out the Contact and Info objects from the supplied root node.
func (i *Info) Build(root *yaml.Node, idx *index.SpecIndex) error {
i.Extensions = low.ExtractExtensions(root)

View File

@@ -55,6 +55,7 @@ x-cli-name: pizza cli`
cliName := n.FindExtension("x-cli-name")
assert.NotNil(t, cliName)
assert.Equal(t, "pizza cli", cliName.Value)
assert.Len(t, n.GetExtensions(), 1)
}
func TestContact_Build(t *testing.T) {

View File

@@ -381,6 +381,11 @@ func (s *Schema) FindProperty(name string) *low.ValueReference[*SchemaProxy] {
return low.FindItemInMap[*SchemaProxy](name, s.Properties.Value)
}
// GetExtensions returns all extensions for Schema
func (s *Schema) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return s.Extensions
}
// Build will perform a number of operations.
// Extraction of the following happens in this method:
// - Extensions

View File

@@ -12,7 +12,8 @@ import (
func TestSchemaProxy_Build(t *testing.T) {
yml := `description: something`
yml := `x-windows: washed
description: something`
var sch SchemaProxy
var idxNode yaml.Node
@@ -21,7 +22,7 @@ func TestSchemaProxy_Build(t *testing.T) {
err := sch.Build(idxNode.Content[0], nil)
assert.NoError(t, err)
assert.Equal(t, "3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb",
assert.Equal(t, "db2a35dd6fb3d9481d0682571b9d687616bb2a34c1887f7863f0b2e769ca7b23",
low.GenerateHashString(&sch))
assert.Equal(t, "something", sch.Schema().Description.Value)
@@ -30,9 +31,11 @@ func TestSchemaProxy_Build(t *testing.T) {
assert.False(t, sch.IsSchemaReference())
// already rendered, should spit out the same
assert.Equal(t, "3fc9b689459d738f8c88a3a48aa9e33542016b7a4052e001aaa536fca74813cb",
assert.Equal(t, "db2a35dd6fb3d9481d0682571b9d687616bb2a34c1887f7863f0b2e769ca7b23",
low.GenerateHashString(&sch))
assert.Len(t, sch.Schema().GetExtensions(), 1)
}
func TestSchemaProxy_Build_CheckRef(t *testing.T) {

View File

@@ -33,6 +33,7 @@ func (x *XML) Build(root *yaml.Node, _ *index.SpecIndex) error {
return nil
}
// GetExtensions returns all Tag extensions and satisfies the low.HasExtensions interface.
func (x *XML) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return x.Extensions
}

View File

@@ -34,6 +34,8 @@ type Hashable interface {
// HasExtensions is implemented by any object that exposes extensions
type HasExtensions[T any] interface {
// GetExtensions returns generic low level extensions
GetExtensions() map[KeyReference[string]]ValueReference[any]
}

View File

@@ -45,6 +45,11 @@ func (h *Header) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, h.Extensions)
}
// GetExtensions returns all Header extensions and satisfies the low.HasExtensions interface.
func (h *Header) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return h.Extensions
}
// Build will build out items, extensions and default value from the supplied node.
func (h *Header) Build(root *yaml.Node, idx *index.SpecIndex) error {
h.Extensions = low.ExtractExtensions(root)

View File

@@ -32,7 +32,8 @@ func TestHeader_Build(t *testing.T) {
func TestHeader_DefaultAsSlice(t *testing.T) {
yml := `default:
yml := `x-ext: thing
default:
- why
- so many
- variations`
@@ -47,7 +48,7 @@ func TestHeader_DefaultAsSlice(t *testing.T) {
assert.NotNil(t, n.Default.Value)
assert.Len(t, n.Default.Value, 3)
assert.Len(t, n.GetExtensions(), 1)
}
func TestHeader_DefaultAsObject(t *testing.T) {

View File

@@ -45,6 +45,11 @@ func (i *Items) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, i.Extensions)
}
// GetExtensions returns all Items extensions and satisfies the low.HasExtensions interface.
func (i *Items) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return i.Extensions
}
// Hash will return a consistent SHA256 Hash of the Items object
func (i *Items) Hash() [32]byte {
var f []string

View File

@@ -31,7 +31,8 @@ func TestItems_Build(t *testing.T) {
func TestItems_DefaultAsSlice(t *testing.T) {
yml := `default:
yml := `x-thing: thing
default:
- pizza
- cake`
@@ -44,6 +45,7 @@ func TestItems_DefaultAsSlice(t *testing.T) {
_ = n.Build(idxNode.Content[0], idx)
assert.Len(t, n.Default.Value, 2)
assert.Len(t, n.GetExtensions(), 1)
}
func TestItems_DefaultAsMap(t *testing.T) {

View File

@@ -78,6 +78,11 @@ func (p *Parameter) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, p.Extensions)
}
// GetExtensions returns all Parameter extensions and satisfies the low.HasExtensions interface.
func (p *Parameter) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return p.Extensions
}
// Build will extract out extensions, schema, items and default value
func (p *Parameter) Build(root *yaml.Node, idx *index.SpecIndex) error {
p.Extensions = low.ExtractExtensions(root)

View File

@@ -207,5 +207,6 @@ allowEmptyValue: true
assert.Equal(t, "int", v.Value.A) // A is v2
assert.True(t, n.GetRequired().Value)
assert.True(t, n.GetAllowEmptyValue().Value)
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -39,6 +39,11 @@ func (p *PathItem) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, p.Extensions)
}
// GetExtensions returns all PathItem extensions and satisfies the low.HasExtensions interface.
func (p *PathItem) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return p.Extensions
}
// Build will extract extensions, parameters and operations for all methods. Every method is handled
// asynchronously, in order to keep things moving quickly for complex operations.
func (p *PathItem) Build(root *yaml.Node, idx *index.SpecIndex) error {

View File

@@ -107,5 +107,6 @@ parameters:
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -19,6 +19,11 @@ type Paths struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all Paths extensions and satisfies the low.HasExtensions interface.
func (p *Paths) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return p.Extensions
}
// FindPath attempts to locate a PathItem instance, given a path key.
func (p *Paths) FindPath(path string) *low.ValueReference[*PathItem] {
for k, j := range p.PathItems {

View File

@@ -96,5 +96,6 @@ x-milk: creamy`
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -31,6 +31,11 @@ func (r *Response) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, r.Extensions)
}
// GetExtensions returns all Response extensions and satisfies the low.HasExtensions interface.
func (r *Response) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return r.Extensions
}
// FindHeader will attempt to locate a Header value, given a key
func (r *Response) FindHeader(hType string) *low.ValueReference[*Header] {
return low.FindItemInMap[*Header](hType, r.Headers.Value)

View File

@@ -110,4 +110,5 @@ headers:
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -21,6 +21,11 @@ type Responses struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all Responses extensions and satisfies the low.HasExtensions interface.
func (r *Responses) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return r.Extensions
}
// Build will extract default value and extensions from node.
func (r *Responses) Build(root *yaml.Node, idx *index.SpecIndex) error {
r.Extensions = low.ExtractExtensions(root)

View File

@@ -114,5 +114,6 @@ x-tea: warm`
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -23,6 +23,11 @@ type Scopes struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all Scopes extensions and satisfies the low.HasExtensions interface.
func (s *Scopes) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return s.Extensions
}
// FindScope will attempt to locate a scope string using a key.
func (s *Scopes) FindScope(scope string) *low.ValueReference[string] {
return low.FindItemInMap[string](scope, s.Values)

View File

@@ -39,5 +39,6 @@ burgers: chips`
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -31,6 +31,11 @@ type SecurityScheme struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all SecurityScheme extensions and satisfies the low.HasExtensions interface.
func (ss *SecurityScheme) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return ss.Extensions
}
// Build will extract extensions and scopes from the node.
func (ss *SecurityScheme) Build(root *yaml.Node, idx *index.SpecIndex) error {
ss.Extensions = low.ExtractExtensions(root)

View File

@@ -94,5 +94,6 @@ authorizationUrl: https://pb33f.io
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -116,6 +116,11 @@ func (s *Swagger) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, s.Extensions)
}
// GetExtensions returns all Swagger/Top level extensions and satisfies the low.HasExtensions interface.
func (s *Swagger) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return s.Extensions
}
func CreateDocument(info *datamodel.SpecInfo) (*Swagger, []error) {
doc := Swagger{Swagger: low.ValueReference[string]{Value: info.Version, ValueNode: info.RootNode}}

View File

@@ -60,6 +60,7 @@ func TestCreateDocument(t *testing.T) {
assert.Equal(t, true, doc.FindExtension("x-pet").Value)
assert.Equal(t, true, doc.FindExtension("X-Pet").Value)
assert.NotNil(t, doc.GetExternalDocs())
assert.Len(t, doc.GetExtensions(), 1)
}
func TestCreateDocument_Info(t *testing.T) {

View File

@@ -25,6 +25,11 @@ type Callback struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all Callback extensions and satisfies the low.HasExtensions interface.
func (cb *Callback) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return cb.Extensions
}
// FindExpression will locate a string expression and return a ValueReference containing the located PathItem
func (cb *Callback) FindExpression(exp string) *low.ValueReference[*PathItem] {
return low.FindItemInMap[*PathItem](exp, cb.Expression.Value)

View File

@@ -150,5 +150,6 @@ beer:
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 2)
}

View File

@@ -33,6 +33,11 @@ type Components struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all Components extensions and satisfies the low.HasExtensions interface.
func (co *Components) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return co.Extensions
}
// Hash will return a consistent SHA256 Hash of the Encoding object
func (co *Components) Hash() [32]byte {
var f []string

View File

@@ -221,7 +221,7 @@ func TestComponents_Build_HashEmpty(t *testing.T) {
err = n.Build(idxNode.Content[0], idx)
assert.NoError(t, err)
assert.Equal(t, "seagull", n.FindExtension("x-curry").Value)
assert.Len(t, n.GetExtensions(), 1)
assert.Equal(t, "9cf2c6ab3f9ff7e5231fcb391c8af5c47406711d2ca366533f21a8bb2f67edfe",
low.GenerateHashString(&n))

View File

@@ -107,6 +107,7 @@ func TestCreateDocument(t *testing.T) {
assert.Equal(t, "Burger Shop", doc.Info.Value.Title.Value)
assert.NotEmpty(t, doc.Info.Value.Title.Value)
assert.Equal(t, "https://pb33f.io/schema", doc.JsonSchemaDialect.Value)
assert.Len(t, doc.GetExtensions(), 1)
}
func TestCreateDocument_Info(t *testing.T) {

View File

@@ -96,6 +96,11 @@ func (d *Document) FindSecurityRequirement(name string) []low.ValueReference[str
return nil
}
// GetExtensions returns all Document extensions and satisfies the low.HasExtensions interface.
func (d *Document) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return d.Extensions
}
func (d *Document) GetExternalDocs() *low.NodeReference[any] {
return &low.NodeReference[any]{
KeyNode: d.ExternalDocs.KeyNode,

View File

@@ -47,6 +47,11 @@ func (h *Header) FindContent(ext string) *low.ValueReference[*MediaType] {
return low.FindItemInMap[*MediaType](ext, h.Content.Value)
}
// GetExtensions returns all Header extensions and satisfies the low.HasExtensions interface.
func (h *Header) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return h.Extensions
}
// Hash will return a consistent SHA256 Hash of the Header object
func (h *Header) Hash() [32]byte {
var f []string

View File

@@ -82,6 +82,7 @@ content:
ext := n.FindExtension("x-family-love").Value
assert.Equal(t, "strong", ext)
assert.Len(t, n.GetExtensions(), 1)
}
func TestHeader_Build_Success_Examples(t *testing.T) {

View File

@@ -35,6 +35,11 @@ type Link struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all Link extensions and satisfies the low.HasExtensions interface.
func (l *Link) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return l.Extensions
}
// FindParameter will attempt to locate a parameter string value, using a parameter name input.
func (l *Link) FindParameter(pName string) *low.ValueReference[string] {
return low.FindItemInMap[string](pName, l.Parameters)

View File

@@ -51,6 +51,7 @@ x-linky: slinky
assert.NotNil(t, n.Server.Value)
assert.Equal(t, "https://pb33f.io", n.Server.Value.URL.Value)
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -27,6 +27,11 @@ type MediaType struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all MediaType extensions and satisfies the low.HasExtensions interface.
func (mt *MediaType) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return mt.Extensions
}
// FindExtension will attempt to locate an extension with the supplied name.
func (mt *MediaType) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, mt.Extensions)

View File

@@ -141,4 +141,5 @@ example: a thing`
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -23,6 +23,11 @@ type OAuthFlows struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all OAuthFlows extensions and satisfies the low.HasExtensions interface.
func (o *OAuthFlows) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return o.Extensions
}
// FindExtension will attempt to locate an extension with the supplied name.
func (o *OAuthFlows) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, o.Extensions)
@@ -89,6 +94,11 @@ type OAuthFlow struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all OAuthFlow extensions and satisfies the low.HasExtensions interface.
func (o *OAuthFlow) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return o.Extensions
}
// FindScope attempts to locate a scope using a specified name.
func (o *OAuthFlow) FindScope(scope string) *low.ValueReference[string] {
return low.FindItemInMap[string](scope, o.Scopes.Value)

View File

@@ -37,6 +37,7 @@ x-tasty: herbs
assert.Equal(t, "https://pb33f.io/token", n.TokenUrl.Value)
assert.Equal(t, "https://pb33f.io/refresh", n.RefreshUrl.Value)
assert.Equal(t, "vanilla", n.FindScope("fresh:cake").Value)
assert.Len(t, n.GetExtensions(), 1)
}
func TestOAuthFlow_Build_Implicit(t *testing.T) {
@@ -57,6 +58,7 @@ x-tasty: herbs`
assert.NoError(t, err)
assert.Equal(t, "herbs", n.FindExtension("x-tasty").Value)
assert.Equal(t, "https://pb33f.io/auth", n.Implicit.Value.AuthorizationUrl.Value)
assert.Len(t, n.GetExtensions(), 1)
}
func TestOAuthFlow_Build_Implicit_Fail(t *testing.T) {

View File

@@ -51,6 +51,11 @@ func (p *Parameter) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, p.Extensions)
}
// GetExtensions returns all extensions for Parameter.
func (p *Parameter) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return p.Extensions
}
// Build will extract examples, extensions and content/media types.
func (p *Parameter) Build(root *yaml.Node, idx *index.SpecIndex) error {
p.Extensions = low.ExtractExtensions(root)

View File

@@ -86,6 +86,7 @@ content:
ext := n.FindExtension("x-family-love").Value
assert.Equal(t, "strong", ext)
assert.Len(t, n.GetExtensions(), 1)
}
func TestParameter_Build_Success_Examples(t *testing.T) {

View File

@@ -99,6 +99,11 @@ func (p *PathItem) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, p.Extensions)
}
// GetExtensions returns all PathItem extensions and satisfies the low.HasExtensions interface.
func (p *PathItem) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return p.Extensions
}
// Build extracts extensions, parameters, servers and each http method defined.
// everything is extracted asynchronously for speed.
func (p *PathItem) Build(root *yaml.Node, idx *index.SpecIndex) error {

View File

@@ -79,4 +79,5 @@ summary: it's another path item`
// hash
assert.Equal(t, n.Hash(), n2.Hash())
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -50,6 +50,11 @@ func (p *Paths) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, p.Extensions)
}
// GetExtensions returns all Paths extensions and satisfies the low.HasExtensions interface.
func (p *Paths) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return p.Extensions
}
// Build will extract extensions and all PathItems. This happens asynchronously for speed.
func (p *Paths) Build(root *yaml.Node, idx *index.SpecIndex) error {
p.Extensions = low.ExtractExtensions(root)

View File

@@ -62,6 +62,7 @@ x-milk: cold`
assert.Len(t, path.Parameters.Value, 1)
assert.Equal(t, "cold", n.FindExtension("x-milk").Value)
assert.Equal(t, "hello", path.Parameters.Value[0].Value.Name.Value)
assert.Len(t, n.GetExtensions(), 1)
}
func TestPaths_Build_Fail(t *testing.T) {

View File

@@ -27,6 +27,11 @@ func (rb *RequestBody) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, rb.Extensions)
}
// GetExtensions returns all RequestBody extensions and satisfies the low.HasExtensions interface.
func (rb *RequestBody) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return rb.Extensions
}
// FindContent attempts to find content/MediaType defined using a specified name.
func (rb *RequestBody) FindContent(cType string) *low.ValueReference[*MediaType] {
return low.FindItemInMap[*MediaType](cType, rb.Content.Value)

View File

@@ -34,6 +34,7 @@ x-requesto: presto`
assert.True(t, n.Required.Value)
assert.Equal(t, "nice.", n.FindContent("fresh/fish").Value.Example.Value)
assert.Equal(t, "presto", n.FindExtension("x-requesto").Value)
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -31,6 +31,11 @@ func (r *Response) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, r.Extensions)
}
// GetExtensions returns all OAuthFlow extensions and satisfies the low.HasExtensions interface.
func (r *Response) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return r.Extensions
}
// FindContent will attempt to locate a MediaType instance using the supplied key.
func (r *Response) FindContent(cType string) *low.ValueReference[*MediaType] {
return low.FindItemInMap[*MediaType](cType, r.Content.Value)

View File

@@ -64,6 +64,7 @@ default:
assert.Equal(t, "c009b2046101bc03df802b4cf23f78176931137e6115bf7b445ca46856c06b51",
low.GenerateHashString(&n))
}
func TestResponses_NoDefault(t *testing.T) {
@@ -98,6 +99,9 @@ x-shoes: old`
assert.Equal(t, "54ab66e6cb8bd226940f421c2387e45215b84c946182435dfe2a3036043fa07c",
low.GenerateHashString(&n))
assert.Len(t, n.FindResponseByCode("200").Value.GetExtensions(), 1)
assert.Len(t, n.GetExtensions(), 1)
}
func TestResponses_Build_FailCodes_WrongType(t *testing.T) {

View File

@@ -38,6 +38,11 @@ type Responses struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all Responses extensions and satisfies the low.HasExtensions interface.
func (r *Responses) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return r.Extensions
}
// Build will extract default response and all Response objects for each code
func (r *Responses) Build(root *yaml.Node, idx *index.SpecIndex) error {
r.Extensions = low.ExtractExtensions(root)

View File

@@ -58,6 +58,11 @@ func (ss *SecurityScheme) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, ss.Extensions)
}
// GetExtensions returns all SecurityScheme extensions and satisfies the low.HasExtensions interface.
func (ss *SecurityScheme) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return ss.Extensions
}
// Build will extract OAuthFlows and extensions from the node.
func (ss *SecurityScheme) Build(root *yaml.Node, idx *index.SpecIndex) error {
ss.Extensions = low.ExtractExtensions(root)

View File

@@ -70,6 +70,7 @@ x-milk: please`
assert.Equal(t, "https://pb33f.io/openid", n.OpenIdConnectUrl.Value)
assert.Equal(t, "please", n.FindExtension("x-milk").Value)
assert.Equal(t, "https://pb33f.io", n.Flows.Value.Implicit.Value.TokenUrl.Value)
assert.Len(t, n.GetExtensions(), 1)
}

View File

@@ -22,6 +22,11 @@ type Server struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// GetExtensions returns all Paths extensions and satisfies the low.HasExtensions interface.
func (s *Server) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return s.Extensions
}
// FindVariable attempts to locate a ServerVariable instance using the supplied key.
func (s *Server) FindVariable(serverVar string) *low.ValueReference[*ServerVariable] {
return low.FindItemInMap[*ServerVariable](serverVar, s.Variables.Value)

View File

@@ -13,7 +13,8 @@ import (
func TestServer_Build(t *testing.T) {
yml := `url: https://pb33f.io
yml := `x-coffee: hot
url: https://pb33f.io
description: high quality software for developers.
variables:
var1:
@@ -45,6 +46,8 @@ variables:
assert.Equal(t, "00eef99ee4a7b746be7b4ccdece59c5a96222c6206f846fafed782c9f3f9b46b",
low.GenerateHashString(s.Value))
assert.Len(t, n.GetExtensions(), 1)
}
func TestServer_Build_NoVars(t *testing.T) {

View File

@@ -5,6 +5,9 @@ package libopenapi
import (
"fmt"
"github.com/pb33f/libopenapi/datamodel/high"
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"
@@ -534,3 +537,120 @@ components:
// Loop Point: #/components/schemas/Two
}
// 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
Frosting string
}
// 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
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
}