Working through mutation designs.

trying out some sketches to get the APIs correct.
This commit is contained in:
Dave Shanley
2023-03-01 11:29:03 -05:00
parent 69d99c7046
commit 441068174c
16 changed files with 416 additions and 40 deletions

View File

@@ -25,3 +25,17 @@ type DocumentConfiguration struct {
// AllowRemoteReferences will allow the index to lookup remote references. This is disabled by default.
AllowRemoteReferences bool
}
func NewOpenDocumentConfiguration() *DocumentConfiguration {
return &DocumentConfiguration{
AllowFileReferences: true,
AllowRemoteReferences: true,
}
}
func NewClosedDocumentConfiguration() *DocumentConfiguration {
return &DocumentConfiguration{
AllowFileReferences: false,
AllowRemoteReferences: false,
}
}

View File

@@ -4,17 +4,19 @@
package base
import (
"github.com/pb33f/libopenapi/datamodel/high"
low "github.com/pb33f/libopenapi/datamodel/low/base"
"gopkg.in/yaml.v3"
)
// Contact represents a high-level representation of the Contact definitions found at
// v2 - https://swagger.io/specification/v2/#contactObject
// v3 - https://spec.openapis.org/oas/v3.1.0#contact-object
type Contact struct {
Name string
URL string
Email string
low *low.Contact
Name string `json:"name,omitempty" yaml:"name,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
Email string `json:"email,omitempty" yaml:"email,omitempty"`
low *low.Contact `json:"-" yaml:"-"` // low-level representation
}
// NewContact will create a new Contact instance using a low-level Contact
@@ -31,3 +33,19 @@ func NewContact(contact *low.Contact) *Contact {
func (c *Contact) GoLow() *low.Contact {
return c.low
}
func (c *Contact) Render() ([]byte, error) {
return yaml.Marshal(c)
}
func (c *Contact) MarshalYAML() (interface{}, error) {
if c == nil {
return nil, nil
}
n := high.CreateEmptyMapNode()
high.AddYAMLNode(n, low.NameLabel, c.Name)
high.AddYAMLNode(n, low.URLLabel, c.URL)
high.AddYAMLNode(n, low.EmailLabel, c.Email)
return n, nil
}

View File

@@ -56,3 +56,25 @@ email: buckaroo@pb33f.io`
fmt.Print(highContact.Name)
// Output: Buckaroo
}
func TestContact_MarshalYAML(t *testing.T) {
highC := &Contact{Name: "dave", URL: "https://pb33f.io", Email: "dave@pb33f.io"}
dat, _ := highC.Render()
// unmarshal yaml into a *yaml.Node instance
var cNode yaml.Node
_ = yaml.Unmarshal(dat, &cNode)
// build low
var lowContact lowbase.Contact
_ = lowmodel.BuildModel(cNode.Content[0], &lowContact)
// build high
highContact := NewContact(&lowContact)
assert.Equal(t, "dave", highContact.Name)
assert.Equal(t, "dave@pb33f.io", highContact.Email)
assert.Equal(t, "https://pb33f.io", highContact.URL)
}

View File

@@ -6,6 +6,7 @@ package base
import (
"github.com/pb33f/libopenapi/datamodel/high"
low "github.com/pb33f/libopenapi/datamodel/low/base"
"gopkg.in/yaml.v3"
)
// Info represents a high-level Info object as defined by both OpenAPI 2 and OpenAPI 3.
@@ -16,13 +17,13 @@ import (
// v2 - https://swagger.io/specification/v2/#infoObject
// v3 - https://spec.openapis.org/oas/v3.1.0#info-object
type Info struct {
Title string
Summary string
Description string
TermsOfService string
Contact *Contact
License *License
Version string
Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"`
Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"`
License *License `json:"license,omitempty" yaml:"license,omitempty"`
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Extensions map[string]any
low *low.Info
}
@@ -62,3 +63,24 @@ func NewInfo(info *low.Info) *Info {
func (i *Info) GoLow() *low.Info {
return i.low
}
// Render will return a YAML representation of the Info object as a byte slice.
func (i *Info) Render() ([]byte, error) {
return yaml.Marshal(i)
}
// MarshalYAML will create a ready to render YAML representation of the Info object.
func (i *Info) MarshalYAML() (interface{}, error) {
if i == nil {
return nil, nil
}
n := high.CreateEmptyMapNode()
high.AddYAMLNode(n, low.TitleLabel, i.Title)
high.AddYAMLNode(n, low.DescriptionLabel, i.Description)
high.AddYAMLNode(n, low.TermsOfServiceLabel, i.TermsOfService)
high.AddYAMLNode(n, low.ContactLabel, i.Contact)
high.AddYAMLNode(n, low.LicenseLabel, i.License)
high.AddYAMLNode(n, low.VersionLabel, i.Version)
high.MarshalExtensions(n, i.Extensions)
return n, nil
}

View File

@@ -106,3 +106,50 @@ url: https://opensource.org/licenses/MIT`
fmt.Print(highLicense.Name)
// Output: MIT
}
func TestInfo_Render(t *testing.T) {
ext := make(map[string]any)
ext["x-pizza"] = "pepperoni"
highI := &Info{
Title: "hey",
Description: "there you",
TermsOfService: "have you got any money",
Contact: &Contact{
Name: "buckaroo",
Email: "buckaroo@pb33f.io",
},
License: &License{
Name: "MIT",
URL: "https://opensource.org/licenses/MIT",
},
Version: "1.2.3",
Extensions: ext,
}
dat, _ := highI.Render()
// unmarshal yaml into a *yaml.Node instance
var cNode yaml.Node
_ = yaml.Unmarshal(dat, &cNode)
// build low
var lowInfo lowbase.Info
_ = lowmodel.BuildModel(cNode.Content[0], &lowInfo)
_ = lowInfo.Build(cNode.Content[0], nil)
// build high
highInfo := NewInfo(&lowInfo)
assert.Equal(t, "hey", highInfo.Title)
assert.Equal(t, "there you", highInfo.Description)
assert.Equal(t, "have you got any money", highInfo.TermsOfService)
assert.Equal(t, "buckaroo", highInfo.Contact.Name)
assert.Equal(t, "buckaroo@pb33f.io", highInfo.Contact.Email)
assert.Equal(t, "MIT", highInfo.License.Name)
assert.Equal(t, "https://opensource.org/licenses/MIT", highInfo.License.URL)
assert.Equal(t, "1.2.3", highInfo.Version)
assert.Equal(t, "pepperoni", highInfo.Extensions["x-pizza"])
}

View File

@@ -0,0 +1,33 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
package base
import (
lowmodel "github.com/pb33f/libopenapi/datamodel/low"
lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
"testing"
)
func TestLicense_Render(t *testing.T) {
highL := &License{Name: "MIT", URL: "https://pb33f.io"}
dat, _ := highL.Render()
// unmarshal yaml into a *yaml.Node instance
var cNode yaml.Node
_ = yaml.Unmarshal(dat, &cNode)
// build low
var lowLicense lowbase.License
_ = lowmodel.BuildModel(cNode.Content[0], &lowLicense)
// build high
highLicense := NewLicense(&lowLicense)
assert.Equal(t, "MIT", highLicense.Name)
assert.Equal(t, "https://pb33f.io", highLicense.URL)
}

View File

@@ -4,15 +4,17 @@
package base
import (
"github.com/pb33f/libopenapi/datamodel/high"
low "github.com/pb33f/libopenapi/datamodel/low/base"
"gopkg.in/yaml.v3"
)
// License is a high-level representation of a License object as defined by OpenAPI 2 and OpenAPI 3
// v2 - https://swagger.io/specification/v2/#licenseObject
// v3 - https://spec.openapis.org/oas/v3.1.0#license-object
type License struct {
Name string
URL string
Name string `json:"name,omitempty" yaml:"name,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
low *low.License
}
@@ -33,3 +35,19 @@ func NewLicense(license *low.License) *License {
func (l *License) GoLow() *low.License {
return l.low
}
// Render will return a YAML representation of the License object as a byte slice.
func (l *License) Render() ([]byte, error) {
return yaml.Marshal(l)
}
// MarshalYAML will create a ready to render YAML representation of the License object.
func (l *License) MarshalYAML() (interface{}, error) {
if l == nil {
return nil, nil
}
n := high.CreateEmptyMapNode()
high.AddYAMLNode(n, low.NameLabel, l.Name)
high.AddYAMLNode(n, low.URLLabel, l.URL)
return n, nil
}

View File

@@ -13,7 +13,12 @@
// those models, things like key/value breakdown of each value, lines, column, source comments etc.
package high
import "github.com/pb33f/libopenapi/datamodel/low"
import (
"github.com/pb33f/libopenapi/datamodel/low"
"gopkg.in/yaml.v3"
"reflect"
"strconv"
)
// GoesLow is used to represent any high-level model. All high level models meet this interface and can be used to
// extract low-level models from any high-level model.
@@ -63,4 +68,75 @@ func UnpackExtensions[T any, R low.HasExtensions[T]](low GoesLow[R]) (map[string
m[key] = g
}
return m, nil
}
}
// MarshalExtensions is a convenience function that makes it easy and simple to marshal an objects extensions into a
// map that can then correctly rendered back down in to YAML.
func MarshalExtensions(parent *yaml.Node, extensions map[string]any) {
for k := range extensions {
AddYAMLNode(parent, k, extensions[k])
}
}
func AddYAMLNode(parent *yaml.Node, key string, value any) *yaml.Node {
if value == nil {
return parent
}
// check the type
t := reflect.TypeOf(value)
l := CreateStringNode(key)
var valueNode *yaml.Node
switch t.Kind() {
case reflect.String:
if value.(string) == "" {
return parent
}
valueNode = CreateStringNode(value.(string))
case reflect.Int:
valueNode = CreateIntNode(value.(int))
case reflect.Struct:
panic("no way dude")
case reflect.Ptr:
rawRender, _ := value.(Renderable).MarshalYAML()
if rawRender != nil {
valueNode = rawRender.(*yaml.Node)
} else {
return parent
}
}
parent.Content = append(parent.Content, l, valueNode)
return parent
}
func CreateEmptyMapNode() *yaml.Node {
n := &yaml.Node{
Kind: yaml.MappingNode,
Tag: "!!map",
}
return n
}
func CreateStringNode(str string) *yaml.Node {
n := &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: str,
}
return n
}
func CreateIntNode(val int) *yaml.Node {
i := strconv.Itoa(val)
n := &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!int",
Value: i,
}
return n
}
type Renderable interface {
MarshalYAML() (interface{}, error)
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/pb33f/libopenapi/datamodel/high/base"
low "github.com/pb33f/libopenapi/datamodel/low/v3"
"github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3"
)
// Document represents a high-level OpenAPI 3 document (both 3.0 & 3.1). A Document is the root of the specification.
@@ -121,27 +122,50 @@ func NewDocument(document *low.Document) *Document {
if !document.Paths.IsEmpty() {
d.Paths = NewPaths(document.Paths.Value)
}
if !document.JsonSchemaDialect.IsEmpty() {
d.JsonSchemaDialect = document.JsonSchemaDialect.Value
}
if !document.Webhooks.IsEmpty() {
hooks := make(map[string]*PathItem)
for h := range document.Webhooks.Value {
hooks[h.Value] = NewPathItem(document.Webhooks.Value[h].Value)
}
d.Webhooks = hooks
}
if !document.Security.IsEmpty() {
var security []*base.SecurityRequirement
for s := range document.Security.Value {
security = append(security, base.NewSecurityRequirement(document.Security.Value[s].Value))
}
d.Security = security
}
return d
if !document.JsonSchemaDialect.IsEmpty() {
d.JsonSchemaDialect = document.JsonSchemaDialect.Value
}
if !document.Webhooks.IsEmpty() {
hooks := make(map[string]*PathItem)
for h := range document.Webhooks.Value {
hooks[h.Value] = NewPathItem(document.Webhooks.Value[h].Value)
}
d.Webhooks = hooks
}
if !document.Security.IsEmpty() {
var security []*base.SecurityRequirement
for s := range document.Security.Value {
security = append(security, base.NewSecurityRequirement(document.Security.Value[s].Value))
}
d.Security = security
}
return d
}
// GoLow returns the low-level Document that was used to create the high level one.
func (d *Document) GoLow() *low.Document {
return d.low
}
// Render will return a YAML representation of the Document object as a byte slice.
func (d *Document) Render() ([]byte, error) {
return yaml.Marshal(d)
}
// MarshalYAML will create a ready to render YAML representation of the Document object.
func (d *Document) MarshalYAML() (interface{}, error) {
n := high.CreateEmptyMapNode()
high.AddYAMLNode(n, low.SchemaDialectLabel, d.JsonSchemaDialect)
high.AddYAMLNode(n, low.OpenAPILabel, d.Version)
high.AddYAMLNode(n, low.InfoLabel, d.Info)
//high.AddYAMLNode(n, low.TagsLabel, d.Tags)
//high.AddYAMLNode(n, low.ServersLabel, d.Servers)
//high.AddYAMLNode(n, low.SecurityLabel, d.Security)
//high.AddYAMLNode(n, low.ServersLabel, d.Servers)
//high.AddYAMLNode(n, low.ExternalDocsLabel, d.ExternalDocs)
//high.AddYAMLNode(n, low.PathsLabel, d.Paths)
//high.AddYAMLNode(n, low.ComponentsLabel, d.Components)
//high.AddYAMLNode(n, low.WebhooksLabel, d.Webhooks)
high.MarshalExtensions(n, d.Extensions)
return n, nil
}

View File

@@ -473,3 +473,25 @@ func TestCircularReferencesDoc(t *testing.T) {
assert.Len(t, d.Components.Schemas, 9)
assert.Len(t, d.Index.GetCircularReferences(), 3)
}
func TestDocument_MarshalYAML(t *testing.T) {
// create a new document
initTest()
h := NewDocument(lowDoc)
// render the document to YAML
r, _ := h.Render()
info, _ := datamodel.ExtractSpecInfo(r)
lowDoc, _ = lowv3.CreateDocumentFromConfig(info, datamodel.NewOpenDocumentConfiguration())
highDoc := NewDocument(lowDoc)
assert.Equal(t, "3.1.0", highDoc.Version)
// TODO: COMPLETE THIS
}

View File

@@ -5,6 +5,13 @@ package base
// Constants for labels used to look up values within OpenAPI specifications.
const (
VersionLabel = "version"
TermsOfServiceLabel = "termsOfService"
DescriptionLabel = "description"
TitleLabel = "title"
EmailLabel = "email"
NameLabel = "name"
URLLabel = "url"
TagsLabel = "tags"
ExternalDocsLabel = "externalDocs"
ExamplesLabel = "examples"

View File

@@ -40,3 +40,4 @@ func (c *Contact) Hash() [32]byte {
}
return sha256.Sum256([]byte(strings.Join(f, "|")))
}

View File

@@ -43,6 +43,7 @@ type HasExtensions[T any] interface {
type HasValue[T any] interface {
GetValue() T
GetValueNode() *yaml.Node
IsEmpty() bool
*T
}

View File

@@ -0,0 +1,4 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
package low

View File

@@ -14,6 +14,7 @@
package libopenapi
import (
"errors"
"fmt"
"github.com/pb33f/libopenapi/index"
@@ -55,19 +56,39 @@ type Document interface {
// any other types.
BuildV3Model() (*DocumentModel[v3high.Document], []error)
// RenderAndReload will render the high level model as it currently exists (including any mutations, additions
// and removals to and from any object in the tree). It will then reload the low level model with the new bytes
// extracted from the model that was re-rendered. This is useful if you want to make changes to the high level model
// and then 'reload' the model into memory, so that line numbers and column numbers are correct and all update
// according to the changes made.
//
// The method returns the raw YAML bytes that were rendered, and any errors that occurred during rebuilding of the model.
// This is a destructive operation, and will re-build the entire model from scratch using the new bytes, so any
// references to the old model will be lost. The second return is the new Document that was created, and the third
// return is any errors hit trying to re-render.
//
// **IMPORTANT** This method only supports OpenAPI Documents. The Swagger model will not support mutations correctly
// and will not update when called. This choice has been made because we don't want to continue supporting Swagger,
// it's too old, so it should be motivation to upgrade to OpenAPI 3.
RenderAndReload() ([]byte, *Document, *DocumentModel[v3high.Document], []error)
// Serialize will re-render a Document back into a []byte slice. If any modifications have been made to the
// underlying data model using low level APIs, then those changes will be reflected in the serialized output.
//
// It's important to know that this should not be used if the resolver has been used on a specification to
// for anything other than checking for circular references. If the resolver is used to resolve the spec, then this
// method may spin out forever if the specification backing the model has circular references.
// Deprecated: This method is deprecated and will be removed in a future release. Use RenderAndReload() instead.
// This method does not support mutations correctly.
Serialize() ([]byte, error)
}
type document struct {
version string
info *datamodel.SpecInfo
config *datamodel.DocumentConfiguration
version string
info *datamodel.SpecInfo
config *datamodel.DocumentConfiguration
highOpenAPI3Model *DocumentModel[v3high.Document]
highSwaggerModel *DocumentModel[v2high.Swagger]
}
// DocumentModel represents either a Swagger document (version 2) or an OpenAPI document (version 3) that is
@@ -135,7 +156,31 @@ func (d *document) Serialize() ([]byte, error) {
}
}
func (d *document) RenderAndReload() ([]byte, *Document, *DocumentModel[v3high.Document], []error) {
if d.highSwaggerModel != nil && d.highOpenAPI3Model == nil {
return nil, nil, nil, []error{errors.New("this method only supports OpenAPI 3 documents, not Swagger")}
}
newBytes, err := d.highOpenAPI3Model.Model.Render()
if err != nil {
return newBytes, nil, nil, []error{err}
}
newDoc, err := NewDocument(newBytes)
if err != nil {
return newBytes, &newDoc, nil, []error{err}
}
// build the model.
model, errs := newDoc.BuildV3Model()
if errs != nil {
return newBytes, &newDoc, model, errs
}
// this document is now dead, long live the new document!
return newBytes, &newDoc, model, nil
}
func (d *document) BuildV2Model() (*DocumentModel[v2high.Swagger], []error) {
if d.highSwaggerModel != nil {
return d.highSwaggerModel, nil
}
var errors []error
if d.info == nil {
errors = append(errors, fmt.Errorf("unable to build swagger document, no specification has been loaded"))
@@ -168,13 +213,17 @@ func (d *document) BuildV2Model() (*DocumentModel[v2high.Swagger], []error) {
}
}
highDoc := v2high.NewSwaggerDocument(lowDoc)
return &DocumentModel[v2high.Swagger]{
d.highSwaggerModel = &DocumentModel[v2high.Swagger]{
Model: *highDoc,
Index: lowDoc.Index,
}, errors
}
return d.highSwaggerModel, errors
}
func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) {
if d.highOpenAPI3Model != nil {
return d.highOpenAPI3Model, nil
}
var errors []error
if d.info == nil {
errors = append(errors, fmt.Errorf("unable to build document, no specification has been loaded"))
@@ -207,10 +256,11 @@ func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) {
}
}
highDoc := v3high.NewDocument(lowDoc)
return &DocumentModel[v3high.Document]{
d.highOpenAPI3Model = &DocumentModel[v3high.Document]{
Model: *highDoc,
Index: lowDoc.Index,
}, errors
}
return d.highOpenAPI3Model, errors
}
// CompareDocuments will accept a left and right Document implementing struct, build a model for the correct

View File

@@ -157,6 +157,23 @@ info:
assert.Equal(t, ymlModified, string(serial))
}
func TestDocument_RenderAndReload(t *testing.T) {
yml := `openapi: 3.0
info:
title: The magic API
`
doc, _ := NewDocument([]byte(yml))
v3Doc, _ := doc.BuildV3Model()
v3Doc.Model.Info.Title = "The magic API - but now, altered!"
bytes, _, newDocModel, err := doc.RenderAndReload()
assert.Nil(t, err)
assert.NotNil(t, bytes)
assert.Equal(t, "The magic API - but now, altered!",
newDocModel.Model.Info.Title)
}
func TestDocument_Serialize_JSON_Modified(t *testing.T) {
json := `{ 'openapi': '3.0',