Skip to content

Commit

Permalink
Use ordered maps
Browse files Browse the repository at this point in the history
Signed-off-by: Pierre Fenoll <[email protected]>
  • Loading branch information
fenollp committed Jan 22, 2024
1 parent e0f8dbd commit f3f2782
Show file tree
Hide file tree
Showing 13 changed files with 257 additions and 160 deletions.
9 changes: 9 additions & 0 deletions .github/docs/openapi3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down Expand Up @@ -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=
Expand Down
90 changes: 52 additions & 38 deletions maps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -126,6 +126,25 @@ EOF
}


maplike_IterNext() {
cat <<EOF >>"$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 <<EOF >>"$maplike"
var _ jsonpointer.JSONPointable = (${type})(nil)
Expand All @@ -151,36 +170,28 @@ maplike_UnMarsh() {
cat <<EOF >>"$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
Expand All @@ -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
Expand All @@ -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"))
})
})
Expand All @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion openapi3/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ package openapi3
import (
"context"
"sort"

orderedmap "github.com/wk8/go-ordered-map/v2"
)

// Callback is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object
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.
Expand Down
2 changes: 1 addition & 1 deletion openapi3/issue513_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 35 additions & 8 deletions openapi3/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit f3f2782

Please sign in to comment.