From f3f2782ef8dd3fa69b9665c59b3c18890fe2998e Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 22 Jan 2024 22:23:54 +0100 Subject: [PATCH] Use ordered maps Signed-off-by: Pierre Fenoll --- .github/docs/openapi3.txt | 9 ++ .github/workflows/go.yml | 5 + go.mod | 3 + go.sum | 6 + maps.sh | 90 ++++++++------ openapi3/callback.go | 4 +- openapi3/issue513_test.go | 2 +- openapi3/loader.go | 43 +++++-- openapi3/maplike.go | 227 +++++++++++++++++++--------------- openapi3/maplike_test.go | 18 ++- openapi3/paths.go | 4 +- openapi3/response.go | 4 +- routers/legacy/router_test.go | 2 +- 13 files changed, 257 insertions(+), 160 deletions(-) diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 44e641f3b..c3bdca93e 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -150,6 +150,9 @@ func NewCallback(opts ...NewCallbackOption) *Callback func NewCallbackWithCapacity(cap int) *Callback NewCallbackWithCapacity builds a callback object of the given capacity. +func (callback *Callback) Iter() *callbackKV + Iter returns a pointer to the first pair, in insertion order. + func (callback Callback) JSONLookup(token string) (interface{}, error) JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable @@ -937,6 +940,9 @@ func (paths *Paths) InMatchingOrder() []string When matching URLs, concrete (non-templated) paths would be matched before their templated counterparts. +func (paths *Paths) Iter() *pathsKV + Iter returns a pointer to the first pair, in insertion order. + func (paths Paths) JSONLookup(token string) (interface{}, error) JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable @@ -1145,6 +1151,9 @@ func NewResponsesWithCapacity(cap int) *Responses func (responses *Responses) Default() *ResponseRef Default returns the default response +func (responses *Responses) Iter() *responsesKV + Iter returns a pointer to the first pair, in insertion order. + func (responses Responses) JSONLookup(token string) (interface{}, error) JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index acb3a0f0d..6dd358341 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -140,6 +140,11 @@ jobs: run: | [[ 31 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]] + - if: runner.os == 'Linux' + name: Ensure opaque usage of orderedmap package + run: | + ! git grep -InE orderedmap -- .github/docs/ + - if: runner.os == 'Linux' name: Missing validation of unknown fields in extensions run: | diff --git a/go.mod b/go.mod index 3d6ff7e83..e214f37f9 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,13 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/perimeterx/marshmallow v1.1.5 github.com/stretchr/testify v1.8.4 + github.com/wk8/go-ordered-map/v2 v2.1.8 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/swag v0.22.8 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum index 82537b7ad..d3877011f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= @@ -26,6 +30,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/maps.sh b/maps.sh index 4e0789176..57387ad1f 100755 --- a/maps.sh +++ b/maps.sh @@ -27,7 +27,7 @@ names+=('paths') [[ "${#types[@]}" = "${#value_types[@]}" ]] [[ "${#types[@]}" = "${#deref_vs[@]}" ]] [[ "${#types[@]}" = "${#names[@]}" ]] -[[ "${#types[@]}" = "$(git grep -InF ' m map[string]*' -- openapi3/loader.go | wc -l)" ]] +[[ "${#types[@]}" = "$(git grep -InF ' om map[string]*' -- openapi3/loader.go | wc -l)" ]] #FIXME: !map maplike_header() { @@ -36,10 +36,10 @@ package openapi3 import ( "encoding/json" - "sort" "strings" "github.com/go-openapi/jsonpointer" + orderedmap "github.com/wk8/go-ordered-map/v2" ) EOF @@ -73,9 +73,9 @@ maplike_NewWithCapa() { // New${type#'*'}WithCapacity builds a ${name} object of the given capacity. func New${type#'*'}WithCapacity(cap int) ${type} { if cap == 0 { - return &${type#'*'}{m: make(map[string]${value_type})} + return &${type#'*'}{om: orderedmap.New[string, ${value_type}]()} } - return &${type#'*'}{m: make(map[string]${value_type}, cap)} + return &${type#'*'}{om: orderedmap.New[string, ${value_type}](cap)} } EOF @@ -89,35 +89,35 @@ func (${name} ${type}) Value(key string) ${value_type} { if ${name}.Len() == 0 { return nil } - return ${name}.m[key] + return ${name}.om.Value(key) } // Set adds or replaces key 'key' of '${name}' with 'value'. // Note: '${name}' MUST be non-nil func (${name} ${type}) Set(key string, value ${value_type}) { - if ${name}.m == nil { - ${name}.m = make(map[string]${value_type}) + if ${name}.om == nil { + ${name}.om = New${type#'*'}WithCapacity(0).om } - ${name}.m[key] = value + _, _ = ${name}.om.Set(key, value) } // Len returns the amount of keys in ${name} excluding ${name}.Extensions. func (${name} ${type}) Len() int { - if ${name} == nil || ${name}.m == nil { + if ${name} == nil || ${name}.om == nil { return 0 } - return len(${name}.m) + return ${name}.om.Len() } // Map returns ${name} as a 'map'. // Note: iteration on Go maps is not ordered. func (${name} ${type}) Map() (m map[string]${value_type}) { - if ${name} == nil || len(${name}.m) == 0 { + if ${name} == nil || ${name}.om == nil { return make(map[string]${value_type}) } - m = make(map[string]${value_type}, len(${name}.m)) - for k, v := range ${name}.m { - m[k] = v + m = make(map[string]${value_type}, ${name}.Len()) + for pair := ${name}.Iter(); pair != nil; pair = pair.Next() { + m[pair.Key] = pair.Value } return } @@ -126,6 +126,25 @@ EOF } +maplike_IterNext() { + cat <>"$maplike" +type ${name}KV orderedmap.Pair[string, ${value_type}] //FIXME: pub? +// Iter returns a pointer to the first pair, in insertion order. +func (${name} ${type}) Iter() *${name}KV { + if ${name}.Len() == 0 { + return nil + } + return (*${name}KV)(${name}.om.Oldest()) +} +// Next returns a pointer to the next pair, in insertion order. +func (pair *${name}KV) Next() *${name}KV { + ompair := (*orderedmap.Pair[string, ${value_type}])(pair) + return (*${name}KV)(ompair.Next()) +} +EOF +} + + maplike_Pointable() { cat <>"$maplike" var _ jsonpointer.JSONPointable = (${type})(nil) @@ -151,36 +170,28 @@ maplike_UnMarsh() { cat <>"$maplike" // MarshalJSON returns the JSON encoding of ${type#'*'}. func (${name} ${type}) MarshalJSON() ([]byte, error) { - m := make(map[string]interface{}, ${name}.Len()+len(${name}.Extensions)) - for k, v := range ${name}.Extensions { - m[k] = v + om := orderedmap.New[string, interface{}](${name}.Len() + len(${name}.Extensions)) + for pair := ${name}.Iter(); pair != nil; pair = pair.Next() { + om.Set(pair.Key, pair.Value) } - for k, v := range ${name}.Map() { - m[k] = v + for k, v := range ${name}.Extensions { + om.Set(k, v) } - return json.Marshal(m) + return om.MarshalJSON() } // UnmarshalJSON sets ${type#'*'} to a copy of data. func (${name} ${type}) UnmarshalJSON(data []byte) (err error) { - var m map[string]interface{} - if err = json.Unmarshal(data, &m); err != nil { + om := orderedmap.New[string, interface{}]() + if err = json.Unmarshal(data, &om); err != nil { return } - ks := make([]string, 0, len(m)) - for k := range m { - ks = append(ks, k) - } - sort.Strings(ks) - - x := ${type#'*'}{ - Extensions: make(map[string]interface{}), - m: make(map[string]${value_type}, len(m)), - } + x := New${type#'*'}WithCapacity(om.Len()) + x.Extensions = make(map[string]interface{}) - for _, k := range ks { - v := m[k] + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + k, v := pair.Key, pair.Value if strings.HasPrefix(k, "x-") { x.Extensions[k] = v continue @@ -194,9 +205,9 @@ func (${name} ${type}) UnmarshalJSON(data []byte) (err error) { if err = vv.UnmarshalJSON(data); err != nil { return } - x.m[k] = &vv + x.Set(k, &vv) } - *${name} = x + *${name} = *x return } EOF @@ -221,8 +232,10 @@ test_body() { require.Equal(t, (${value_type})(nil), x.Value("key")) x.Set("key", &${value_type#'*'}{}) require.Equal(t, 1, x.Len()) - require.Equal(t, map[string]${value_type}{"key": {}}, x.Map()) - require.Equal(t, &${value_type#'*'}{}, x.Value("key")) + m := x.Map() + require.Equal(t, map[string]${value_type}{"key": {}}, m) + m["key"].Ref = "bla" + require.Equal(t, &${value_type#'*'}{Ref: "bla"}, x.Value("key")) }) }) @@ -242,6 +255,7 @@ for i in "${!types[@]}"; do type="$type" name="$name" value_type="$value_type" maplike_NewWithCapa type="$type" name="$name" value_type="$value_type" maplike_ValueSetLen + type="$type" name="$name" value_type="$value_type" maplike_IterNext type="$type" name="$name" deref_v="$deref_v" maplike_Pointable type="$type" name="$name" value_type="$value_type" maplike_UnMarsh [[ $((i+1)) != "${#types[@]}" ]] && echo >>"$maplike" diff --git a/openapi3/callback.go b/openapi3/callback.go index 13532b15c..b2b9b8625 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -3,6 +3,8 @@ package openapi3 import ( "context" "sort" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) // Callback is specified by OpenAPI/Swagger standard version 3. @@ -10,7 +12,7 @@ import ( type Callback struct { Extensions map[string]interface{} `json:"-" yaml:"-"` - m map[string]*PathItem + om *orderedmap.OrderedMap[string, *PathItem] } // NewCallback builds a Callback object with path items in insertion order. diff --git a/openapi3/issue513_test.go b/openapi3/issue513_test.go index 38454f672..25a63b0af 100644 --- a/openapi3/issue513_test.go +++ b/openapi3/issue513_test.go @@ -157,7 +157,7 @@ components: require.ErrorContains(t, err, `extra sibling fields: [schema]`) } -func TestIssue513KOMixesRefAlongWithOtherFieldsDisallowed(t *testing.T) { +func TestIssue513KOMixesRefAlongWithOtherFieldsDisallowed(t *testing.T) { //FIXME: drop? spec := ` openapi: "3.0.3" info: diff --git a/openapi3/loader.go b/openapi3/loader.go index 1128aaa78..3c9371ebb 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -308,7 +308,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv drill := func(cursor interface{}) (interface{}, error) { for _, pathPart := range strings.Split(fragment[1:], "/") { - pathPart = unescapeRefString(pathPart) + pathPart = strings.Replace(strings.Replace(pathPart, "~1", "/", -1), "~0", "~", -1) attempted := false switch c := cursor.(type) { @@ -332,11 +332,11 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv } case *Responses: - cursor = c.m // m map[string]*ResponseRef + cursor = c.Map() // om map[string]*ResponseRef case *Callback: - cursor = c.m // m map[string]*PathItem + cursor = c.Map() // om map[string]*PathItem case *Paths: - cursor = c.m // m map[string]*PathItem + cursor = c.Map() // om map[string]*PathItem } if !attempted { @@ -472,6 +472,10 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { return enc, nil } } + + if v := omdriller(val, fieldName); v != nil { + return v, nil + } } return nil, fmt.Errorf("struct field %q not found", fieldName) @@ -480,6 +484,33 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { } } +func omdriller(val reflect.Value, fieldName string) interface{} { + // TODO: ge -B1 '^\s+om [*]orderedmap' -- openapi3/ + switch tyname := val.Type().Name(); tyname { + case "Paths": + if om := val.Interface().(Paths).om; om != nil { + if v, ok := (*om).Get(fieldName); ok { + return v + } + } + + case "Responses": + if om := val.Interface().(Responses).om; om != nil { + if v, ok := (*om).Get(fieldName); ok { + return v + } + } + + case "Callback": + if om := val.Interface().(Callback).om; om != nil { + if v, ok := (*om).Get(fieldName); ok { + return v + } + } + } + return nil +} + func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { if ref != "" && ref[0] == '#' { return doc, ref, path, nil @@ -1091,10 +1122,6 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat return } -func unescapeRefString(ref string) string { - return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1) -} - func visitedLimit(visited []string, ref string) bool { visitedCount := 0 for _, v := range visited { diff --git a/openapi3/maplike.go b/openapi3/maplike.go index 1f438538e..3032f0a8b 100644 --- a/openapi3/maplike.go +++ b/openapi3/maplike.go @@ -2,18 +2,18 @@ package openapi3 import ( "encoding/json" - "sort" "strings" "github.com/go-openapi/jsonpointer" + orderedmap "github.com/wk8/go-ordered-map/v2" ) // NewResponsesWithCapacity builds a responses object of the given capacity. func NewResponsesWithCapacity(cap int) *Responses { if cap == 0 { - return &Responses{m: make(map[string]*ResponseRef)} + return &Responses{om: orderedmap.New[string, *ResponseRef]()} } - return &Responses{m: make(map[string]*ResponseRef, cap)} + return &Responses{om: orderedmap.New[string, *ResponseRef](cap)} } // Value returns the responses for key or nil @@ -21,39 +21,54 @@ func (responses *Responses) Value(key string) *ResponseRef { if responses.Len() == 0 { return nil } - return responses.m[key] + return responses.om.Value(key) } // Set adds or replaces key 'key' of 'responses' with 'value'. // Note: 'responses' MUST be non-nil func (responses *Responses) Set(key string, value *ResponseRef) { - if responses.m == nil { - responses.m = make(map[string]*ResponseRef) + if responses.om == nil { + responses.om = NewResponsesWithCapacity(0).om } - responses.m[key] = value + _, _ = responses.om.Set(key, value) } // Len returns the amount of keys in responses excluding responses.Extensions. func (responses *Responses) Len() int { - if responses == nil || responses.m == nil { + if responses == nil || responses.om == nil { return 0 } - return len(responses.m) + return responses.om.Len() } // Map returns responses as a 'map'. // Note: iteration on Go maps is not ordered. func (responses *Responses) Map() (m map[string]*ResponseRef) { - if responses == nil || len(responses.m) == 0 { + if responses == nil || responses.om == nil { return make(map[string]*ResponseRef) } - m = make(map[string]*ResponseRef, len(responses.m)) - for k, v := range responses.m { - m[k] = v + m = make(map[string]*ResponseRef, responses.Len()) + for pair := responses.Iter(); pair != nil; pair = pair.Next() { + m[pair.Key] = pair.Value } return } +type responsesKV orderedmap.Pair[string, *ResponseRef] //FIXME: pub? +// Iter returns a pointer to the first pair, in insertion order. +func (responses *Responses) Iter() *responsesKV { + if responses.Len() == 0 { + return nil + } + return (*responsesKV)(responses.om.Oldest()) +} + +// Next returns a pointer to the next pair, in insertion order. +func (pair *responsesKV) Next() *responsesKV { + ompair := (*orderedmap.Pair[string, *ResponseRef])(pair) + return (*responsesKV)(ompair.Next()) +} + var _ jsonpointer.JSONPointable = (*Responses)(nil) // JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable @@ -71,36 +86,28 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) { // MarshalJSON returns the JSON encoding of Responses. func (responses *Responses) MarshalJSON() ([]byte, error) { - m := make(map[string]interface{}, responses.Len()+len(responses.Extensions)) - for k, v := range responses.Extensions { - m[k] = v + om := orderedmap.New[string, interface{}](responses.Len() + len(responses.Extensions)) + for pair := responses.Iter(); pair != nil; pair = pair.Next() { + om.Set(pair.Key, pair.Value) } - for k, v := range responses.Map() { - m[k] = v + for k, v := range responses.Extensions { + om.Set(k, v) } - return json.Marshal(m) + return om.MarshalJSON() } // UnmarshalJSON sets Responses to a copy of data. func (responses *Responses) UnmarshalJSON(data []byte) (err error) { - var m map[string]interface{} - if err = json.Unmarshal(data, &m); err != nil { + om := orderedmap.New[string, interface{}]() + if err = json.Unmarshal(data, &om); err != nil { return } - ks := make([]string, 0, len(m)) - for k := range m { - ks = append(ks, k) - } - sort.Strings(ks) - - x := Responses{ - Extensions: make(map[string]interface{}), - m: make(map[string]*ResponseRef, len(m)), - } + x := NewResponsesWithCapacity(om.Len()) + x.Extensions = make(map[string]interface{}) - for _, k := range ks { - v := m[k] + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + k, v := pair.Key, pair.Value if strings.HasPrefix(k, "x-") { x.Extensions[k] = v continue @@ -114,18 +121,18 @@ func (responses *Responses) UnmarshalJSON(data []byte) (err error) { if err = vv.UnmarshalJSON(data); err != nil { return } - x.m[k] = &vv + x.Set(k, &vv) } - *responses = x + *responses = *x return } // NewCallbackWithCapacity builds a callback object of the given capacity. func NewCallbackWithCapacity(cap int) *Callback { if cap == 0 { - return &Callback{m: make(map[string]*PathItem)} + return &Callback{om: orderedmap.New[string, *PathItem]()} } - return &Callback{m: make(map[string]*PathItem, cap)} + return &Callback{om: orderedmap.New[string, *PathItem](cap)} } // Value returns the callback for key or nil @@ -133,39 +140,54 @@ func (callback *Callback) Value(key string) *PathItem { if callback.Len() == 0 { return nil } - return callback.m[key] + return callback.om.Value(key) } // Set adds or replaces key 'key' of 'callback' with 'value'. // Note: 'callback' MUST be non-nil func (callback *Callback) Set(key string, value *PathItem) { - if callback.m == nil { - callback.m = make(map[string]*PathItem) + if callback.om == nil { + callback.om = NewCallbackWithCapacity(0).om } - callback.m[key] = value + _, _ = callback.om.Set(key, value) } // Len returns the amount of keys in callback excluding callback.Extensions. func (callback *Callback) Len() int { - if callback == nil || callback.m == nil { + if callback == nil || callback.om == nil { return 0 } - return len(callback.m) + return callback.om.Len() } // Map returns callback as a 'map'. // Note: iteration on Go maps is not ordered. func (callback *Callback) Map() (m map[string]*PathItem) { - if callback == nil || len(callback.m) == 0 { + if callback == nil || callback.om == nil { return make(map[string]*PathItem) } - m = make(map[string]*PathItem, len(callback.m)) - for k, v := range callback.m { - m[k] = v + m = make(map[string]*PathItem, callback.Len()) + for pair := callback.Iter(); pair != nil; pair = pair.Next() { + m[pair.Key] = pair.Value } return } +type callbackKV orderedmap.Pair[string, *PathItem] //FIXME: pub? +// Iter returns a pointer to the first pair, in insertion order. +func (callback *Callback) Iter() *callbackKV { + if callback.Len() == 0 { + return nil + } + return (*callbackKV)(callback.om.Oldest()) +} + +// Next returns a pointer to the next pair, in insertion order. +func (pair *callbackKV) Next() *callbackKV { + ompair := (*orderedmap.Pair[string, *PathItem])(pair) + return (*callbackKV)(ompair.Next()) +} + var _ jsonpointer.JSONPointable = (*Callback)(nil) // JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable @@ -183,36 +205,28 @@ func (callback Callback) JSONLookup(token string) (interface{}, error) { // MarshalJSON returns the JSON encoding of Callback. func (callback *Callback) MarshalJSON() ([]byte, error) { - m := make(map[string]interface{}, callback.Len()+len(callback.Extensions)) - for k, v := range callback.Extensions { - m[k] = v + om := orderedmap.New[string, interface{}](callback.Len() + len(callback.Extensions)) + for pair := callback.Iter(); pair != nil; pair = pair.Next() { + om.Set(pair.Key, pair.Value) } - for k, v := range callback.Map() { - m[k] = v + for k, v := range callback.Extensions { + om.Set(k, v) } - return json.Marshal(m) + return om.MarshalJSON() } // UnmarshalJSON sets Callback to a copy of data. func (callback *Callback) UnmarshalJSON(data []byte) (err error) { - var m map[string]interface{} - if err = json.Unmarshal(data, &m); err != nil { + om := orderedmap.New[string, interface{}]() + if err = json.Unmarshal(data, &om); err != nil { return } - ks := make([]string, 0, len(m)) - for k := range m { - ks = append(ks, k) - } - sort.Strings(ks) + x := NewCallbackWithCapacity(om.Len()) + x.Extensions = make(map[string]interface{}) - x := Callback{ - Extensions: make(map[string]interface{}), - m: make(map[string]*PathItem, len(m)), - } - - for _, k := range ks { - v := m[k] + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + k, v := pair.Key, pair.Value if strings.HasPrefix(k, "x-") { x.Extensions[k] = v continue @@ -226,18 +240,18 @@ func (callback *Callback) UnmarshalJSON(data []byte) (err error) { if err = vv.UnmarshalJSON(data); err != nil { return } - x.m[k] = &vv + x.Set(k, &vv) } - *callback = x + *callback = *x return } // NewPathsWithCapacity builds a paths object of the given capacity. func NewPathsWithCapacity(cap int) *Paths { if cap == 0 { - return &Paths{m: make(map[string]*PathItem)} + return &Paths{om: orderedmap.New[string, *PathItem]()} } - return &Paths{m: make(map[string]*PathItem, cap)} + return &Paths{om: orderedmap.New[string, *PathItem](cap)} } // Value returns the paths for key or nil @@ -245,39 +259,54 @@ func (paths *Paths) Value(key string) *PathItem { if paths.Len() == 0 { return nil } - return paths.m[key] + return paths.om.Value(key) } // Set adds or replaces key 'key' of 'paths' with 'value'. // Note: 'paths' MUST be non-nil func (paths *Paths) Set(key string, value *PathItem) { - if paths.m == nil { - paths.m = make(map[string]*PathItem) + if paths.om == nil { + paths.om = NewPathsWithCapacity(0).om } - paths.m[key] = value + _, _ = paths.om.Set(key, value) } // Len returns the amount of keys in paths excluding paths.Extensions. func (paths *Paths) Len() int { - if paths == nil || paths.m == nil { + if paths == nil || paths.om == nil { return 0 } - return len(paths.m) + return paths.om.Len() } // Map returns paths as a 'map'. // Note: iteration on Go maps is not ordered. func (paths *Paths) Map() (m map[string]*PathItem) { - if paths == nil || len(paths.m) == 0 { + if paths == nil || paths.om == nil { return make(map[string]*PathItem) } - m = make(map[string]*PathItem, len(paths.m)) - for k, v := range paths.m { - m[k] = v + m = make(map[string]*PathItem, paths.Len()) + for pair := paths.Iter(); pair != nil; pair = pair.Next() { + m[pair.Key] = pair.Value } return } +type pathsKV orderedmap.Pair[string, *PathItem] //FIXME: pub? +// Iter returns a pointer to the first pair, in insertion order. +func (paths *Paths) Iter() *pathsKV { + if paths.Len() == 0 { + return nil + } + return (*pathsKV)(paths.om.Oldest()) +} + +// Next returns a pointer to the next pair, in insertion order. +func (pair *pathsKV) Next() *pathsKV { + ompair := (*orderedmap.Pair[string, *PathItem])(pair) + return (*pathsKV)(ompair.Next()) +} + var _ jsonpointer.JSONPointable = (*Paths)(nil) // JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable @@ -295,36 +324,28 @@ func (paths Paths) JSONLookup(token string) (interface{}, error) { // MarshalJSON returns the JSON encoding of Paths. func (paths *Paths) MarshalJSON() ([]byte, error) { - m := make(map[string]interface{}, paths.Len()+len(paths.Extensions)) - for k, v := range paths.Extensions { - m[k] = v + om := orderedmap.New[string, interface{}](paths.Len() + len(paths.Extensions)) + for pair := paths.Iter(); pair != nil; pair = pair.Next() { + om.Set(pair.Key, pair.Value) } - for k, v := range paths.Map() { - m[k] = v + for k, v := range paths.Extensions { + om.Set(k, v) } - return json.Marshal(m) + return om.MarshalJSON() } // UnmarshalJSON sets Paths to a copy of data. func (paths *Paths) UnmarshalJSON(data []byte) (err error) { - var m map[string]interface{} - if err = json.Unmarshal(data, &m); err != nil { + om := orderedmap.New[string, interface{}]() + if err = json.Unmarshal(data, &om); err != nil { return } - ks := make([]string, 0, len(m)) - for k := range m { - ks = append(ks, k) - } - sort.Strings(ks) - - x := Paths{ - Extensions: make(map[string]interface{}), - m: make(map[string]*PathItem, len(m)), - } + x := NewPathsWithCapacity(om.Len()) + x.Extensions = make(map[string]interface{}) - for _, k := range ks { - v := m[k] + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + k, v := pair.Key, pair.Value if strings.HasPrefix(k, "x-") { x.Extensions[k] = v continue @@ -338,8 +359,8 @@ func (paths *Paths) UnmarshalJSON(data []byte) (err error) { if err = vv.UnmarshalJSON(data); err != nil { return } - x.m[k] = &vv + x.Set(k, &vv) } - *paths = x + *paths = *x return } diff --git a/openapi3/maplike_test.go b/openapi3/maplike_test.go index 3f92fd68f..e67d1ec1f 100644 --- a/openapi3/maplike_test.go +++ b/openapi3/maplike_test.go @@ -25,8 +25,10 @@ func TestMaplikeMethods(t *testing.T) { require.Equal(t, (*ResponseRef)(nil), x.Value("key")) x.Set("key", &ResponseRef{}) require.Equal(t, 1, x.Len()) - require.Equal(t, map[string]*ResponseRef{"key": {}}, x.Map()) - require.Equal(t, &ResponseRef{}, x.Value("key")) + m := x.Map() + require.Equal(t, map[string]*ResponseRef{"key": {}}, m) + m["key"].Ref = "bla" + require.Equal(t, &ResponseRef{Ref: "bla"}, x.Value("key")) }) }) @@ -46,8 +48,10 @@ func TestMaplikeMethods(t *testing.T) { require.Equal(t, (*PathItem)(nil), x.Value("key")) x.Set("key", &PathItem{}) require.Equal(t, 1, x.Len()) - require.Equal(t, map[string]*PathItem{"key": {}}, x.Map()) - require.Equal(t, &PathItem{}, x.Value("key")) + m := x.Map() + require.Equal(t, map[string]*PathItem{"key": {}}, m) + m["key"].Ref = "bla" + require.Equal(t, &PathItem{Ref: "bla"}, x.Value("key")) }) }) @@ -67,8 +71,10 @@ func TestMaplikeMethods(t *testing.T) { require.Equal(t, (*PathItem)(nil), x.Value("key")) x.Set("key", &PathItem{}) require.Equal(t, 1, x.Len()) - require.Equal(t, map[string]*PathItem{"key": {}}, x.Map()) - require.Equal(t, &PathItem{}, x.Value("key")) + m := x.Map() + require.Equal(t, map[string]*PathItem{"key": {}}, m) + m["key"].Ref = "bla" + require.Equal(t, &PathItem{Ref: "bla"}, x.Value("key")) }) }) diff --git a/openapi3/paths.go b/openapi3/paths.go index ac4f58bbb..1307d6f9f 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -5,6 +5,8 @@ import ( "fmt" "sort" "strings" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) // Paths is specified by OpenAPI/Swagger standard version 3. @@ -12,7 +14,7 @@ import ( type Paths struct { Extensions map[string]interface{} `json:"-" yaml:"-"` - m map[string]*PathItem + om *orderedmap.OrderedMap[string, *PathItem] } // NewPaths builds a paths object with path items in insertion order. diff --git a/openapi3/response.go b/openapi3/response.go index d8b047257..9b43466d0 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -6,6 +6,8 @@ import ( "errors" "sort" "strconv" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) // Responses is specified by OpenAPI/Swagger 3.0 standard. @@ -13,7 +15,7 @@ import ( type Responses struct { Extensions map[string]interface{} `json:"-" yaml:"-"` - m map[string]*ResponseRef + om *orderedmap.OrderedMap[string, *ResponseRef] } // NewResponses builds a responses object with response objects in insertion order. diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go index 99704b19f..dead673b2 100644 --- a/routers/legacy/router_test.go +++ b/routers/legacy/router_test.go @@ -200,7 +200,7 @@ func TestRouter(t *testing.T) { } content := openapi3.NewContentWithJSONSchema(schema) responses := openapi3.NewResponses() - responses.Value("default").Value.Content = content + responses.Default().Value.Content = content doc.Paths.Set("/withExamples", &openapi3.PathItem{ Get: &openapi3.Operation{Responses: responses}, })