From a2b7119af7039912271a852dec7f81cd45d6c1f2 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Tue, 13 Sep 2022 09:15:55 -0400 Subject: [PATCH] Added mutate command to low level API This simple method gives the low API a super powerful and simple way to mutate the value of any node, which is then reflected in the root node, can than be serialized again and, voila! now our spec is editable. --- datamodel/low/reference.go | 8 ++++ datamodel/spec_info.go | 9 +++- document.go | 14 ++++++ document_test.go | 60 ++++++++++++++++++++++++++ libopenapi.go | 88 +++++++++++++++++--------------------- 5 files changed, 128 insertions(+), 51 deletions(-) diff --git a/datamodel/low/reference.go b/datamodel/low/reference.go index b0b2636..2ae2580 100644 --- a/datamodel/low/reference.go +++ b/datamodel/low/reference.go @@ -40,6 +40,10 @@ func (n NodeReference[T]) GenerateMapKey() string { return fmt.Sprintf("%d:%d", n.ValueNode.Line, n.ValueNode.Column) } +func (n NodeReference[T]) Mutate(value T) { + n.ValueNode.Value = fmt.Sprintf("%v", value) +} + func (n ValueReference[T]) IsEmpty() bool { return n.ValueNode == nil } @@ -56,6 +60,10 @@ func (n KeyReference[T]) GenerateMapKey() string { return fmt.Sprintf("%d:%d", n.KeyNode.Line, n.KeyNode.Column) } +func (n ValueReference[T]) Mutate(value T) { + n.ValueNode.Value = fmt.Sprintf("%v", value) +} + func IsCircular(node *yaml.Node, idx *index.SpecIndex) bool { if idx == nil { return false // no index! nothing we can do. diff --git a/datamodel/spec_info.go b/datamodel/spec_info.go index ed0d141..6f1a536 100644 --- a/datamodel/spec_info.go +++ b/datamodel/spec_info.go @@ -13,6 +13,11 @@ import ( "time" ) +const ( + JSONFileType = "json" + YAMLFileType = "yaml" +) + // SpecInfo represents a 'ready-to-process' OpenAPI Document. type SpecInfo struct { SpecType string `json:"type"` @@ -59,9 +64,9 @@ func ExtractSpecInfo(spec []byte) (*SpecInfo, error) { } if runes[0] == '{' && runes[len(runes)-1] == '}' { - specVersion.SpecFileType = "json" + specVersion.SpecFileType = JSONFileType } else { - specVersion.SpecFileType = "yaml" + specVersion.SpecFileType = YAMLFileType } err := yaml.Unmarshal(spec, &parsedSpec) diff --git a/document.go b/document.go index 2084df6..28f297d 100644 --- a/document.go +++ b/document.go @@ -10,6 +10,8 @@ import ( v3high "github.com/pb33f/libopenapi/datamodel/high/3.0" v2low "github.com/pb33f/libopenapi/datamodel/low/2.0" v3low "github.com/pb33f/libopenapi/datamodel/low/3.0" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" ) type Document struct { @@ -40,6 +42,18 @@ func (d *Document) GetSpecInfo() *datamodel.SpecInfo { return d.info } +func (d *Document) Serialize() ([]byte, error) { + if d.info == nil { + return nil, fmt.Errorf("unable to serialize, document has not yet been initialized") + } + if d.info.SpecFileType == datamodel.YAMLFileType { + return yaml.Marshal(d.info.RootNode) + } else { + yamlData, _ := yaml.Marshal(d.info.RootNode) + return utils.ConvertYAMLtoJSON(yamlData) + } +} + func (d *Document) BuildV2Document() (*DocumentModel[v2high.Swagger], []error) { var errors []error if d.info == nil { diff --git a/document_test.go b/document_test.go index 6643d88..f4c8101 100644 --- a/document_test.go +++ b/document_test.go @@ -106,3 +106,63 @@ paths: assert.Len(t, docErr, 1) assert.Nil(t, v3Doc) } + +func TestDocument_Serialize_Error(t *testing.T) { + doc := new(Document) // not how this should be instantiated. + _, err := doc.Serialize() + assert.Error(t, err) +} + +func TestDocument_Serialize(t *testing.T) { + + yml := `openapi: 3.0 +info: + title: The magic API +` + doc, _ := NewDocument([]byte(yml)) + serial, err := doc.Serialize() + assert.NoError(t, err) + assert.Equal(t, yml, string(serial)) +} + +func TestDocument_Serialize_Modified(t *testing.T) { + + yml := `openapi: 3.0 +info: + title: The magic API +` + + ymlModified := `openapi: 3.0 +info: + title: The magic API - but now, altered! +` + doc, _ := NewDocument([]byte(yml)) + + v3Doc, _ := doc.BuildV3Document() + + v3Doc.Model.Info.GoLow().Title.Mutate("The magic API - but now, altered!") + + serial, err := doc.Serialize() + assert.NoError(t, err) + assert.Equal(t, ymlModified, string(serial)) +} + +func TestDocument_Serialize_JSON_Modified(t *testing.T) { + + json := `{ 'openapi': '3.0', + 'info': { + 'title': 'The magic API' + } +} +` + jsonModified := `{"info":{"title":"The magic API - but now, altered!"},"openapi":"3.0"}` + doc, _ := NewDocument([]byte(json)) + + v3Doc, _ := doc.BuildV3Document() + + v3Doc.Model.Info.GoLow().Title.Mutate("The magic API - but now, altered!") + + serial, err := doc.Serialize() + assert.NoError(t, err) + assert.Equal(t, jsonModified, string(serial)) +} diff --git a/libopenapi.go b/libopenapi.go index 1bb36d8..320b093 100644 --- a/libopenapi.go +++ b/libopenapi.go @@ -1,51 +1,41 @@ package main -import ( - "fmt" - "github.com/pb33f/libopenapi/datamodel" - high "github.com/pb33f/libopenapi/datamodel/high/3.0" - low "github.com/pb33f/libopenapi/datamodel/low/3.0" - "gopkg.in/yaml.v3" - "io/ioutil" - "os" -) - -func main() { - - testData := `openapi: 3.0.1 -info: - title: this is a title - description: this is a description -tags: - - name: Tag A - description: cake - x-hack: true - - name: Tag B - description: coffee - x-code: hack` - - data := []byte(testData) - _ = ioutil.WriteFile("sample.yaml", data, 0664) - - info, _ := datamodel.ExtractSpecInfo(data) - lowDoc, err := low.CreateDocument(info) - if len(err) > 0 { - for e := range err { - fmt.Printf("%e\n", err[e]) - } - return - } - highDoc := high.NewDocument(lowDoc) - - highDoc.Info.GoLow().Title.ValueNode.Value = "let's hack this" - //highDoc.Tags[0].SetName("We are a new name now") - //highDoc.Tags[0].SetDescription("and a new description") - - //newTag := lowDoc.AddTag() - //fmt.Println(newTag) - modified, _ := yaml.Marshal(info.RootNode) - fmt.Println(string(modified)) - - os.Remove("sample.yaml") - -} +//func main() { +// +// testData := `openapi: 3.0.1 +//info: +// title: this is a title +// description: this is a description +//tags: +// - name: Tag A +// description: cake +// x-hack: true +// - name: Tag B +// description: coffee +// x-code: hack` +// +// data := []byte(testData) +// _ = ioutil.WriteFile("sample.yaml", data, 0664) +// +// info, _ := datamodel.ExtractSpecInfo(data) +// lowDoc, err := low.CreateDocument(info) +// if len(err) > 0 { +// for e := range err { +// fmt.Printf("%e\n", err[e]) +// } +// return +// } +// highDoc := high.NewDocument(lowDoc) +// +// highDoc.Info.GoLow().Title.ValueNode.Value = "let's hack this" +// //highDoc.Tags[0].SetName("We are a new name now") +// //highDoc.Tags[0].SetDescription("and a new description") +// +// //newTag := lowDoc.AddTag() +// //fmt.Println(newTag) +// modified, _ := yaml.Marshal(info.RootNode) +// fmt.Println(string(modified)) +// +// os.Remove("sample.yaml") +// +//}