Skip to content

Commit db8be28

Browse files
committed
Use ordered maps
Signed-off-by: Pierre Fenoll <[email protected]>
1 parent e0f8dbd commit db8be28

File tree

12 files changed

+258
-159
lines changed

12 files changed

+258
-159
lines changed

.github/docs/openapi3.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ func NewCallback(opts ...NewCallbackOption) *Callback
150150
func NewCallbackWithCapacity(cap int) *Callback
151151
NewCallbackWithCapacity builds a callback object of the given capacity.
152152

153+
func (callback *Callback) Iter() *callbackKV
154+
Iter returns a pointer to the first pair, in insertion order.
155+
153156
func (callback Callback) JSONLookup(token string) (interface{}, error)
154157
JSONLookup implements
155158
https://github.com/go-openapi/jsonpointer#JSONPointable
@@ -937,6 +940,9 @@ func (paths *Paths) InMatchingOrder() []string
937940
When matching URLs, concrete (non-templated) paths would be matched before
938941
their templated counterparts.
939942

943+
func (paths *Paths) Iter() *pathsKV
944+
Iter returns a pointer to the first pair, in insertion order.
945+
940946
func (paths Paths) JSONLookup(token string) (interface{}, error)
941947
JSONLookup implements
942948
https://github.com/go-openapi/jsonpointer#JSONPointable
@@ -1145,6 +1151,9 @@ func NewResponsesWithCapacity(cap int) *Responses
11451151
func (responses *Responses) Default() *ResponseRef
11461152
Default returns the default response
11471153

1154+
func (responses *Responses) Iter() *responsesKV
1155+
Iter returns a pointer to the first pair, in insertion order.
1156+
11481157
func (responses Responses) JSONLookup(token string) (interface{}, error)
11491158
JSONLookup implements
11501159
https://github.com/go-openapi/jsonpointer#JSONPointable

.github/workflows/go.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ jobs:
140140
run: |
141141
[[ 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) ]]
142142
143+
- if: runner.os == 'Linux'
144+
name: Ensure opaque usage of orderedmap package
145+
run: |
146+
! git grep -InE orderedmap -- .github/docs/
147+
143148
- if: runner.os == 'Linux'
144149
name: Missing validation of unknown fields in extensions
145150
run: |

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ require (
99
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
1010
github.com/perimeterx/marshmallow v1.1.5
1111
github.com/stretchr/testify v1.8.4
12+
github.com/wk8/go-ordered-map/v2 v2.1.8
1213
gopkg.in/yaml.v3 v3.0.1
1314
)
1415

1516
require (
17+
github.com/bahlo/generic-list-go v0.2.0 // indirect
18+
github.com/buger/jsonparser v1.1.1 // indirect
1619
github.com/davecgh/go-spew v1.1.1 // indirect
1720
github.com/go-openapi/swag v0.22.8 // indirect
1821
github.com/josharian/intern v1.0.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
2+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
3+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
4+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
15
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
26
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
37
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
2630
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
2731
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
2832
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
33+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
34+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
2935
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3036
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
3137
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

maps.sh

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ names+=('paths')
2727
[[ "${#types[@]}" = "${#value_types[@]}" ]]
2828
[[ "${#types[@]}" = "${#deref_vs[@]}" ]]
2929
[[ "${#types[@]}" = "${#names[@]}" ]]
30-
[[ "${#types[@]}" = "$(git grep -InF ' m map[string]*' -- openapi3/loader.go | wc -l)" ]]
30+
[[ "${#types[@]}" = "$(git grep -InF ' om map[string]*' -- openapi3/loader.go | wc -l)" ]] #FIXME: !map
3131

3232

3333
maplike_header() {
@@ -36,10 +36,10 @@ package openapi3
3636
3737
import (
3838
"encoding/json"
39-
"sort"
4039
"strings"
4140
4241
"github.com/go-openapi/jsonpointer"
42+
orderedmap "github.com/wk8/go-ordered-map/v2"
4343
)
4444
4545
EOF
@@ -73,9 +73,9 @@ maplike_NewWithCapa() {
7373
// New${type#'*'}WithCapacity builds a ${name} object of the given capacity.
7474
func New${type#'*'}WithCapacity(cap int) ${type} {
7575
if cap == 0 {
76-
return &${type#'*'}{m: make(map[string]${value_type})}
76+
return &${type#'*'}{om: orderedmap.New[string, ${value_type}]()}
7777
}
78-
return &${type#'*'}{m: make(map[string]${value_type}, cap)}
78+
return &${type#'*'}{om: orderedmap.New[string, ${value_type}](cap)}
7979
}
8080
8181
EOF
@@ -89,35 +89,35 @@ func (${name} ${type}) Value(key string) ${value_type} {
8989
if ${name}.Len() == 0 {
9090
return nil
9191
}
92-
return ${name}.m[key]
92+
return ${name}.om.Value(key)
9393
}
9494
9595
// Set adds or replaces key 'key' of '${name}' with 'value'.
9696
// Note: '${name}' MUST be non-nil
9797
func (${name} ${type}) Set(key string, value ${value_type}) {
98-
if ${name}.m == nil {
99-
${name}.m = make(map[string]${value_type})
98+
if ${name}.om == nil {
99+
${name}.om = New${type#'*'}WithCapacity(0).om
100100
}
101-
${name}.m[key] = value
101+
_, _ = ${name}.om.Set(key, value)
102102
}
103103
104104
// Len returns the amount of keys in ${name} excluding ${name}.Extensions.
105105
func (${name} ${type}) Len() int {
106-
if ${name} == nil || ${name}.m == nil {
106+
if ${name} == nil || ${name}.om == nil {
107107
return 0
108108
}
109-
return len(${name}.m)
109+
return ${name}.om.Len()
110110
}
111111
112112
// Map returns ${name} as a 'map'.
113113
// Note: iteration on Go maps is not ordered.
114114
func (${name} ${type}) Map() (m map[string]${value_type}) {
115-
if ${name} == nil || len(${name}.m) == 0 {
115+
if ${name} == nil || ${name}.om == nil {
116116
return make(map[string]${value_type})
117117
}
118-
m = make(map[string]${value_type}, len(${name}.m))
119-
for k, v := range ${name}.m {
120-
m[k] = v
118+
m = make(map[string]${value_type}, ${name}.Len())
119+
for pair := ${name}.Iter(); pair != nil; pair = pair.Next() {
120+
m[pair.Key] = pair.Value
121121
}
122122
return
123123
}
@@ -126,6 +126,27 @@ EOF
126126
}
127127

128128

129+
maplike_IterNext() {
130+
cat <<EOF >>"$maplike"
131+
type ${name}KV orderedmap.Pair[string, ${value_type}] //FIXME: pub?
132+
// Iter returns a pointer to the first pair, in insertion order.
133+
func (${name} ${type}) Iter() *${name}KV {
134+
if ${name}.Len() == 0 {
135+
return nil
136+
}
137+
return (*${name}KV)(${name}.om.Oldest())
138+
}
139+
140+
// Next returns a pointer to the next pair, in insertion order.
141+
func (pair *${name}KV) Next() *${name}KV {
142+
ompair := (*orderedmap.Pair[string, ${value_type}])(pair)
143+
return (*${name}KV)(ompair.Next())
144+
}
145+
146+
EOF
147+
}
148+
149+
129150
maplike_Pointable() {
130151
cat <<EOF >>"$maplike"
131152
var _ jsonpointer.JSONPointable = (${type})(nil)
@@ -151,36 +172,28 @@ maplike_UnMarsh() {
151172
cat <<EOF >>"$maplike"
152173
// MarshalJSON returns the JSON encoding of ${type#'*'}.
153174
func (${name} ${type}) MarshalJSON() ([]byte, error) {
154-
m := make(map[string]interface{}, ${name}.Len()+len(${name}.Extensions))
155-
for k, v := range ${name}.Extensions {
156-
m[k] = v
175+
om := orderedmap.New[string, interface{}](${name}.Len() + len(${name}.Extensions))
176+
for pair := ${name}.Iter(); pair != nil; pair = pair.Next() {
177+
om.Set(pair.Key, pair.Value)
157178
}
158-
for k, v := range ${name}.Map() {
159-
m[k] = v
179+
for k, v := range ${name}.Extensions {
180+
om.Set(k, v)
160181
}
161-
return json.Marshal(m)
182+
return om.MarshalJSON()
162183
}
163184
164185
// UnmarshalJSON sets ${type#'*'} to a copy of data.
165186
func (${name} ${type}) UnmarshalJSON(data []byte) (err error) {
166-
var m map[string]interface{}
167-
if err = json.Unmarshal(data, &m); err != nil {
187+
om := orderedmap.New[string, interface{}]()
188+
if err = json.Unmarshal(data, &om); err != nil {
168189
return
169190
}
170191
171-
ks := make([]string, 0, len(m))
172-
for k := range m {
173-
ks = append(ks, k)
174-
}
175-
sort.Strings(ks)
176-
177-
x := ${type#'*'}{
178-
Extensions: make(map[string]interface{}),
179-
m: make(map[string]${value_type}, len(m)),
180-
}
192+
x := New${type#'*'}WithCapacity(om.Len())
193+
x.Extensions = make(map[string]interface{})
181194
182-
for _, k := range ks {
183-
v := m[k]
195+
for pair := om.Oldest(); pair != nil; pair = pair.Next() {
196+
k, v := pair.Key, pair.Value
184197
if strings.HasPrefix(k, "x-") {
185198
x.Extensions[k] = v
186199
continue
@@ -194,9 +207,9 @@ func (${name} ${type}) UnmarshalJSON(data []byte) (err error) {
194207
if err = vv.UnmarshalJSON(data); err != nil {
195208
return
196209
}
197-
x.m[k] = &vv
210+
x.Set(k, &vv)
198211
}
199-
*${name} = x
212+
*${name} = *x
200213
return
201214
}
202215
EOF
@@ -221,8 +234,10 @@ test_body() {
221234
require.Equal(t, (${value_type})(nil), x.Value("key"))
222235
x.Set("key", &${value_type#'*'}{})
223236
require.Equal(t, 1, x.Len())
224-
require.Equal(t, map[string]${value_type}{"key": {}}, x.Map())
225-
require.Equal(t, &${value_type#'*'}{}, x.Value("key"))
237+
m := x.Map()
238+
require.Equal(t, map[string]${value_type}{"key": {}}, m)
239+
m["key"].Ref = "bla"
240+
require.Equal(t, &${value_type#'*'}{Ref: "bla"}, x.Value("key"))
226241
})
227242
})
228243
@@ -242,6 +257,7 @@ for i in "${!types[@]}"; do
242257

243258
type="$type" name="$name" value_type="$value_type" maplike_NewWithCapa
244259
type="$type" name="$name" value_type="$value_type" maplike_ValueSetLen
260+
type="$type" name="$name" value_type="$value_type" maplike_IterNext
245261
type="$type" name="$name" deref_v="$deref_v" maplike_Pointable
246262
type="$type" name="$name" value_type="$value_type" maplike_UnMarsh
247263
[[ $((i+1)) != "${#types[@]}" ]] && echo >>"$maplike"

openapi3/callback.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ package openapi3
33
import (
44
"context"
55
"sort"
6+
7+
orderedmap "github.com/wk8/go-ordered-map/v2"
68
)
79

810
// Callback is specified by OpenAPI/Swagger standard version 3.
911
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object
1012
type Callback struct {
1113
Extensions map[string]interface{} `json:"-" yaml:"-"`
1214

13-
m map[string]*PathItem
15+
om *orderedmap.OrderedMap[string, *PathItem]
1416
}
1517

1618
// NewCallback builds a Callback object with path items in insertion order.

openapi3/loader.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv
308308

309309
drill := func(cursor interface{}) (interface{}, error) {
310310
for _, pathPart := range strings.Split(fragment[1:], "/") {
311-
pathPart = unescapeRefString(pathPart)
311+
pathPart = strings.Replace(strings.Replace(pathPart, "~1", "/", -1), "~0", "~", -1)
312312
attempted := false
313313

314314
switch c := cursor.(type) {
@@ -332,11 +332,11 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv
332332
}
333333

334334
case *Responses:
335-
cursor = c.m // m map[string]*ResponseRef
335+
cursor = c.Map() // om map[string]*ResponseRef
336336
case *Callback:
337-
cursor = c.m // m map[string]*PathItem
337+
cursor = c.Map() // om map[string]*PathItem
338338
case *Paths:
339-
cursor = c.m // m map[string]*PathItem
339+
cursor = c.Map() // om map[string]*PathItem
340340
}
341341

342342
if !attempted {
@@ -472,6 +472,10 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
472472
return enc, nil
473473
}
474474
}
475+
476+
if v := omdriller(val, fieldName); v != nil {
477+
return v, nil
478+
}
475479
}
476480
return nil, fmt.Errorf("struct field %q not found", fieldName)
477481

@@ -480,6 +484,33 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
480484
}
481485
}
482486

487+
func omdriller(val reflect.Value, fieldName string) interface{} {
488+
// TODO: ge -B1 '^\s+om [*]orderedmap' -- openapi3/
489+
switch tyname := val.Type().Name(); tyname {
490+
case "Paths":
491+
if om := val.Interface().(Paths).om; om != nil {
492+
if v, ok := (*om).Get(fieldName); ok {
493+
return v
494+
}
495+
}
496+
497+
case "Responses":
498+
if om := val.Interface().(Responses).om; om != nil {
499+
if v, ok := (*om).Get(fieldName); ok {
500+
return v
501+
}
502+
}
503+
504+
case "Callback":
505+
if om := val.Interface().(Callback).om; om != nil {
506+
if v, ok := (*om).Get(fieldName); ok {
507+
return v
508+
}
509+
}
510+
}
511+
return nil
512+
}
513+
483514
func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) {
484515
if ref != "" && ref[0] == '#' {
485516
return doc, ref, path, nil
@@ -1091,10 +1122,6 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat
10911122
return
10921123
}
10931124

1094-
func unescapeRefString(ref string) string {
1095-
return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1)
1096-
}
1097-
10981125
func visitedLimit(visited []string, ref string) bool {
10991126
visitedCount := 0
11001127
for _, v := range visited {

0 commit comments

Comments
 (0)