Added new Render method to top level document.

Like `RenderAndReload` this method will render / print out the document, however it won’t read it back in and reload the model from the ground up. This is a non-destructive method that does not reload the model from the rendered document.

If you don’t use the low model or you don’t care that the high level model and low-level models are out of sync? No problem. Otherwise keep this in mind that the low level model will only represent the original un-mutated document, as will the index.

Signed-off-by: quobix <dave@quobix.com>
This commit is contained in:
quobix
2023-09-13 07:06:15 -04:00
parent c700c19849
commit 43bf162e0c
6 changed files with 798 additions and 674 deletions

View File

@@ -34,7 +34,7 @@ x-cli-name: pizza cli`
err := low.BuildModel(idxNode.Content[0], &n) err := low.BuildModel(idxNode.Content[0], &n)
assert.NoError(t, err) assert.NoError(t, err)
err = n.Build(nil,idxNode.Content[0], idx) err = n.Build(nil, idxNode.Content[0], idx)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pizza", n.Title.Value) assert.Equal(t, "pizza", n.Title.Value)

View File

@@ -72,6 +72,21 @@ type Document interface {
// it's too old, so it should be motivation to upgrade to OpenAPI 3. // it's too old, so it should be motivation to upgrade to OpenAPI 3.
RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], []error) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], []error)
// Render will render the high level model as it currently exists (including any mutations, additions
// and removals to and from any object in the tree). Unlike RenderAndReload, Render will simply print the state
// of the model as it currently exists, and will not re-load the model into memory. It means that the low-level and
// the high-level models will be out of sync, and the index will only be useful for the original document.
//
// Why use this instead of RenderAndReload?
//
// The simple answer is that RenderAndReload is a destructive operation, and will re-build the entire model from
// scratch using the new bytes, which is desirable 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 the index is accurate.
// However, if you don't care about the low-level model, and you're not using the index, and you just want to
// print the state of the model as it currently exists, then Render() is the method to use.
// **IMPORTANT** This method only supports OpenAPI Documents.
Render() ([]byte, error)
// Serialize will re-render a Document back into a []byte slice. If any modifications have been made to the // 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. // underlying data model using low level APIs, then those changes will be reflected in the serialized output.
// //
@@ -168,14 +183,32 @@ func (d *document) Serialize() ([]byte, error) {
} }
func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], []error) { func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Document], []error) {
newBytes, rerr := d.Render()
if rerr != nil {
return nil, nil, nil, []error{rerr}
}
newDoc, err := NewDocumentWithConfiguration(newBytes, d.config)
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) Render() ([]byte, error) {
if d.highSwaggerModel != nil && d.highOpenAPI3Model == nil { if d.highSwaggerModel != nil && d.highOpenAPI3Model == nil {
return nil, nil, nil, []error{errors.New("this method only supports OpenAPI 3 documents, not Swagger")} return nil, errors.New("this method only supports OpenAPI 3 documents, not Swagger")
} }
var newBytes []byte var newBytes []byte
// render the model as the correct type based on the source.
// https://github.com/pb33f/libopenapi/issues/105
if d.info.SpecFileType == datamodel.JSONFileType { if d.info.SpecFileType == datamodel.JSONFileType {
jsonIndent := " " jsonIndent := " "
i := d.info.OriginalIndentation i := d.info.OriginalIndentation
@@ -190,17 +223,7 @@ func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Do
newBytes = d.highOpenAPI3Model.Model.RenderWithIndention(d.info.OriginalIndentation) newBytes = d.highOpenAPI3Model.Model.RenderWithIndention(d.info.OriginalIndentation)
} }
newDoc, err := NewDocumentWithConfiguration(newBytes, d.config) return newBytes, nil
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) { func (d *document) BuildV2Model() (*DocumentModel[v2high.Swagger], []error) {

View File

@@ -8,7 +8,6 @@ import (
"github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/pb33f/libopenapi/what-changed/model" "github.com/pb33f/libopenapi/what-changed/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"io/ioutil"
"os" "os"
"strings" "strings"
"testing" "testing"
@@ -230,7 +229,7 @@ func TestDocument_RenderAndReload_ChangeCheck_Asana(t *testing.T) {
func TestDocument_RenderAndReload(t *testing.T) { func TestDocument_RenderAndReload(t *testing.T) {
// load an OpenAPI 3 specification from bytes // load an OpenAPI 3 specification from bytes
petstore, _ := ioutil.ReadFile("test_specs/petstorev3.json") petstore, _ := os.ReadFile("test_specs/petstorev3.json")
// create a new document from specification bytes // create a new document from specification bytes
doc, err := NewDocument(petstore) doc, err := NewDocument(petstore)
@@ -276,10 +275,101 @@ func TestDocument_RenderAndReload(t *testing.T) {
assert.Equal(t, "https://pb33f.io", assert.Equal(t, "https://pb33f.io",
h.Components.SecuritySchemes["petstore_auth"].Flows.Implicit.AuthorizationUrl) h.Components.SecuritySchemes["petstore_auth"].Flows.Implicit.AuthorizationUrl)
}
func TestDocument_Render(t *testing.T) {
// load an OpenAPI 3 specification from bytes
petstore, _ := os.ReadFile("test_specs/petstorev3.json")
// create a new document from specification bytes
doc, err := NewDocument(petstore)
// if anything went wrong, an error is thrown
if err != nil {
panic(fmt.Sprintf("cannot create new document: %e", err))
}
// because we know this is a v3 spec, we can build a ready to go model from it.
m, _ := doc.BuildV3Model()
// mutate the model
h := m.Model
h.Paths.PathItems["/pet/findByStatus"].Get.OperationId = "findACakeInABakery"
h.Paths.PathItems["/pet/findByStatus"].Get.Responses.Codes["400"].Description = "a nice bucket of mice"
h.Paths.PathItems["/pet/findByTags"].Get.Tags =
append(h.Paths.PathItems["/pet/findByTags"].Get.Tags, "gurgle", "giggle")
h.Paths.PathItems["/pet/{petId}"].Delete.Security = append(h.Paths.PathItems["/pet/{petId}"].Delete.Security,
&base.SecurityRequirement{Requirements: map[string][]string{
"pizza-and-cake": {"read:abook", "write:asong"},
}})
h.Components.Schemas["Order"].Schema().Properties["status"].Schema().Example = "I am a teapot, filled with love."
h.Components.SecuritySchemes["petstore_auth"].Flows.Implicit.AuthorizationUrl = "https://pb33f.io"
bytes, e := doc.Render()
assert.NoError(t, e)
assert.NotNil(t, bytes)
newDoc, docErr := NewDocument(bytes)
assert.NoError(t, docErr)
newDocModel, docErrs := newDoc.BuildV3Model()
assert.Len(t, docErrs, 0)
h = newDocModel.Model
assert.Equal(t, "findACakeInABakery", h.Paths.PathItems["/pet/findByStatus"].Get.OperationId)
assert.Equal(t, "a nice bucket of mice",
h.Paths.PathItems["/pet/findByStatus"].Get.Responses.Codes["400"].Description)
assert.Len(t, h.Paths.PathItems["/pet/findByTags"].Get.Tags, 3)
assert.Len(t, h.Paths.PathItems["/pet/findByTags"].Get.Tags, 3)
yu := h.Paths.PathItems["/pet/{petId}"].Delete.Security
assert.Equal(t, "read:abook", yu[len(yu)-1].Requirements["pizza-and-cake"][0])
assert.Equal(t, "I am a teapot, filled with love.",
h.Components.Schemas["Order"].Schema().Properties["status"].Schema().Example)
assert.Equal(t, "https://pb33f.io",
h.Components.SecuritySchemes["petstore_auth"].Flows.Implicit.AuthorizationUrl)
}
func TestDocument_RenderWithLargeIndention(t *testing.T) {
json := `{
"openapi": "3.0"
}`
doc, _ := NewDocument([]byte(json))
doc.BuildV3Model()
bytes, _ := doc.Render()
assert.Equal(t, json, string(bytes))
} }
func TestDocument_Render_ChangeCheck_Burgershop(t *testing.T) {
bs, _ := os.ReadFile("test_specs/burgershop.openapi.yaml")
doc, _ := NewDocument(bs)
doc.BuildV3Model()
rend, _ := doc.Render()
newDoc, _ := NewDocument(rend)
// compare documents
compReport, errs := CompareDocuments(doc, newDoc)
// should noth be nil.
assert.Nil(t, errs)
assert.NotNil(t, rend)
assert.Nil(t, compReport)
}
func TestDocument_RenderAndReload_Swagger(t *testing.T) { func TestDocument_RenderAndReload_Swagger(t *testing.T) {
petstore, _ := ioutil.ReadFile("test_specs/petstorev2.json") petstore, _ := os.ReadFile("test_specs/petstorev2.json")
doc, _ := NewDocument(petstore) doc, _ := NewDocument(petstore)
doc.BuildV2Model() doc.BuildV2Model()
doc.BuildV2Model() doc.BuildV2Model()
@@ -289,8 +379,19 @@ func TestDocument_RenderAndReload_Swagger(t *testing.T) {
} }
func TestDocument_Render_Swagger(t *testing.T) {
petstore, _ := os.ReadFile("test_specs/petstorev2.json")
doc, _ := NewDocument(petstore)
doc.BuildV2Model()
doc.BuildV2Model()
_, e := doc.Render()
assert.Error(t, e)
assert.Equal(t, "this method only supports OpenAPI 3 documents, not Swagger", e.Error())
}
func TestDocument_BuildModelPreBuild(t *testing.T) { func TestDocument_BuildModelPreBuild(t *testing.T) {
petstore, _ := ioutil.ReadFile("test_specs/petstorev3.json") petstore, _ := os.ReadFile("test_specs/petstorev3.json")
doc, e := NewDocument(petstore) doc, e := NewDocument(petstore)
assert.NoError(t, e) assert.NoError(t, e)
doc.BuildV3Model() doc.BuildV3Model()
@@ -314,7 +415,7 @@ func TestDocument_AnyDocWithConfig(t *testing.T) {
} }
func TestDocument_BuildModelCircular(t *testing.T) { func TestDocument_BuildModelCircular(t *testing.T) {
petstore, _ := ioutil.ReadFile("test_specs/circular-tests.yaml") petstore, _ := os.ReadFile("test_specs/circular-tests.yaml")
doc, _ := NewDocument(petstore) doc, _ := NewDocument(petstore)
m, e := doc.BuildV3Model() m, e := doc.BuildV3Model()
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -322,7 +423,7 @@ func TestDocument_BuildModelCircular(t *testing.T) {
} }
func TestDocument_BuildModelBad(t *testing.T) { func TestDocument_BuildModelBad(t *testing.T) {
petstore, _ := ioutil.ReadFile("test_specs/badref-burgershop.openapi.yaml") petstore, _ := os.ReadFile("test_specs/badref-burgershop.openapi.yaml")
doc, _ := NewDocument(petstore) doc, _ := NewDocument(petstore)
m, e := doc.BuildV3Model() m, e := doc.BuildV3Model()
assert.Nil(t, m) assert.Nil(t, m)
@@ -387,8 +488,8 @@ paths:
} }
func TestDocument_BuildModel_CompareDocsV3_LeftError(t *testing.T) { func TestDocument_BuildModel_CompareDocsV3_LeftError(t *testing.T) {
burgerShopOriginal, _ := ioutil.ReadFile("test_specs/badref-burgershop.openapi.yaml") burgerShopOriginal, _ := os.ReadFile("test_specs/badref-burgershop.openapi.yaml")
burgerShopUpdated, _ := ioutil.ReadFile("test_specs/burgershop.openapi-modified.yaml") burgerShopUpdated, _ := os.ReadFile("test_specs/burgershop.openapi-modified.yaml")
originalDoc, _ := NewDocument(burgerShopOriginal) originalDoc, _ := NewDocument(burgerShopOriginal)
updatedDoc, _ := NewDocument(burgerShopUpdated) updatedDoc, _ := NewDocument(burgerShopUpdated)
changes, errors := CompareDocuments(originalDoc, updatedDoc) changes, errors := CompareDocuments(originalDoc, updatedDoc)
@@ -398,8 +499,8 @@ func TestDocument_BuildModel_CompareDocsV3_LeftError(t *testing.T) {
func TestDocument_BuildModel_CompareDocsV3_RightError(t *testing.T) { func TestDocument_BuildModel_CompareDocsV3_RightError(t *testing.T) {
burgerShopOriginal, _ := ioutil.ReadFile("test_specs/badref-burgershop.openapi.yaml") burgerShopOriginal, _ := os.ReadFile("test_specs/badref-burgershop.openapi.yaml")
burgerShopUpdated, _ := ioutil.ReadFile("test_specs/burgershop.openapi-modified.yaml") burgerShopUpdated, _ := os.ReadFile("test_specs/burgershop.openapi-modified.yaml")
originalDoc, _ := NewDocument(burgerShopOriginal) originalDoc, _ := NewDocument(burgerShopOriginal)
updatedDoc, _ := NewDocument(burgerShopUpdated) updatedDoc, _ := NewDocument(burgerShopUpdated)
changes, errors := CompareDocuments(updatedDoc, originalDoc) changes, errors := CompareDocuments(updatedDoc, originalDoc)
@@ -410,8 +511,8 @@ func TestDocument_BuildModel_CompareDocsV3_RightError(t *testing.T) {
func TestDocument_BuildModel_CompareDocsV2_Error(t *testing.T) { func TestDocument_BuildModel_CompareDocsV2_Error(t *testing.T) {
burgerShopOriginal, _ := ioutil.ReadFile("test_specs/petstorev2-badref.json") burgerShopOriginal, _ := os.ReadFile("test_specs/petstorev2-badref.json")
burgerShopUpdated, _ := ioutil.ReadFile("test_specs/petstorev2-badref.json") burgerShopUpdated, _ := os.ReadFile("test_specs/petstorev2-badref.json")
originalDoc, _ := NewDocument(burgerShopOriginal) originalDoc, _ := NewDocument(burgerShopOriginal)
updatedDoc, _ := NewDocument(burgerShopUpdated) updatedDoc, _ := NewDocument(burgerShopUpdated)
changes, errors := CompareDocuments(updatedDoc, originalDoc) changes, errors := CompareDocuments(updatedDoc, originalDoc)
@@ -422,8 +523,8 @@ func TestDocument_BuildModel_CompareDocsV2_Error(t *testing.T) {
func TestDocument_BuildModel_CompareDocsV2V3Mix_Error(t *testing.T) { func TestDocument_BuildModel_CompareDocsV2V3Mix_Error(t *testing.T) {
burgerShopOriginal, _ := ioutil.ReadFile("test_specs/petstorev2.json") burgerShopOriginal, _ := os.ReadFile("test_specs/petstorev2.json")
burgerShopUpdated, _ := ioutil.ReadFile("test_specs/petstorev3.json") burgerShopUpdated, _ := os.ReadFile("test_specs/petstorev3.json")
originalDoc, _ := NewDocument(burgerShopOriginal) originalDoc, _ := NewDocument(burgerShopOriginal)
updatedDoc, _ := NewDocument(burgerShopUpdated) updatedDoc, _ := NewDocument(burgerShopUpdated)
changes, errors := CompareDocuments(updatedDoc, originalDoc) changes, errors := CompareDocuments(updatedDoc, originalDoc)
@@ -433,7 +534,7 @@ func TestDocument_BuildModel_CompareDocsV2V3Mix_Error(t *testing.T) {
} }
func TestSchemaRefIsFollowed(t *testing.T) { func TestSchemaRefIsFollowed(t *testing.T) {
petstore, _ := ioutil.ReadFile("test_specs/ref-followed.yaml") petstore, _ := os.ReadFile("test_specs/ref-followed.yaml")
// create a new document from specification bytes // create a new document from specification bytes
document, err := NewDocument(petstore) document, err := NewDocument(petstore)

View File

@@ -4,86 +4,86 @@
package index package index
import ( import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/pb33f/libopenapi/utils" "github.com/pb33f/libopenapi/utils"
"github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// FindComponent will locate a component by its reference, returns nil if nothing is found. // FindComponent will locate a component by its reference, returns nil if nothing is found.
// This method will recurse through remote, local and file references. For each new external reference // This method will recurse through remote, local and file references. For each new external reference
// a new index will be created. These indexes can then be traversed recursively. // a new index will be created. These indexes can then be traversed recursively.
func (index *SpecIndex) FindComponent(componentId string, parent *yaml.Node) *Reference { func (index *SpecIndex) FindComponent(componentId string, parent *yaml.Node) *Reference {
if index.root == nil { if index.root == nil {
return nil return nil
} }
remoteLookup := func(id string) (*yaml.Node, *yaml.Node, error) { remoteLookup := func(id string) (*yaml.Node, *yaml.Node, error) {
if index.config.AllowRemoteLookup { if index.config.AllowRemoteLookup {
return index.lookupRemoteReference(id) return index.lookupRemoteReference(id)
} else { } else {
return nil, nil, fmt.Errorf("remote lookups are not permitted, " + return nil, nil, fmt.Errorf("remote lookups are not permitted, " +
"please set AllowRemoteLookup to true in the configuration") "please set AllowRemoteLookup to true in the configuration")
} }
} }
fileLookup := func(id string) (*yaml.Node, *yaml.Node, error) { fileLookup := func(id string) (*yaml.Node, *yaml.Node, error) {
if index.config.AllowFileLookup { if index.config.AllowFileLookup {
return index.lookupFileReference(id) return index.lookupFileReference(id)
} else { } else {
return nil, nil, fmt.Errorf("local lookups are not permitted, " + return nil, nil, fmt.Errorf("local lookups are not permitted, " +
"please set AllowFileLookup to true in the configuration") "please set AllowFileLookup to true in the configuration")
} }
} }
switch DetermineReferenceResolveType(componentId) { switch DetermineReferenceResolveType(componentId) {
case LocalResolve: // ideally, every single ref in every single spec is local. however, this is not the case. case LocalResolve: // ideally, every single ref in every single spec is local. however, this is not the case.
return index.FindComponentInRoot(componentId) return index.FindComponentInRoot(componentId)
case HttpResolve: case HttpResolve:
uri := strings.Split(componentId, "#") uri := strings.Split(componentId, "#")
if len(uri) >= 2 { if len(uri) >= 2 {
return index.performExternalLookup(uri, componentId, remoteLookup, parent) return index.performExternalLookup(uri, componentId, remoteLookup, parent)
} }
if len(uri) == 1 { if len(uri) == 1 {
// if there is no reference, second segment is empty / has no name // if there is no reference, second segment is empty / has no name
// this means there is no component to look-up and the entire file should be pulled in. // this means there is no component to look-up and the entire file should be pulled in.
// to stop all the other code from breaking (that is expecting a component), let's just post-pend // to stop all the other code from breaking (that is expecting a component), let's just post-pend
// a hash to the end of the componentId and ensure the uri slice is as expected. // a hash to the end of the componentId and ensure the uri slice is as expected.
// described in https://github.com/pb33f/libopenapi/issues/37 // described in https://github.com/pb33f/libopenapi/issues/37
componentId = fmt.Sprintf("%s#", componentId) componentId = fmt.Sprintf("%s#", componentId)
uri = append(uri, "") uri = append(uri, "")
return index.performExternalLookup(uri, componentId, remoteLookup, parent) return index.performExternalLookup(uri, componentId, remoteLookup, parent)
} }
case FileResolve: case FileResolve:
uri := strings.Split(componentId, "#") uri := strings.Split(componentId, "#")
if len(uri) == 2 { if len(uri) == 2 {
return index.performExternalLookup(uri, componentId, fileLookup, parent) return index.performExternalLookup(uri, componentId, fileLookup, parent)
} }
if len(uri) == 1 { if len(uri) == 1 {
// if there is no reference, second segment is empty / has no name // if there is no reference, second segment is empty / has no name
// this means there is no component to look-up and the entire file should be pulled in. // this means there is no component to look-up and the entire file should be pulled in.
// to stop all the other code from breaking (that is expecting a component), let's just post-pend // to stop all the other code from breaking (that is expecting a component), let's just post-pend
// a hash to the end of the componentId and ensure the uri slice is as expected. // a hash to the end of the componentId and ensure the uri slice is as expected.
// described in https://github.com/pb33f/libopenapi/issues/37 // described in https://github.com/pb33f/libopenapi/issues/37
// //
// ^^ this same issue was re-reported in file based lookups in vacuum. // ^^ this same issue was re-reported in file based lookups in vacuum.
// more info here: https://github.com/daveshanley/vacuum/issues/225 // more info here: https://github.com/daveshanley/vacuum/issues/225
componentId = fmt.Sprintf("%s#", componentId) componentId = fmt.Sprintf("%s#", componentId)
uri = append(uri, "") uri = append(uri, "")
return index.performExternalLookup(uri, componentId, fileLookup, parent) return index.performExternalLookup(uri, componentId, fileLookup, parent)
} }
} }
return nil return nil
} }
var httpClient = &http.Client{Timeout: time.Duration(60) * time.Second} var httpClient = &http.Client{Timeout: time.Duration(60) * time.Second}
@@ -91,367 +91,367 @@ var httpClient = &http.Client{Timeout: time.Duration(60) * time.Second}
type RemoteURLHandler = func(url string) (*http.Response, error) type RemoteURLHandler = func(url string) (*http.Response, error)
func getRemoteDoc(g RemoteURLHandler, u string, d chan []byte, e chan error) { func getRemoteDoc(g RemoteURLHandler, u string, d chan []byte, e chan error) {
resp, err := g(u) resp, err := g(u)
if err != nil { if err != nil {
e <- err e <- err
close(e) close(e)
close(d) close(d)
return return
} }
var body []byte var body []byte
body, _ = io.ReadAll(resp.Body) body, _ = io.ReadAll(resp.Body)
d <- body d <- body
close(e) close(e)
close(d) close(d)
} }
func (index *SpecIndex) lookupRemoteReference(ref string) (*yaml.Node, *yaml.Node, error) { func (index *SpecIndex) lookupRemoteReference(ref string) (*yaml.Node, *yaml.Node, error) {
// split string to remove file reference // split string to remove file reference
uri := strings.Split(ref, "#") uri := strings.Split(ref, "#")
// have we already seen this remote source? // have we already seen this remote source?
var parsedRemoteDocument *yaml.Node var parsedRemoteDocument *yaml.Node
alreadySeen, foundDocument := index.CheckForSeenRemoteSource(uri[0]) alreadySeen, foundDocument := index.CheckForSeenRemoteSource(uri[0])
if alreadySeen { if alreadySeen {
parsedRemoteDocument = foundDocument parsedRemoteDocument = foundDocument
} else { } else {
d := make(chan bool) d := make(chan bool)
var body []byte var body []byte
var err error var err error
go func(uri string) { go func(uri string) {
bc := make(chan []byte) bc := make(chan []byte)
ec := make(chan error) ec := make(chan error)
var getter RemoteURLHandler = httpClient.Get var getter RemoteURLHandler = httpClient.Get
if index.config != nil && index.config.RemoteURLHandler != nil { if index.config != nil && index.config.RemoteURLHandler != nil {
getter = index.config.RemoteURLHandler getter = index.config.RemoteURLHandler
} }
// if we have a remote handler, use it instead of the default. // if we have a remote handler, use it instead of the default.
if index.config != nil && index.config.FSHandler != nil { if index.config != nil && index.config.FSHandler != nil {
go func() { go func() {
remoteFS := index.config.FSHandler remoteFS := index.config.FSHandler
remoteFile, rErr := remoteFS.Open(uri) remoteFile, rErr := remoteFS.Open(uri)
if rErr != nil { if rErr != nil {
e := fmt.Errorf("unable to open remote file: %s", rErr) e := fmt.Errorf("unable to open remote file: %s", rErr)
ec <- e ec <- e
return return
} }
b, ioErr := io.ReadAll(remoteFile) b, ioErr := io.ReadAll(remoteFile)
if ioErr != nil { if ioErr != nil {
e := fmt.Errorf("unable to read remote file bytes: %s", ioErr) e := fmt.Errorf("unable to read remote file bytes: %s", ioErr)
ec <- e ec <- e
return return
} }
bc <- b bc <- b
}() }()
} else { } else {
go getRemoteDoc(getter, uri, bc, ec) go getRemoteDoc(getter, uri, bc, ec)
} }
select { select {
case v := <-bc: case v := <-bc:
body = v body = v
break break
case er := <-ec: case er := <-ec:
err = er err = er
break break
} }
if len(body) > 0 { if len(body) > 0 {
var remoteDoc yaml.Node var remoteDoc yaml.Node
er := yaml.Unmarshal(body, &remoteDoc) er := yaml.Unmarshal(body, &remoteDoc)
if er != nil { if er != nil {
err = er err = er
d <- true d <- true
return return
} }
parsedRemoteDocument = &remoteDoc parsedRemoteDocument = &remoteDoc
if index.config != nil { if index.config != nil {
index.config.seenRemoteSources.Store(uri, &remoteDoc) index.config.seenRemoteSources.Store(uri, &remoteDoc)
} }
} }
d <- true d <- true
}(uri[0]) }(uri[0])
// wait for double go fun. // wait for double go fun.
<-d <-d
if err != nil { if err != nil {
// no bueno. // no bueno.
return nil, nil, err return nil, nil, err
} }
} }
// lookup item from reference by using a path query. // lookup item from reference by using a path query.
var query string var query string
if len(uri) >= 2 { if len(uri) >= 2 {
query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", ".")) query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", "."))
} else { } else {
query = "$" query = "$"
} }
query, err := url.PathUnescape(query) query, err := url.PathUnescape(query)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// remove any URL encoding // remove any URL encoding
query = strings.Replace(query, "~1", "./", 1) query = strings.Replace(query, "~1", "./", 1)
query = strings.ReplaceAll(query, "~1", "/") query = strings.ReplaceAll(query, "~1", "/")
path, err := yamlpath.NewPath(query) path, err := yamlpath.NewPath(query)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
result, _ := path.Find(parsedRemoteDocument) result, _ := path.Find(parsedRemoteDocument)
if len(result) == 1 { if len(result) == 1 {
return result[0], parsedRemoteDocument, nil return result[0], parsedRemoteDocument, nil
} }
return nil, nil, nil return nil, nil, nil
} }
func (index *SpecIndex) lookupFileReference(ref string) (*yaml.Node, *yaml.Node, error) { func (index *SpecIndex) lookupFileReference(ref string) (*yaml.Node, *yaml.Node, error) {
// split string to remove file reference // split string to remove file reference
uri := strings.Split(ref, "#") uri := strings.Split(ref, "#")
file := strings.ReplaceAll(uri[0], "file:", "") file := strings.ReplaceAll(uri[0], "file:", "")
filePath := filepath.Dir(file) filePath := filepath.Dir(file)
fileName := filepath.Base(file) fileName := filepath.Base(file)
var parsedRemoteDocument *yaml.Node var parsedRemoteDocument *yaml.Node
if index.seenRemoteSources[file] != nil { if index.seenRemoteSources[file] != nil {
parsedRemoteDocument = index.seenRemoteSources[file] parsedRemoteDocument = index.seenRemoteSources[file]
} else { } else {
base := index.config.BasePath base := index.config.BasePath
fileToRead := filepath.Join(base, filePath, fileName) fileToRead := filepath.Join(base, filePath, fileName)
var body []byte var body []byte
var err error var err error
// if we have an FS handler, use it instead of the default behavior // if we have an FS handler, use it instead of the default behavior
if index.config != nil && index.config.FSHandler != nil { if index.config != nil && index.config.FSHandler != nil {
remoteFS := index.config.FSHandler remoteFS := index.config.FSHandler
remoteFile, rErr := remoteFS.Open(fileToRead) remoteFile, rErr := remoteFS.Open(fileToRead)
if rErr != nil { if rErr != nil {
e := fmt.Errorf("unable to open file: %s", rErr) e := fmt.Errorf("unable to open file: %s", rErr)
return nil, nil, e return nil, nil, e
} }
body, err = io.ReadAll(remoteFile) body, err = io.ReadAll(remoteFile)
if err != nil { if err != nil {
e := fmt.Errorf("unable to read file bytes: %s", err) e := fmt.Errorf("unable to read file bytes: %s", err)
return nil, nil, e return nil, nil, e
} }
} else { } else {
// try and read the file off the local file system, if it fails // try and read the file off the local file system, if it fails
// check for a baseURL and then ask our remote lookup function to go try and get it. // check for a baseURL and then ask our remote lookup function to go try and get it.
body, err = os.ReadFile(fileToRead) body, err = os.ReadFile(fileToRead)
if err != nil { if err != nil {
// if we have a baseURL, then we can try and get the file from there. // if we have a baseURL, then we can try and get the file from there.
if index.config != nil && index.config.BaseURL != nil { if index.config != nil && index.config.BaseURL != nil {
u := index.config.BaseURL u := index.config.BaseURL
remoteRef := GenerateCleanSpecConfigBaseURL(u, ref, true) remoteRef := GenerateCleanSpecConfigBaseURL(u, ref, true)
a, b, e := index.lookupRemoteReference(remoteRef) a, b, e := index.lookupRemoteReference(remoteRef)
if e != nil { if e != nil {
// give up, we can't find the file, not locally, not remotely. It's toast. // give up, we can't find the file, not locally, not remotely. It's toast.
return nil, nil, e return nil, nil, e
} }
return a, b, nil return a, b, nil
} else { } else {
// no baseURL? then we can't do anything, give up. // no baseURL? then we can't do anything, give up.
return nil, nil, err return nil, nil, err
} }
} }
} }
var remoteDoc yaml.Node var remoteDoc yaml.Node
err = yaml.Unmarshal(body, &remoteDoc) err = yaml.Unmarshal(body, &remoteDoc)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
parsedRemoteDocument = &remoteDoc parsedRemoteDocument = &remoteDoc
if index.seenLocalSources != nil { if index.seenLocalSources != nil {
index.sourceLock.Lock() index.sourceLock.Lock()
index.seenLocalSources[file] = &remoteDoc index.seenLocalSources[file] = &remoteDoc
index.sourceLock.Unlock() index.sourceLock.Unlock()
} }
} }
// lookup item from reference by using a path query. // lookup item from reference by using a path query.
var query string var query string
if len(uri) >= 2 { if len(uri) >= 2 {
query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", ".")) query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", "."))
} else { } else {
query = "$" query = "$"
} }
query, err := url.PathUnescape(query) query, err := url.PathUnescape(query)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// remove any URL encoding // remove any URL encoding
query = strings.Replace(query, "~1", "./", 1) query = strings.Replace(query, "~1", "./", 1)
query = strings.ReplaceAll(query, "~1", "/") query = strings.ReplaceAll(query, "~1", "/")
path, err := yamlpath.NewPath(query) path, err := yamlpath.NewPath(query)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
result, _ := path.Find(parsedRemoteDocument) result, _ := path.Find(parsedRemoteDocument)
if len(result) == 1 { if len(result) == 1 {
return result[0], parsedRemoteDocument, nil return result[0], parsedRemoteDocument, nil
} }
return nil, parsedRemoteDocument, nil return nil, parsedRemoteDocument, nil
} }
func (index *SpecIndex) FindComponentInRoot(componentId string) *Reference { func (index *SpecIndex) FindComponentInRoot(componentId string) *Reference {
if index.root != nil { if index.root != nil {
// check component for url encoding. // check component for url encoding.
if strings.Contains(componentId, "%") { if strings.Contains(componentId, "%") {
// decode the url. // decode the url.
componentId, _ = url.QueryUnescape(componentId) componentId, _ = url.QueryUnescape(componentId)
} }
name, friendlySearch := utils.ConvertComponentIdIntoFriendlyPathSearch(componentId) name, friendlySearch := utils.ConvertComponentIdIntoFriendlyPathSearch(componentId)
path, err := yamlpath.NewPath(friendlySearch) path, err := yamlpath.NewPath(friendlySearch)
if path == nil || err != nil { if path == nil || err != nil {
return nil // no component found return nil // no component found
} }
res, _ := path.Find(index.root) res, _ := path.Find(index.root)
if len(res) == 1 { if len(res) == 1 {
resNode := res[0] resNode := res[0]
if res[0].Kind == yaml.DocumentNode { if res[0].Kind == yaml.DocumentNode {
resNode = res[0].Content[0] resNode = res[0].Content[0]
} }
ref := &Reference{ ref := &Reference{
Definition: componentId, Definition: componentId,
Name: name, Name: name,
Node: resNode, Node: resNode,
Path: friendlySearch, Path: friendlySearch,
RequiredRefProperties: index.extractDefinitionRequiredRefProperties(res[0], map[string][]string{}), RequiredRefProperties: index.extractDefinitionRequiredRefProperties(res[0], map[string][]string{}),
} }
return ref return ref
} }
} }
return nil return nil
} }
func (index *SpecIndex) performExternalLookup(uri []string, componentId string, func (index *SpecIndex) performExternalLookup(uri []string, componentId string,
lookupFunction ExternalLookupFunction, parent *yaml.Node) *Reference { lookupFunction ExternalLookupFunction, parent *yaml.Node) *Reference {
if len(uri) > 0 { if len(uri) > 0 {
index.externalLock.RLock() index.externalLock.RLock()
externalSpecIndex := index.externalSpecIndex[uri[0]] externalSpecIndex := index.externalSpecIndex[uri[0]]
index.externalLock.RUnlock() index.externalLock.RUnlock()
if externalSpecIndex == nil { if externalSpecIndex == nil {
_, newRoot, err := lookupFunction(componentId) _, newRoot, err := lookupFunction(componentId)
if err != nil { if err != nil {
indexError := &IndexingError{ indexError := &IndexingError{
Err: err, Err: err,
Node: parent, Node: parent,
Path: componentId, Path: componentId,
} }
index.errorLock.Lock() index.errorLock.Lock()
index.refErrors = append(index.refErrors, indexError) index.refErrors = append(index.refErrors, indexError)
index.errorLock.Unlock() index.errorLock.Unlock()
return nil return nil
} }
// cool, cool, lets index this spec also. This is a recursive action and will keep going // cool, cool, lets index this spec also. This is a recursive action and will keep going
// until all remote references have been found. // until all remote references have been found.
var bp *url.URL var bp *url.URL
var bd string var bd string
if index.config.BaseURL != nil { if index.config.BaseURL != nil {
bp = index.config.BaseURL bp = index.config.BaseURL
} }
if index.config.BasePath != "" { if index.config.BasePath != "" {
bd = index.config.BasePath bd = index.config.BasePath
} }
var path, newBasePath string var path, newBasePath string
var newUrl *url.URL var newUrl *url.URL
if bp != nil { if bp != nil {
path = GenerateCleanSpecConfigBaseURL(bp, uri[0], false) path = GenerateCleanSpecConfigBaseURL(bp, uri[0], false)
newUrl, _ = url.Parse(path) newUrl, _ = url.Parse(path)
newBasePath = filepath.Dir(filepath.Join(index.config.BasePath, filepath.Dir(newUrl.Path))) newBasePath = filepath.Dir(filepath.Join(index.config.BasePath, filepath.Dir(newUrl.Path)))
} }
if bd != "" { if bd != "" {
if len(uri[0]) > 0 { if len(uri[0]) > 0 {
// if there is no base url defined, but we can know we have been requested remotely, // if there is no base url defined, but we can know we have been requested remotely,
// set the base url to the remote url base path. // set the base url to the remote url base path.
// first check if the first param is actually a URL // first check if the first param is actually a URL
io, er := url.ParseRequestURI(uri[0]) io, er := url.ParseRequestURI(uri[0])
if er != nil { if er != nil {
newBasePath = filepath.Dir(filepath.Join(bd, uri[0])) newBasePath = filepath.Dir(filepath.Join(bd, uri[0]))
} else { } else {
if newUrl == nil || newUrl.String() != io.String() { if newUrl == nil || newUrl.String() != io.String() {
newUrl, _ = url.Parse(fmt.Sprintf("%s://%s%s", io.Scheme, io.Host, filepath.Dir(io.Path))) newUrl, _ = url.Parse(fmt.Sprintf("%s://%s%s", io.Scheme, io.Host, filepath.Dir(io.Path)))
} }
newBasePath = filepath.Dir(filepath.Join(bd, uri[1])) newBasePath = filepath.Dir(filepath.Join(bd, uri[1]))
} }
} else { } else {
newBasePath = filepath.Dir(filepath.Join(bd, uri[0])) newBasePath = filepath.Dir(filepath.Join(bd, uri[0]))
} }
} }
if newUrl != nil || newBasePath != "" { if newUrl != nil || newBasePath != "" {
newConfig := &SpecIndexConfig{ newConfig := &SpecIndexConfig{
BaseURL: newUrl, BaseURL: newUrl,
BasePath: newBasePath, BasePath: newBasePath,
AllowRemoteLookup: index.config.AllowRemoteLookup, AllowRemoteLookup: index.config.AllowRemoteLookup,
AllowFileLookup: index.config.AllowFileLookup, AllowFileLookup: index.config.AllowFileLookup,
ParentIndex: index, ParentIndex: index,
seenRemoteSources: index.config.seenRemoteSources, seenRemoteSources: index.config.seenRemoteSources,
remoteLock: index.config.remoteLock, remoteLock: index.config.remoteLock,
uri: uri, uri: uri,
} }
var newIndex *SpecIndex var newIndex *SpecIndex
seen := index.SearchAncestryForSeenURI(uri[0]) seen := index.SearchAncestryForSeenURI(uri[0])
if seen == nil { if seen == nil {
newIndex = NewSpecIndexWithConfig(newRoot, newConfig) newIndex = NewSpecIndexWithConfig(newRoot, newConfig)
index.refLock.Lock() index.refLock.Lock()
index.externalLock.Lock() index.externalLock.Lock()
index.externalSpecIndex[uri[0]] = newIndex index.externalSpecIndex[uri[0]] = newIndex
index.externalLock.Unlock() index.externalLock.Unlock()
newIndex.relativePath = path newIndex.relativePath = path
newIndex.parentIndex = index newIndex.parentIndex = index
index.AddChild(newIndex) index.AddChild(newIndex)
index.refLock.Unlock() index.refLock.Unlock()
externalSpecIndex = newIndex externalSpecIndex = newIndex
} else { } else {
externalSpecIndex = seen externalSpecIndex = seen
} }
} }
} }
if externalSpecIndex != nil { if externalSpecIndex != nil {
foundRef := externalSpecIndex.FindComponentInRoot(uri[1]) foundRef := externalSpecIndex.FindComponentInRoot(uri[1])
if foundRef != nil { if foundRef != nil {
nameSegs := strings.Split(uri[1], "/") nameSegs := strings.Split(uri[1], "/")
ref := &Reference{ ref := &Reference{
Definition: componentId, Definition: componentId,
Name: nameSegs[len(nameSegs)-1], Name: nameSegs[len(nameSegs)-1],
Node: foundRef.Node, Node: foundRef.Node,
IsRemote: true, IsRemote: true,
RemoteLocation: componentId, RemoteLocation: componentId,
Path: foundRef.Path, Path: foundRef.Path,
} }
return ref return ref
} }
} }
} }
return nil return nil
} }

View File

@@ -4,125 +4,125 @@
package renderer package renderer
import ( import (
"fmt" "fmt"
"github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi"
"os" "os"
) )
func ExampleMockGenerator_generateBurgerMock_yaml() { func ExampleMockGenerator_generateBurgerMock_yaml() {
// create a new YAML mock generator // create a new YAML mock generator
mg := NewMockGenerator(YAML) mg := NewMockGenerator(YAML)
burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml") burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
// create a new document from specification and build a v3 model. // create a new document from specification and build a v3 model.
document, _ := libopenapi.NewDocument(burgerShop) document, _ := libopenapi.NewDocument(burgerShop)
v3Model, _ := document.BuildV3Model() v3Model, _ := document.BuildV3Model()
// create a mock of the Burger model // create a mock of the Burger model
burgerModel := v3Model.Model.Components.Schemas["Burger"] burgerModel := v3Model.Model.Components.Schemas["Burger"]
burger := burgerModel.Schema() burger := burgerModel.Schema()
mock, err := mg.GenerateMock(burger, "") mock, err := mg.GenerateMock(burger, "")
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(string(mock)) fmt.Println(string(mock))
// Output: name: Big Mac // Output: name: Big Mac
//numPatties: 2 //numPatties: 2
} }
func ExampleMockGenerator_generateFriesMock_json() { func ExampleMockGenerator_generateFriesMock_json() {
// create a new YAML mock generator // create a new YAML mock generator
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml") burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
// create a new document from specification and build a v3 model. // create a new document from specification and build a v3 model.
document, _ := libopenapi.NewDocument(burgerShop) document, _ := libopenapi.NewDocument(burgerShop)
v3Model, _ := document.BuildV3Model() v3Model, _ := document.BuildV3Model()
// create a mock of the Fries model // create a mock of the Fries model
friesModel := v3Model.Model.Components.Schemas["Fries"] friesModel := v3Model.Model.Components.Schemas["Fries"]
fries := friesModel.Schema() fries := friesModel.Schema()
mock, err := mg.GenerateMock(fries, "") mock, err := mg.GenerateMock(fries, "")
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(string(mock)) fmt.Println(string(mock))
// Output: {"favoriteDrink":{"drinkType":"coke","size":"M"},"potatoShape":"Crispy Shoestring"} // Output: {"favoriteDrink":{"drinkType":"coke","size":"M"},"potatoShape":"Crispy Shoestring"}
} }
func ExampleMockGenerator_generateRequestMock_json() { func ExampleMockGenerator_generateRequestMock_json() {
// create a new YAML mock generator // create a new YAML mock generator
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml") burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
// create a new document from specification and build a v3 model. // create a new document from specification and build a v3 model.
document, _ := libopenapi.NewDocument(burgerShop) document, _ := libopenapi.NewDocument(burgerShop)
v3Model, _ := document.BuildV3Model() v3Model, _ := document.BuildV3Model()
// create a mock of the burger request model, extracted from the operation directly. // create a mock of the burger request model, extracted from the operation directly.
burgerRequestModel := v3Model.Model.Paths.PathItems["/burgers"].Post.RequestBody.Content["application/json"] burgerRequestModel := v3Model.Model.Paths.PathItems["/burgers"].Post.RequestBody.Content["application/json"]
// use the 'cakeBurger' example to generate a mock // use the 'cakeBurger' example to generate a mock
mock, err := mg.GenerateMock(burgerRequestModel, "cakeBurger") mock, err := mg.GenerateMock(burgerRequestModel, "cakeBurger")
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(string(mock)) fmt.Println(string(mock))
// Output: {"name":"Chocolate Cake Burger","numPatties":5} // Output: {"name":"Chocolate Cake Burger","numPatties":5}
} }
func ExampleMockGenerator_generateResponseMock_json() { func ExampleMockGenerator_generateResponseMock_json() {
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
// create a new YAML mock generator // create a new YAML mock generator
burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml") burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
// create a new document from specification and build a v3 model. // create a new document from specification and build a v3 model.
document, _ := libopenapi.NewDocument(burgerShop) document, _ := libopenapi.NewDocument(burgerShop)
v3Model, _ := document.BuildV3Model() v3Model, _ := document.BuildV3Model()
// create a mock of the burger response model, extracted from the operation directly. // create a mock of the burger response model, extracted from the operation directly.
burgerResponseModel := v3Model.Model.Paths.PathItems["/burgers"].Post.Responses.Codes["200"].Content["application/json"] burgerResponseModel := v3Model.Model.Paths.PathItems["/burgers"].Post.Responses.Codes["200"].Content["application/json"]
// use the 'filetOFish' example to generate a mock // use the 'filetOFish' example to generate a mock
mock, err := mg.GenerateMock(burgerResponseModel, "filetOFish") mock, err := mg.GenerateMock(burgerResponseModel, "filetOFish")
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(string(mock)) fmt.Println(string(mock))
// Output: {"name":"Filet-O-Fish","numPatties":1} // Output: {"name":"Filet-O-Fish","numPatties":1}
} }
func ExampleMockGenerator_generatePolymorphicMock_json() { func ExampleMockGenerator_generatePolymorphicMock_json() {
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
// create a new YAML mock generator // create a new YAML mock generator
burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml") burgerShop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
// create a new document from specification and build a v3 model. // create a new document from specification and build a v3 model.
document, _ := libopenapi.NewDocument(burgerShop) document, _ := libopenapi.NewDocument(burgerShop)
v3Model, _ := document.BuildV3Model() v3Model, _ := document.BuildV3Model()
// create a mock of the SomePayload component, which uses polymorphism (incorrectly) // create a mock of the SomePayload component, which uses polymorphism (incorrectly)
payloadModel := v3Model.Model.Components.Schemas["SomePayload"] payloadModel := v3Model.Model.Components.Schemas["SomePayload"]
payload := payloadModel.Schema() payload := payloadModel.Schema()
mock, err := mg.GenerateMock(payload, "") mock, err := mg.GenerateMock(payload, "")
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(string(mock)) fmt.Println(string(mock))
// Output: {"drinkType":"coke","size":"M"} // Output: {"drinkType":"coke","size":"M"}
} }

View File

@@ -4,26 +4,26 @@
package renderer package renderer
import ( import (
"encoding/json" "encoding/json"
highbase "github.com/pb33f/libopenapi/datamodel/high/base" highbase "github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
lowbase "github.com/pb33f/libopenapi/datamodel/low/base" lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"strings" "strings"
"testing" "testing"
) )
type fakeMockable struct { type fakeMockable struct {
Schema *highbase.SchemaProxy Schema *highbase.SchemaProxy
Example any Example any
Examples map[string]*highbase.Example Examples map[string]*highbase.Example
} }
type fakeMockableButWithASchemaNotAProxy struct { type fakeMockableButWithASchemaNotAProxy struct {
Schema *highbase.Schema Schema *highbase.Schema
Example any Example any
Examples map[string]*highbase.Example Examples map[string]*highbase.Example
} }
var simpleFakeMockSchema = `type: string var simpleFakeMockSchema = `type: string
@@ -40,240 +40,240 @@ properties:
maximum: 400` maximum: 400`
func createFakeMock(mock string, values map[string]any, example any) *fakeMockable { func createFakeMock(mock string, values map[string]any, example any) *fakeMockable {
var root yaml.Node var root yaml.Node
_ = yaml.Unmarshal([]byte(mock), &root) _ = yaml.Unmarshal([]byte(mock), &root)
var lowProxy lowbase.SchemaProxy var lowProxy lowbase.SchemaProxy
_ = lowProxy.Build(&root, root.Content[0], nil) _ = lowProxy.Build(&root, root.Content[0], nil)
lowRef := low.NodeReference[*lowbase.SchemaProxy]{ lowRef := low.NodeReference[*lowbase.SchemaProxy]{
Value: &lowProxy, Value: &lowProxy,
} }
highSchema := highbase.NewSchemaProxy(&lowRef) highSchema := highbase.NewSchemaProxy(&lowRef)
examples := make(map[string]*highbase.Example) examples := make(map[string]*highbase.Example)
for k, v := range values { for k, v := range values {
examples[k] = &highbase.Example{ examples[k] = &highbase.Example{
Value: v, Value: v,
} }
} }
return &fakeMockable{ return &fakeMockable{
Schema: highSchema, Schema: highSchema,
Example: example, Example: example,
Examples: examples, Examples: examples,
} }
} }
func createFakeMockWithoutProxy(mock string, values map[string]any, example any) *fakeMockableButWithASchemaNotAProxy { func createFakeMockWithoutProxy(mock string, values map[string]any, example any) *fakeMockableButWithASchemaNotAProxy {
var root yaml.Node var root yaml.Node
_ = yaml.Unmarshal([]byte(mock), &root) _ = yaml.Unmarshal([]byte(mock), &root)
var lowProxy lowbase.SchemaProxy var lowProxy lowbase.SchemaProxy
_ = lowProxy.Build(&root, root.Content[0], nil) _ = lowProxy.Build(&root, root.Content[0], nil)
lowRef := low.NodeReference[*lowbase.SchemaProxy]{ lowRef := low.NodeReference[*lowbase.SchemaProxy]{
Value: &lowProxy, Value: &lowProxy,
} }
highSchema := highbase.NewSchemaProxy(&lowRef) highSchema := highbase.NewSchemaProxy(&lowRef)
examples := make(map[string]*highbase.Example) examples := make(map[string]*highbase.Example)
for k, v := range values { for k, v := range values {
examples[k] = &highbase.Example{ examples[k] = &highbase.Example{
Value: v, Value: v,
} }
} }
return &fakeMockableButWithASchemaNotAProxy{ return &fakeMockableButWithASchemaNotAProxy{
Schema: highSchema.Schema(), Schema: highSchema.Schema(),
Example: example, Example: example,
Examples: examples, Examples: examples,
} }
} }
func TestNewMockGenerator(t *testing.T) { func TestNewMockGenerator(t *testing.T) {
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
assert.NotNil(t, mg) assert.NotNil(t, mg)
} }
func TestNewMockGeneratorWithDictionary(t *testing.T) { func TestNewMockGeneratorWithDictionary(t *testing.T) {
mg := NewMockGeneratorWithDictionary("", JSON) mg := NewMockGeneratorWithDictionary("", JSON)
assert.NotNil(t, mg) assert.NotNil(t, mg)
} }
func TestMockGenerator_GenerateJSONMock_BadObject(t *testing.T) { func TestMockGenerator_GenerateJSONMock_BadObject(t *testing.T) {
type NotMockable struct { type NotMockable struct {
pizza string pizza string
} }
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
mock, err := mg.GenerateMock(&NotMockable{}, "") mock, err := mg.GenerateMock(&NotMockable{}, "")
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, mock) assert.Nil(t, mock)
} }
func TestMockGenerator_GenerateJSONMock_EmptyObject(t *testing.T) { func TestMockGenerator_GenerateJSONMock_EmptyObject(t *testing.T) {
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
mock, err := mg.GenerateMock(&fakeMockable{}, "") mock, err := mg.GenerateMock(&fakeMockable{}, "")
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, mock) assert.Nil(t, mock)
} }
func TestMockGenerator_GenerateJSONMock_SuppliedExample_JSON(t *testing.T) { func TestMockGenerator_GenerateJSONMock_SuppliedExample_JSON(t *testing.T) {
fakeExample := map[string]any{ fakeExample := map[string]any{
"fish-and-chips": "cod-and-chips-twice", "fish-and-chips": "cod-and-chips-twice",
} }
fake := createFakeMock(simpleFakeMockSchema, nil, fakeExample) fake := createFakeMock(simpleFakeMockSchema, nil, fakeExample)
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
mock, err := mg.GenerateMock(fake, "") mock, err := mg.GenerateMock(fake, "")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\"fish-and-chips\":\"cod-and-chips-twice\"}", string(mock)) assert.Equal(t, "{\"fish-and-chips\":\"cod-and-chips-twice\"}", string(mock))
} }
func TestMockGenerator_GenerateJSONMock_SuppliedExample_YAML(t *testing.T) { func TestMockGenerator_GenerateJSONMock_SuppliedExample_YAML(t *testing.T) {
fakeExample := map[string]any{ fakeExample := map[string]any{
"fish-and-chips": "cod-and-chips-twice", "fish-and-chips": "cod-and-chips-twice",
} }
fake := createFakeMock(simpleFakeMockSchema, nil, fakeExample) fake := createFakeMock(simpleFakeMockSchema, nil, fakeExample)
mg := NewMockGenerator(YAML) mg := NewMockGenerator(YAML)
mock, err := mg.GenerateMock(fake, "") mock, err := mg.GenerateMock(fake, "")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "fish-and-chips: cod-and-chips-twice", strings.TrimSpace(string(mock))) assert.Equal(t, "fish-and-chips: cod-and-chips-twice", strings.TrimSpace(string(mock)))
} }
func TestMockGenerator_GenerateJSONMock_MultiExamples_NoName_JSON(t *testing.T) { func TestMockGenerator_GenerateJSONMock_MultiExamples_NoName_JSON(t *testing.T) {
fakeExample := map[string]any{ fakeExample := map[string]any{
"exampleOne": map[string]any{ "exampleOne": map[string]any{
"fish-and-chips": "cod-and-chips-twice", "fish-and-chips": "cod-and-chips-twice",
}, },
"exampleTwo": map[string]any{ "exampleTwo": map[string]any{
"rice-and-peas": "brown-or-white-rice", "rice-and-peas": "brown-or-white-rice",
}, },
} }
fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil) fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
mock, err := mg.GenerateMock(fake, "JimmyJammyJimJams") // does not exist mock, err := mg.GenerateMock(fake, "JimmyJammyJimJams") // does not exist
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, string(mock)) assert.NotEmpty(t, string(mock))
} }
func TestMockGenerator_GenerateJSONMock_MultiExamples_JSON(t *testing.T) { func TestMockGenerator_GenerateJSONMock_MultiExamples_JSON(t *testing.T) {
fakeExample := map[string]any{ fakeExample := map[string]any{
"exampleOne": map[string]any{ "exampleOne": map[string]any{
"fish-and-chips": "cod-and-chips-twice", "fish-and-chips": "cod-and-chips-twice",
}, },
"exampleTwo": map[string]any{ "exampleTwo": map[string]any{
"rice-and-peas": "brown-or-white-rice", "rice-and-peas": "brown-or-white-rice",
}, },
} }
fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil) fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
mock, err := mg.GenerateMock(fake, "exampleTwo") mock, err := mg.GenerateMock(fake, "exampleTwo")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\"rice-and-peas\":\"brown-or-white-rice\"}", string(mock)) assert.Equal(t, "{\"rice-and-peas\":\"brown-or-white-rice\"}", string(mock))
} }
func TestMockGenerator_GenerateJSONMock_MultiExamples_PrettyJSON(t *testing.T) { func TestMockGenerator_GenerateJSONMock_MultiExamples_PrettyJSON(t *testing.T) {
fakeExample := map[string]any{ fakeExample := map[string]any{
"exampleOne": map[string]any{ "exampleOne": map[string]any{
"fish-and-chips": "cod-and-chips-twice", "fish-and-chips": "cod-and-chips-twice",
}, },
"exampleTwo": map[string]any{ "exampleTwo": map[string]any{
"rice-and-peas": "brown-or-white-rice", "rice-and-peas": "brown-or-white-rice",
"peas": "buttery", "peas": "buttery",
}, },
} }
fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil) fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
mg.SetPretty() mg.SetPretty()
mock, err := mg.GenerateMock(fake, "exampleTwo") mock, err := mg.GenerateMock(fake, "exampleTwo")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "{\n \"peas\": \"buttery\",\n \"rice-and-peas\": \"brown-or-white-rice\"\n}", string(mock)) assert.Equal(t, "{\n \"peas\": \"buttery\",\n \"rice-and-peas\": \"brown-or-white-rice\"\n}", string(mock))
} }
func TestMockGenerator_GenerateJSONMock_MultiExamples_YAML(t *testing.T) { func TestMockGenerator_GenerateJSONMock_MultiExamples_YAML(t *testing.T) {
fakeExample := map[string]any{ fakeExample := map[string]any{
"exampleOne": map[string]any{ "exampleOne": map[string]any{
"fish-and-chips": "cod-and-chips-twice", "fish-and-chips": "cod-and-chips-twice",
}, },
"exampleTwo": map[string]any{ "exampleTwo": map[string]any{
"rice-and-peas": "brown-or-white-rice", "rice-and-peas": "brown-or-white-rice",
}, },
} }
fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil) fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
mg := NewMockGenerator(YAML) mg := NewMockGenerator(YAML)
mock, err := mg.GenerateMock(fake, "exampleTwo") mock, err := mg.GenerateMock(fake, "exampleTwo")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "rice-and-peas: brown-or-white-rice", strings.TrimSpace(string(mock))) assert.Equal(t, "rice-and-peas: brown-or-white-rice", strings.TrimSpace(string(mock)))
} }
func TestMockGenerator_GenerateJSONMock_NoExamples_JSON(t *testing.T) { func TestMockGenerator_GenerateJSONMock_NoExamples_JSON(t *testing.T) {
fake := createFakeMock(simpleFakeMockSchema, nil, nil) fake := createFakeMock(simpleFakeMockSchema, nil, nil)
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
mock, err := mg.GenerateMock(fake, "") mock, err := mg.GenerateMock(fake, "")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "magic-herbs", string(mock)) assert.Equal(t, "magic-herbs", string(mock))
} }
func TestMockGenerator_GenerateJSONMock_NoExamples_YAML(t *testing.T) { func TestMockGenerator_GenerateJSONMock_NoExamples_YAML(t *testing.T) {
fake := createFakeMock(simpleFakeMockSchema, nil, nil) fake := createFakeMock(simpleFakeMockSchema, nil, nil)
mg := NewMockGenerator(YAML) mg := NewMockGenerator(YAML)
mock, err := mg.GenerateMock(fake, "") mock, err := mg.GenerateMock(fake, "")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "magic-herbs", string(mock)) assert.Equal(t, "magic-herbs", string(mock))
} }
func TestMockGenerator_GenerateJSONMock_Object_NoExamples_JSON(t *testing.T) { func TestMockGenerator_GenerateJSONMock_Object_NoExamples_JSON(t *testing.T) {
fake := createFakeMock(objectFakeMockSchema, nil, nil) fake := createFakeMock(objectFakeMockSchema, nil, nil)
mg := NewMockGenerator(JSON) mg := NewMockGenerator(JSON)
mock, err := mg.GenerateMock(fake, "") mock, err := mg.GenerateMock(fake, "")
assert.NoError(t, err) assert.NoError(t, err)
// re-serialize back into a map and check the values // re-serialize back into a map and check the values
var m map[string]any var m map[string]any
err = json.Unmarshal(mock, &m) err = json.Unmarshal(mock, &m)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, m, 2) assert.Len(t, m, 2)
assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6) assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6)
assert.GreaterOrEqual(t, m["herbs"].(float64), float64(350)) assert.GreaterOrEqual(t, m["herbs"].(float64), float64(350))
assert.LessOrEqual(t, m["herbs"].(float64), float64(400)) assert.LessOrEqual(t, m["herbs"].(float64), float64(400))
} }
func TestMockGenerator_GenerateJSONMock_Object_NoExamples_YAML(t *testing.T) { func TestMockGenerator_GenerateJSONMock_Object_NoExamples_YAML(t *testing.T) {
fake := createFakeMock(objectFakeMockSchema, nil, nil) fake := createFakeMock(objectFakeMockSchema, nil, nil)
mg := NewMockGenerator(YAML) mg := NewMockGenerator(YAML)
mock, err := mg.GenerateMock(fake, "") mock, err := mg.GenerateMock(fake, "")
assert.NoError(t, err) assert.NoError(t, err)
// re-serialize back into a map and check the values // re-serialize back into a map and check the values
var m map[string]any var m map[string]any
err = yaml.Unmarshal(mock, &m) err = yaml.Unmarshal(mock, &m)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, m, 2) assert.Len(t, m, 2)
assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6) assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6)
assert.GreaterOrEqual(t, m["herbs"].(int), 350) assert.GreaterOrEqual(t, m["herbs"].(int), 350)
assert.LessOrEqual(t, m["herbs"].(int), 400) assert.LessOrEqual(t, m["herbs"].(int), 400)
} }
// should result in the exact same output as the above test // should result in the exact same output as the above test
func TestMockGenerator_GenerateJSONMock_Object_RawSchema(t *testing.T) { func TestMockGenerator_GenerateJSONMock_Object_RawSchema(t *testing.T) {
fake := createFakeMockWithoutProxy(objectFakeMockSchema, nil, nil) fake := createFakeMockWithoutProxy(objectFakeMockSchema, nil, nil)
mg := NewMockGenerator(YAML) mg := NewMockGenerator(YAML)
mock, err := mg.GenerateMock(fake, "") mock, err := mg.GenerateMock(fake, "")
assert.NoError(t, err) assert.NoError(t, err)
// re-serialize back into a map and check the values // re-serialize back into a map and check the values
var m map[string]any var m map[string]any
err = yaml.Unmarshal(mock, &m) err = yaml.Unmarshal(mock, &m)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, m, 2) assert.Len(t, m, 2)
assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6) assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6)
assert.GreaterOrEqual(t, m["herbs"].(int), 350) assert.GreaterOrEqual(t, m["herbs"].(int), 350)
assert.LessOrEqual(t, m["herbs"].(int), 400) assert.LessOrEqual(t, m["herbs"].(int), 400)
} }