From 652e818456e422341a7505fe6430ffb6730bcbf1 Mon Sep 17 00:00:00 2001 From: Shawn Poulson Date: Thu, 3 Aug 2023 16:18:03 -0400 Subject: [PATCH] Refactor `Paths` to `OrderedMap`. --- datamodel/high/v2/paths.go | 16 ++--- datamodel/high/v2/swagger_test.go | 4 +- datamodel/high/v3/document_test.go | 11 ++-- datamodel/high/v3/media_type_test.go | 4 +- datamodel/high/v3/package_test.go | 6 +- datamodel/high/v3/paths.go | 28 +++++--- datamodel/high/v3/paths_test.go | 4 +- datamodel/low/v2/paths.go | 48 +++++++++----- datamodel/low/v3/paths.go | 48 +++++++++----- document_examples_test.go | 20 +++--- document_test.go | 95 +++++++++++++++++++++++----- what-changed/model/paths.go | 32 ++++++---- 12 files changed, 216 insertions(+), 100 deletions(-) diff --git a/datamodel/high/v2/paths.go b/datamodel/high/v2/paths.go index e55b701..462076a 100644 --- a/datamodel/high/v2/paths.go +++ b/datamodel/high/v2/paths.go @@ -4,15 +4,15 @@ package v2 import ( - "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/low" v2low "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/orderedmap" ) // Paths represents a high-level Swagger / OpenAPI Paths object, backed by a low-level one. type Paths struct { - PathItems map[string]*PathItem + PathItems orderedmap.Map[string, *PathItem] Extensions map[string]any low *v2low.Paths } @@ -22,19 +22,19 @@ func NewPaths(paths *v2low.Paths) *Paths { p := new(Paths) p.low = paths p.Extensions = high.ExtractExtensions(paths.Extensions) - pathItems := make(map[string]*PathItem) + pathItems := orderedmap.New[string, *PathItem]() - translateFunc := func(key low.KeyReference[string], value low.ValueReference[*v2low.PathItem]) (asyncResult[*PathItem], error) { + translateFunc := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*v2low.PathItem]]) (asyncResult[*PathItem], error) { return asyncResult[*PathItem]{ - key: key.Value, - result: NewPathItem(value.Value), + key: pair.Key().Value, + result: NewPathItem(pair.Value().Value), }, nil } resultFunc := func(result asyncResult[*PathItem]) error { - pathItems[result.key] = result.result + pathItems.Set(result.key, result.result) return nil } - _ = datamodel.TranslateMapParallel[low.KeyReference[string], low.ValueReference[*v2low.PathItem], asyncResult[*PathItem]]( + _ = orderedmap.TranslateMapParallel[low.KeyReference[string], low.ValueReference[*v2low.PathItem], asyncResult[*PathItem]]( paths.PathItems, translateFunc, resultFunc, ) p.PathItems = pathItems diff --git a/datamodel/high/v2/swagger_test.go b/datamodel/high/v2/swagger_test.go index e21eed4..14bf06a 100644 --- a/datamodel/high/v2/swagger_test.go +++ b/datamodel/high/v2/swagger_test.go @@ -204,7 +204,7 @@ func TestNewSwaggerDocument_Paths(t *testing.T) { highDoc := NewSwaggerDocument(doc) assert.Len(t, highDoc.Paths.PathItems, 15) - upload := highDoc.Paths.PathItems["/pet/{petId}/uploadImage"] + upload := highDoc.Paths.PathItems.GetOrZero("/pet/{petId}/uploadImage") assert.Equal(t, "man", upload.Extensions["x-potato"]) assert.Nil(t, upload.Get) assert.Nil(t, upload.Put) @@ -262,7 +262,7 @@ func TestNewSwaggerDocument_Responses(t *testing.T) { initTest() highDoc := NewSwaggerDocument(doc) - upload := highDoc.Paths.PathItems["/pet/{petId}/uploadImage"].Post + upload := highDoc.Paths.PathItems.GetOrZero("/pet/{petId}/uploadImage").Post assert.Len(t, upload.Responses.Codes, 1) diff --git a/datamodel/high/v3/document_test.go b/datamodel/high/v3/document_test.go index 5c67cd8..d394b7b 100644 --- a/datamodel/high/v3/document_test.go +++ b/datamodel/high/v3/document_test.go @@ -14,6 +14,7 @@ import ( v2 "github.com/pb33f/libopenapi/datamodel/high/v2" lowv2 "github.com/pb33f/libopenapi/datamodel/low/v2" lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" ) @@ -330,7 +331,7 @@ func TestNewDocument_Paths(t *testing.T) { } func testBurgerShop(t *testing.T, h *Document, checkLines bool) { - burgersOp := h.Paths.PathItems["/burgers"] + burgersOp := h.Paths.PathItems.GetOrZero("/burgers") assert.Len(t, burgersOp.GetOperations(), 1) assert.Equal(t, "meaty", burgersOp.Extensions["x-burger-meta"]) @@ -410,7 +411,7 @@ func TestAsanaAsDoc(t *testing.T) { } d := NewDocument(lowDoc) assert.NotNil(t, d) - assert.Equal(t, 118, len(d.Paths.PathItems)) + assert.Equal(t, 118, orderedmap.Len(d.Paths.PathItems)) } func TestDigitalOceanAsDocFromSHA(t *testing.T) { @@ -434,7 +435,7 @@ func TestDigitalOceanAsDocFromSHA(t *testing.T) { } d := NewDocument(lowDoc) assert.NotNil(t, d) - assert.Equal(t, 183, len(d.Paths.PathItems)) + assert.Equal(t, 183, orderedmap.Len(d.Paths.PathItems)) } @@ -448,7 +449,7 @@ func TestPetstoreAsDoc(t *testing.T) { } d := NewDocument(lowDoc) assert.NotNil(t, d) - assert.Equal(t, 13, len(d.Paths.PathItems)) + assert.Equal(t, 13, orderedmap.Len(d.Paths.PathItems)) } func TestCircularReferencesDoc(t *testing.T) { @@ -532,7 +533,7 @@ func TestDocument_MarshalJSON(t *testing.T) { lowDoc, _ = lowv3.CreateDocumentFromConfig(info, datamodel.NewOpenDocumentConfiguration()) newDoc := NewDocument(lowDoc) - assert.Equal(t, len(newDoc.Paths.PathItems), len(highDoc.Paths.PathItems)) + assert.Equal(t, orderedmap.Len(newDoc.Paths.PathItems), orderedmap.Len(highDoc.Paths.PathItems)) assert.Equal(t, len(newDoc.Components.Schemas), len(highDoc.Components.Schemas)) } diff --git a/datamodel/high/v3/media_type_test.go b/datamodel/high/v3/media_type_test.go index 6574ef8..361dc64 100644 --- a/datamodel/high/v3/media_type_test.go +++ b/datamodel/high/v3/media_type_test.go @@ -28,7 +28,7 @@ func TestMediaType_MarshalYAMLInline(t *testing.T) { // create a new document and extract a media type object from it. d := NewDocument(lowDoc) - mt := d.Paths.PathItems["/pet"].Put.RequestBody.Content["application/json"] + mt := d.Paths.PathItems.GetOrZero("/pet").Put.RequestBody.Content["application/json"] // render out the media type yml, _ := mt.Render() @@ -118,7 +118,7 @@ func TestMediaType_MarshalYAML(t *testing.T) { // create a new document and extract a media type object from it. d := NewDocument(lowDoc) - mt := d.Paths.PathItems["/pet"].Put.RequestBody.Content["application/json"] + mt := d.Paths.PathItems.GetOrZero("/pet").Put.RequestBody.Content["application/json"] // render out the media type yml, _ := mt.Render() diff --git a/datamodel/high/v3/package_test.go b/datamodel/high/v3/package_test.go index cc1e6e3..6c43270 100644 --- a/datamodel/high/v3/package_test.go +++ b/datamodel/high/v3/package_test.go @@ -5,9 +5,11 @@ package v3 import ( "fmt" + "io/ioutil" + "github.com/pb33f/libopenapi/datamodel" lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" - "io/ioutil" + "github.com/pb33f/libopenapi/orderedmap" ) // An example of how to create a new high-level OpenAPI 3+ document from an OpenAPI specification. @@ -36,6 +38,6 @@ func Example_createHighLevelOpenAPIDocument() { // Print out some details fmt.Printf("Petstore contains %d paths and %d component schemas", - len(doc.Paths.PathItems), len(doc.Components.Schemas)) + orderedmap.Len(doc.Paths.PathItems), len(doc.Components.Schemas)) // Output: Petstore contains 13 paths and 8 component schemas } diff --git a/datamodel/high/v3/paths.go b/datamodel/high/v3/paths.go index 7e2e184..fa0f241 100644 --- a/datamodel/high/v3/paths.go +++ b/datamodel/high/v3/paths.go @@ -6,10 +6,10 @@ package v3 import ( "sort" - "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/low" v3low "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -21,8 +21,8 @@ import ( // constraints. // - https://spec.openapis.org/oas/v3.1.0#paths-object type Paths struct { - PathItems map[string]*PathItem `json:"-" yaml:"-"` - Extensions map[string]any `json:"-" yaml:"-"` + PathItems orderedmap.Map[string, *PathItem] `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` low *v3low.Paths } @@ -31,21 +31,21 @@ func NewPaths(paths *v3low.Paths) *Paths { p := new(Paths) p.low = paths p.Extensions = high.ExtractExtensions(paths.Extensions) - items := make(map[string]*PathItem) + items := orderedmap.New[string, *PathItem]() type pathItemResult struct { key string value *PathItem } - translateFunc := func(key low.KeyReference[string], value low.ValueReference[*v3low.PathItem]) (pathItemResult, error) { - return pathItemResult{key: key.Value, value: NewPathItem(value.Value)}, nil + translateFunc := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*v3low.PathItem]]) (pathItemResult, error) { + return pathItemResult{key: pair.Key().Value, value: NewPathItem(pair.Value().Value)}, nil } resultFunc := func(value pathItemResult) error { - items[value.key] = value.value + items.Set(value.key, value.value) return nil } - _ = datamodel.TranslateMapParallel[low.KeyReference[string], low.ValueReference[*v3low.PathItem], pathItemResult]( + _ = orderedmap.TranslateMapParallel[low.KeyReference[string], low.ValueReference[*v3low.PathItem], pathItemResult]( paths.PathItems, translateFunc, resultFunc, ) p.PathItems = items @@ -84,7 +84,9 @@ func (p *Paths) MarshalYAML() (interface{}, error) { } var mapped []*pathItem - for k, pi := range p.PathItems { + action := func(pair orderedmap.Pair[string, *PathItem]) error { + k := pair.Key() + pi := pair.Value() ln := 9999 // default to a high value to weight new content to the bottom. if p.low != nil { lpi := p.low.FindPath(k) @@ -93,7 +95,9 @@ func (p *Paths) MarshalYAML() (interface{}, error) { } } mapped = append(mapped, &pathItem{pi, k, ln, nil}) + return nil } + _ = orderedmap.For[string, *PathItem](p.PathItems, action) nb := high.NewNodeBuilder(p, p.low) extNode := nb.Render() @@ -138,7 +142,9 @@ func (p *Paths) MarshalYAMLInline() (interface{}, error) { } var mapped []*pathItem - for k, pi := range p.PathItems { + action := func(pair orderedmap.Pair[string, *PathItem]) error { + k := pair.Key() + pi := pair.Value() ln := 9999 // default to a high value to weight new content to the bottom. if p.low != nil { lpi := p.low.FindPath(k) @@ -147,7 +153,9 @@ func (p *Paths) MarshalYAMLInline() (interface{}, error) { } } mapped = append(mapped, &pathItem{pi, k, ln, nil}) + return nil } + _ = orderedmap.For[string, *PathItem](p.PathItems, action) nb := high.NewNodeBuilder(p, p.low) nb.Resolve = true diff --git a/datamodel/high/v3/paths_test.go b/datamodel/high/v3/paths_test.go index a2a847f..0f47bd7 100644 --- a/datamodel/high/v3/paths_test.go +++ b/datamodel/high/v3/paths_test.go @@ -48,7 +48,7 @@ func TestPaths_MarshalYAML(t *testing.T) { // mutate deprecated := true - high.PathItems["/beer"].Get.Deprecated = &deprecated + high.PathItems.GetOrZero("/beer").Get.Deprecated = &deprecated yml = `/foo/bar/bizzle: get: @@ -100,7 +100,7 @@ func TestPaths_MarshalYAMLInline(t *testing.T) { // mutate deprecated := true - high.PathItems["/beer"].Get.Deprecated = &deprecated + high.PathItems.GetOrZero("/beer").Get.Deprecated = &deprecated yml = `/foo/bar/bizzle: get: diff --git a/datamodel/low/v2/paths.go b/datamodel/low/v2/paths.go index 97647e6..1165b3c 100644 --- a/datamodel/low/v2/paths.go +++ b/datamodel/low/v2/paths.go @@ -6,6 +6,7 @@ package v2 import ( "crypto/sha256" "fmt" + "io" "sort" "strings" "sync" @@ -13,13 +14,14 @@ import ( "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) // Paths represents a low-level Swagger / OpenAPI Paths object. type Paths struct { - PathItems map[low.KeyReference[string]]low.ValueReference[*PathItem] + PathItems orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]] Extensions map[low.KeyReference[string]]low.ValueReference[any] } @@ -29,23 +31,30 @@ func (p *Paths) GetExtensions() map[low.KeyReference[string]]low.ValueReference[ } // 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 { - if k.Value == path { - return &j +func (p *Paths) FindPath(path string) (result *low.ValueReference[*PathItem]) { + action := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*PathItem]]) error { + if pair.Key().Value == path { + result = pair.ValuePtr() + return io.EOF } + return nil } - return nil + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*PathItem]](p.PathItems, action) + return result } // FindPathAndKey attempts to locate a PathItem instance, given a path key. -func (p *Paths) FindPathAndKey(path string) (*low.KeyReference[string], *low.ValueReference[*PathItem]) { - for k, j := range p.PathItems { - if k.Value == path { - return &k, &j +func (p *Paths) FindPathAndKey(path string) (key *low.KeyReference[string], value *low.ValueReference[*PathItem]) { + action := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*PathItem]]) error { + if pair.Key().Value == path { + key = pair.KeyPtr() + value = pair.ValuePtr() + return io.EOF } + return nil } - return nil, nil + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*PathItem]](p.PathItems, action) + return key, value } // FindExtension will attempt to locate an extension value given a name. @@ -68,7 +77,7 @@ func (p *Paths) Build(_, root *yaml.Node, idx *index.SpecIndex) error { currentNode *yaml.Node pathNode *yaml.Node } - pathsMap := make(map[low.KeyReference[string]]low.ValueReference[*PathItem]) + pathsMap := orderedmap.New[low.KeyReference[string], low.ValueReference[*PathItem]]() in := make(chan buildInput) out := make(chan pathBuildResult) done := make(chan struct{}) @@ -115,7 +124,7 @@ func (p *Paths) Build(_, root *yaml.Node, idx *index.SpecIndex) error { if !ok { break } - pathsMap[result.key] = result.value + pathsMap.Set(result.key, result.value) } close(done) wg.Done() @@ -154,14 +163,19 @@ func (p *Paths) Build(_, root *yaml.Node, idx *index.SpecIndex) error { // Hash will return a consistent SHA256 Hash of the PathItem object func (p *Paths) Hash() [32]byte { var f []string - l := make([]string, len(p.PathItems)) + l := make([]string, orderedmap.Len(p.PathItems)) keys := make(map[string]low.ValueReference[*PathItem]) z := 0 - for k := range p.PathItems { - keys[k.Value] = p.PathItems[k] - l[z] = k.Value + + action := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*PathItem]]) error { + k := pair.Key().Value + keys[k] = pair.Value() + l[z] = k z++ + return nil } + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*PathItem]](p.PathItems, action) + sort.Strings(l) for k := range l { f = append(f, low.GenerateHashString(keys[l[k]].Value)) diff --git a/datamodel/low/v3/paths.go b/datamodel/low/v3/paths.go index 1e5501e..a3613ee 100644 --- a/datamodel/low/v3/paths.go +++ b/datamodel/low/v3/paths.go @@ -6,6 +6,7 @@ package v3 import ( "crypto/sha256" "fmt" + "io" "sort" "strings" "sync" @@ -13,6 +14,7 @@ import ( "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -24,29 +26,36 @@ import ( // constraints. // - https://spec.openapis.org/oas/v3.1.0#paths-object type Paths struct { - PathItems map[low.KeyReference[string]]low.ValueReference[*PathItem] + PathItems orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]] Extensions map[low.KeyReference[string]]low.ValueReference[any] *low.Reference } // FindPath will attempt to locate a PathItem using the provided path string. -func (p *Paths) FindPath(path string) *low.ValueReference[*PathItem] { - for k, j := range p.PathItems { - if k.Value == path { - return &j +func (p *Paths) FindPath(path string) (result *low.ValueReference[*PathItem]) { + action := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*PathItem]]) error { + if pair.Key().Value == path { + result = pair.ValuePtr() + return io.EOF } + return nil } - return nil + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*PathItem]](p.PathItems, action) + return result } // FindPathAndKey attempts to locate a PathItem instance, given a path key. -func (p *Paths) FindPathAndKey(path string) (*low.KeyReference[string], *low.ValueReference[*PathItem]) { - for k, j := range p.PathItems { - if k.Value == path { - return &k, &j +func (p *Paths) FindPathAndKey(path string) (key *low.KeyReference[string], value *low.ValueReference[*PathItem]) { + action := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*PathItem]]) error { + if pair.Key().Value == path { + key = pair.KeyPtr() + value = pair.ValuePtr() + return io.EOF } + return nil } - return nil, nil + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*PathItem]](p.PathItems, action) + return key, value } // FindExtension will attempt to locate an extension using the specified string. @@ -75,7 +84,7 @@ func (p *Paths) Build(_, root *yaml.Node, idx *index.SpecIndex) error { currentNode *yaml.Node pathNode *yaml.Node } - pathsMap := make(map[low.KeyReference[string]]low.ValueReference[*PathItem]) + pathsMap := orderedmap.New[low.KeyReference[string], low.ValueReference[*PathItem]]() in := make(chan buildInput) out := make(chan buildResult) done := make(chan struct{}) @@ -122,7 +131,7 @@ func (p *Paths) Build(_, root *yaml.Node, idx *index.SpecIndex) error { if !ok { break } - pathsMap[result.key] = result.value + pathsMap.Set(result.key, result.value) } close(done) wg.Done() @@ -185,14 +194,19 @@ func (p *Paths) Build(_, root *yaml.Node, idx *index.SpecIndex) error { // Hash will return a consistent SHA256 Hash of the PathItem object func (p *Paths) Hash() [32]byte { var f []string - l := make([]string, len(p.PathItems)) + l := make([]string, orderedmap.Len(p.PathItems)) keys := make(map[string]low.ValueReference[*PathItem]) z := 0 - for k := range p.PathItems { - keys[k.Value] = p.PathItems[k] - l[z] = k.Value + + action := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*PathItem]]) error { + k := pair.Key().Value + keys[k] = pair.Value() + l[z] = k z++ + return nil } + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*PathItem]](p.PathItems, action) + sort.Strings(l) for k := range l { f = append(f, fmt.Sprintf("%s-%s", l[k], low.GenerateHashString(keys[l[k]].Value))) diff --git a/document_examples_test.go b/document_examples_test.go index 04131ff..1225726 100644 --- a/document_examples_test.go +++ b/document_examples_test.go @@ -5,12 +5,14 @@ package libopenapi import ( "fmt" - "github.com/pb33f/libopenapi/datamodel" "net/url" "os" "strings" "testing" + "github.com/pb33f/libopenapi/datamodel" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/datamodel/high" v3high "github.com/pb33f/libopenapi/datamodel/high/v3" low "github.com/pb33f/libopenapi/datamodel/low/base" @@ -47,7 +49,7 @@ func ExampleNewDocument_fromOpenAPI3Document() { } // get a count of the number of paths and schemas. - paths := len(v3Model.Model.Paths.PathItems) + paths := orderedmap.Len(v3Model.Model.Paths.PathItems) schemas := len(v3Model.Model.Components.Schemas) // print the number of paths and schemas in the document @@ -153,7 +155,7 @@ func ExampleNewDocument_fromSwaggerDocument() { } // get a count of the number of paths and schemas. - paths := len(v2Model.Model.Paths.PathItems) + paths := orderedmap.Len(v2Model.Model.Paths.PathItems) schemas := len(v2Model.Model.Definitions.Definitions) // print the number of paths and schemas in the document @@ -184,7 +186,7 @@ func ExampleNewDocument_fromUnknownVersion() { errors = errs } if len(errors) <= 0 { - paths = len(v3Model.Model.Paths.PathItems) + paths = orderedmap.Len(v3Model.Model.Paths.PathItems) schemas = len(v3Model.Model.Components.Schemas) } } @@ -194,7 +196,7 @@ func ExampleNewDocument_fromUnknownVersion() { errors = errs } if len(errors) <= 0 { - paths = len(v2Model.Model.Paths.PathItems) + paths = orderedmap.Len(v2Model.Model.Paths.PathItems) schemas = len(v2Model.Model.Definitions.Definitions) } } @@ -539,7 +541,7 @@ components: burgers: someBurger: sauce: ketchup - patty: meat + patty: meat anotherBurger: sauce: mayo patty: lamb` @@ -641,10 +643,10 @@ func ExampleNewDocument_modifyAndReRender() { } // capture original number of paths - originalPaths := len(v3Model.Model.Paths.PathItems) + originalPaths := orderedmap.Len(v3Model.Model.Paths.PathItems) // add the path to the document - v3Model.Model.Paths.PathItems["/new/path"] = newPath + v3Model.Model.Paths.PathItems.Set("/new/path", newPath) // render the document back to bytes and reload the model. rawBytes, _, newModel, errs := doc.RenderAndReload() @@ -655,7 +657,7 @@ func ExampleNewDocument_modifyAndReRender() { } // capture new number of paths after re-rendering - newPaths := len(newModel.Model.Paths.PathItems) + newPaths := orderedmap.Len(newModel.Model.Paths.PathItems) // print the number of paths and schemas in the document fmt.Printf("There were %d original paths. There are now %d paths in the document\n", originalPaths, newPaths) diff --git a/document_test.go b/document_test.go index 2c9be68..cc0e0a8 100644 --- a/document_test.go +++ b/document_test.go @@ -4,13 +4,22 @@ package libopenapi import ( "fmt" + "os" + "strings" + "testing" + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/what-changed/model" "github.com/stretchr/testify/assert" - "os" - "strings" - "testing" + + "github.com/pb33f/libopenapi/datamodel" + "github.com/pb33f/libopenapi/datamodel/high/base" + v3high "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/what-changed/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLoadDocument_Simple_V2(t *testing.T) { @@ -244,12 +253,12 @@ func TestDocument_RenderAndReload(t *testing.T) { // 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.GetOrZero("/pet/findByStatus").Get.OperationId = "findACakeInABakery" + h.Paths.PathItems.GetOrZero("/pet/findByStatus").Get.Responses.Codes["400"].Description = "a nice bucket of mice" + h.Paths.PathItems.GetOrZero("/pet/findByTags").Get.Tags = + append(h.Paths.PathItems.GetOrZero("/pet/findByTags").Get.Tags, "gurgle", "giggle") - h.Paths.PathItems["/pet/{petId}"].Delete.Security = append(h.Paths.PathItems["/pet/{petId}"].Delete.Security, + h.Paths.PathItems.GetOrZero("/pet/{petId}").Delete.Security = append(h.Paths.PathItems.GetOrZero("/pet/{petId}").Delete.Security, &base.SecurityRequirement{Requirements: map[string][]string{ "pizza-and-cake": {"read:abook", "write:asong"}, }}) @@ -262,13 +271,13 @@ func TestDocument_RenderAndReload(t *testing.T) { assert.NotNil(t, bytes) h = newDocModel.Model - assert.Equal(t, "findACakeInABakery", h.Paths.PathItems["/pet/findByStatus"].Get.OperationId) + assert.Equal(t, "findACakeInABakery", h.Paths.PathItems.GetOrZero("/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) + h.Paths.PathItems.GetOrZero("/pet/findByStatus").Get.Responses.Codes["400"].Description) + assert.Len(t, h.Paths.PathItems.GetOrZero("/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.Len(t, h.Paths.PathItems.GetOrZero("/pet/findByTags").Get.Tags, 3) + yu := h.Paths.PathItems.GetOrZero("/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) @@ -479,7 +488,7 @@ paths: } // extract operation. - operation := result.Model.Paths.PathItems["/something"].Get + operation := result.Model.Paths.PathItems.GetOrZero("/something").Get // print it out. fmt.Printf("param1: %s, is reference? %t, original reference %s", @@ -647,7 +656,7 @@ paths: // panic(errs) // } // -// assert.Equal(t, "crs", result.Model.Paths.PathItems["/test"].Get.Parameters[0].Name) +// assert.Equal(t, "crs", result.Model.Paths.PathItems.GetOrZero("/test").Get.Parameters[0].Name) //} func TestDocument_ExampleMap(t *testing.T) { @@ -858,3 +867,59 @@ components: assert.Len(t, m.Index.GetCircularReferences(), 0) } + +// Ensure document ordering is preserved after building and loading. +func TestDocument_Render_PreserveOrder(t *testing.T) { + t.Run("Paths", func(t *testing.T) { + const pathCount = 100 + doc, err := NewDocument([]byte(`openapi: 3.1.0`)) + require.NoError(t, err) + model, errs := doc.BuildV3Model() + require.Empty(t, errs) + pathItems := orderedmap.New[string, *v3high.PathItem]() + model.Model.Paths = &v3high.Paths{ + PathItems: pathItems, + } + for i := 0; i < pathCount; i++ { + pathItem := &v3high.PathItem{ + Get: &v3high.Operation{ + Parameters: make([]*v3high.Parameter, 0), + }, + } + pathName := fmt.Sprintf("/foobar/%d", i) + pathItems.Set(pathName, pathItem) + } + + checkOrder := func(t *testing.T, doc Document) { + model, errs := doc.BuildV3Model() + require.Empty(t, errs) + pathItems := model.Model.Paths.PathItems + require.Equal(t, pathCount, orderedmap.Len(pathItems)) + + var i int + _ = orderedmap.For(model.Model.Paths.PathItems, func(pair orderedmap.Pair[string, *v3high.PathItem]) error { + pathName := fmt.Sprintf("/foobar/%d", i) + assert.Equal(t, pathName, pair.Key()) + i++ + return nil + }) + assert.Equal(t, pathCount, i) + } + + checkOrder(t, doc) + yamlBytes, doc, _, errs := doc.RenderAndReload() + require.Empty(t, errs) + + t.Run("Unmarshalled YAML ordering", func(t *testing.T) { + // Reload YAML into new Document, verify ordering. + doc2, err := NewDocument(yamlBytes) + require.NoError(t, err) + checkOrder(t, doc2) + }) + + t.Run("Reloaded document ordering", func(t *testing.T) { + // Verify ordering of reloaded document after call to RenderAndReload(). + checkOrder(t, doc) + }) + }) +} diff --git a/what-changed/model/paths.go b/what-changed/model/paths.go index 5d63aa2..a6386ac 100644 --- a/what-changed/model/paths.go +++ b/what-changed/model/paths.go @@ -4,11 +4,13 @@ package model import ( - "github.com/pb33f/libopenapi/datamodel/low" - "github.com/pb33f/libopenapi/datamodel/low/v2" - v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "reflect" "sync" + + "github.com/pb33f/libopenapi/datamodel/low" + v2 "github.com/pb33f/libopenapi/datamodel/low/v2" + v3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/orderedmap" ) // PathsChanges represents changes found between two Swagger or OpenAPI Paths Objects. @@ -75,12 +77,16 @@ func ComparePaths(l, r any) *PathsChanges { lKeys := make(map[string]low.ValueReference[*v2.PathItem]) rKeys := make(map[string]low.ValueReference[*v2.PathItem]) - for k := range lPath.PathItems { - lKeys[k.Value] = lPath.PathItems[k] + laction := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*v2.PathItem]]) error { + lKeys[pair.Key().Value] = pair.Value() + return nil } - for k := range rPath.PathItems { - rKeys[k.Value] = rPath.PathItems[k] + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*v2.PathItem]](lPath.PathItems, laction) + raction := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*v2.PathItem]]) error { + rKeys[pair.Key().Value] = pair.Value() + return nil } + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*v2.PathItem]](rPath.PathItems, raction) // run every comparison in a thread. var mLock sync.Mutex @@ -146,12 +152,16 @@ func ComparePaths(l, r any) *PathsChanges { lKeys := make(map[string]low.ValueReference[*v3.PathItem]) rKeys := make(map[string]low.ValueReference[*v3.PathItem]) - for k := range lPath.PathItems { - lKeys[k.Value] = lPath.PathItems[k] + laction := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*v3.PathItem]]) error { + lKeys[pair.Key().Value] = pair.Value() + return nil } - for k := range rPath.PathItems { - rKeys[k.Value] = rPath.PathItems[k] + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*v3.PathItem]](lPath.PathItems, laction) + raction := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*v3.PathItem]]) error { + rKeys[pair.Key().Value] = pair.Value() + return nil } + _ = orderedmap.For[low.KeyReference[string], low.ValueReference[*v3.PathItem]](rPath.PathItems, raction) // run every comparison in a thread. var mLock sync.Mutex