From 0fedbbdfa640b2b3cf53b917caa085dd820aa40a Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 5 Dec 2022 00:43:31 +0100 Subject: [PATCH] Use ordered maps Signed-off-by: Pierre Fenoll --- .github/workflows/go.yml | 4 +- go.mod | 15 ++- go.sum | 6 + openapi2/openapi2.go | 12 +- openapi2/paths.go | 80 ++++++++++++ openapi2conv/issue558_test.go | 4 +- openapi2conv/issue573_test.go | 4 +- openapi2conv/openapi2_conv.go | 33 ++--- openapi3/callback.go | 116 +++++++++++++++--- openapi3/components.go | 15 +-- openapi3/internalize_refs.go | 27 ++-- openapi3/issue301_test.go | 22 ++-- openapi3/issue341_test.go | 7 +- openapi3/issue376_test.go | 2 +- openapi3/issue513_test.go | 44 +------ ...oad_cicular_ref_with_external_file_test.go | 3 - openapi3/load_with_go_embed_test.go | 5 +- openapi3/loader.go | 23 ++-- .../loader_empty_response_description_test.go | 6 +- openapi3/loader_issue220_test.go | 8 +- openapi3/loader_outside_refs_test.go | 2 +- openapi3/loader_read_from_uri_func_test.go | 2 +- openapi3/loader_recursive_ref_test.go | 6 +- openapi3/loader_relative_refs_test.go | 58 ++++----- openapi3/loader_test.go | 36 +++--- openapi3/openapi3.go | 8 +- openapi3/openapi3_test.go | 8 +- openapi3/operation.go | 4 +- openapi3/paths.go | 111 ++++++++++++++--- openapi3/security_scheme.go | 11 +- openapi3filter/validation_test.go | 30 ++--- routers/gorillamux/router.go | 5 +- routers/gorillamux/router_test.go | 46 +++---- routers/legacy/router.go | 5 +- routers/legacy/router_test.go | 34 ++--- 35 files changed, 521 insertions(+), 281 deletions(-) create mode 100644 openapi2/paths.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index dab35cb89..ec560dea7 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: true matrix: - go: ['1.16', '1.x'] + go: ['1.x'] os: - ubuntu-latest - windows-latest @@ -93,7 +93,7 @@ jobs: run: | ! git grep -InE 'json:"' | grep -v _test.go | grep -v yaml: - - if: runner.os == 'Linux' && matrix.go != '1.16' + - if: runner.os == 'Linux' name: nilness run: go run golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@latest ./... diff --git a/go.mod b/go.mod index 12a2f1af7..60e06e204 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/getkin/kin-openapi -go 1.16 +go 1.18 require ( github.com/go-openapi/jsonpointer v0.19.5 @@ -9,6 +9,17 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/perimeterx/marshmallow v1.1.4 github.com/stretchr/testify v1.8.1 - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.5 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.19.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum index 4d05787e4..b5aa95c38 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -40,6 +44,8 @@ github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/wk8/go-ordered-map/v2 v2.1.5 h1:jLbYIFyWQMUwHLO20cImlCRBoNc5lp0nmE2dvwcxc7k= +github.com/wk8/go-ordered-map/v2 v2.1.5/go.mod h1:9Xvgm2mV2kSq2SAm0Y608tBmu8akTzI7c2bz7/G7ZN4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 88835db95..07769e09f 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -18,7 +18,7 @@ type T struct { Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` Host string `json:"host,omitempty" yaml:"host,omitempty"` BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` - Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` + Paths *Paths `json:"paths,omitempty" yaml:"paths,omitempty"` Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` @@ -53,8 +53,8 @@ func (doc T) MarshalJSON() ([]byte, error) { if x := doc.BasePath; x != "" { m["basePath"] = x } - if x := doc.Paths; len(x) != 0 { - m["paths"] = x + if x := doc.Paths; x.Len() != 0 { + m["paths"] = x.om } if x := doc.Definitions; len(x) != 0 { m["definitions"] = x @@ -106,12 +106,12 @@ func (doc *T) UnmarshalJSON(data []byte) error { func (doc *T) AddOperation(path string, method string, operation *Operation) { if doc.Paths == nil { - doc.Paths = make(map[string]*PathItem) + doc.Paths = NewPaths() } - pathItem := doc.Paths[path] + pathItem := doc.Paths.Value(path) if pathItem == nil { pathItem = &PathItem{} - doc.Paths[path] = pathItem + doc.Paths.Set(path, pathItem) } pathItem.SetOperation(method, operation) } diff --git a/openapi2/paths.go b/openapi2/paths.go new file mode 100644 index 000000000..59af893da --- /dev/null +++ b/openapi2/paths.go @@ -0,0 +1,80 @@ +package openapi2 + +import ( + "encoding/json" + + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +type Paths struct { + om *orderedmap.OrderedMap[string, *PathItem] +} + +// MarshalJSON returns the JSON encoding of Paths. +func (paths *Paths) MarshalJSON() ([]byte, error) { + if paths == nil || paths.om == nil { + return []byte("{}"), nil + } + return paths.om.MarshalJSON() +} + +// UnmarshalJSON sets Paths to a copy of data. +func (paths *Paths) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &paths.om) +} + +func (paths *Paths) Value(key string) *PathItem { + // if paths == nil || paths.om == nil { + // return nil + // } + return paths.om.Value(key) +} + +func (paths *Paths) Set(key string, value *PathItem) { + // if paths != nil || paths.om != nil { + _, _ = paths.om.Set(key, value) + // } +} + +func (paths *Paths) Len() int { + if paths == nil || paths.om == nil { + return 0 + } + return paths.om.Len() +} + +func (paths *Paths) Iter() *pathsKV { + if paths == nil || paths.om == nil { + return nil + } + return (*pathsKV)(paths.om.Oldest()) +} + +type pathsKV orderedmap.Pair[string, *PathItem] //FIXME: pub? + +func (pair *pathsKV) Next() *pathsKV { + ompair := (*orderedmap.Pair[string, *PathItem])(pair) + return (*pathsKV)(ompair.Next()) +} + +// NewPathsWithCapacity builds a paths object of the given capacity. +func NewPathsWithCapacity(cap int) *Paths { + return &Paths{om: orderedmap.New[string, *PathItem](cap)} +} + +// NewPaths builds a paths object with path items in insertion order. +func NewPaths(opts ...NewPathsOption) *Paths { + paths := NewPathsWithCapacity(len(opts)) + for _, opt := range opts { + opt(paths) + } + return paths +} + +// NewPathsOption describes options to NewPaths func +type NewPathsOption func(*Paths) + +// WithPath adds paths as an option to NewPaths +func WithPath(path string, pathItem *PathItem) NewPathsOption { + return func(paths *Paths) { paths.Set(path, pathItem) } +} diff --git a/openapi2conv/issue558_test.go b/openapi2conv/issue558_test.go index 78661bf78..35ce85b4f 100644 --- a/openapi2conv/issue558_test.go +++ b/openapi2conv/issue558_test.go @@ -27,11 +27,11 @@ paths: ` doc3, err := v2v3YAML([]byte(spec)) require.NoError(t, err) - require.NotEmpty(t, doc3.Paths["/test"].Get.Deprecated) + require.NotEmpty(t, doc3.Paths.Value("/test").Get.Deprecated) _, err = yaml.Marshal(doc3) require.NoError(t, err) doc2, err := FromV3(doc3) require.NoError(t, err) - require.NotEmpty(t, doc2.Paths["/test"].Get.Deprecated) + require.NotEmpty(t, doc2.Paths.Value("/test").Get.Deprecated) } diff --git a/openapi2conv/issue573_test.go b/openapi2conv/issue573_test.go index cefac409e..a748024ae 100644 --- a/openapi2conv/issue573_test.go +++ b/openapi2conv/issue573_test.go @@ -36,13 +36,13 @@ func TestIssue573(t *testing.T) { // Make sure the response content appears for each mime-type originally // appeared in "produces". - pingGetContent := v3.Paths["/ping"].Get.Responses["200"].Value.Content + pingGetContent := v3.Paths.Value("/ping").Get.Responses["200"].Value.Content require.Len(t, pingGetContent, 2) require.Contains(t, pingGetContent, "application/toml") require.Contains(t, pingGetContent, "application/xml") // Is "produces" is not explicitly specified, default to "application/json". - pingPostContent := v3.Paths["/ping"].Post.Responses["200"].Value.Content + pingPostContent := v3.Paths.Value("/ping").Post.Responses["200"].Value.Content require.Len(t, pingPostContent, 1) require.Contains(t, pingPostContent, "application/json") } diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index c80e67201..61ea07405 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -7,6 +7,8 @@ import ( "sort" "strings" + orderedmap "github.com/wk8/go-ordered-map/v2" + "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" ) @@ -66,16 +68,16 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { } } - if paths := doc2.Paths; len(paths) != 0 { - doc3Paths := make(map[string]*openapi3.PathItem, len(paths)) - for path, pathItem := range paths { + if paths := doc2.Paths; paths.Len() != 0 { + doc3.Paths = openapi3.NewPathsWithCapacity(paths.Len()) + for pair := paths.Iter(); pair != nil; pair = pair.Next() { + path, pathItem := pair.Key, pair.Value r, err := ToV3PathItem(doc2, doc3.Components, pathItem, doc2.Consumes) if err != nil { return nil, err } - doc3Paths[path] = r + doc3.Paths.Set(path, r) } - doc3.Paths = doc3Paths } if responses := doc2.Responses; len(responses) != 0 { @@ -540,9 +542,9 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu result.Type = "oauth2" flows := &openapi3.OAuthFlows{} result.Flows = flows - scopesMap := make(map[string]string) + scopesMap := orderedmap.New[string, string](len(securityScheme.Scopes)) for scope, desc := range securityScheme.Scopes { - scopesMap[scope] = desc + scopesMap.Set(scope, desc) } flow := &openapi3.OAuthFlow{ AuthorizationURL: securityScheme.AuthorizationURL, @@ -611,7 +613,8 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { if isHTTP { doc2.Schemes = append(doc2.Schemes, "http") } - for path, pathItem := range doc3.Paths { + for pair := doc3.Paths.Iter(); pair != nil; pair = pair.Next() { + path, pathItem := pair.Key, pair.Value if pathItem == nil { continue } @@ -636,7 +639,7 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { params = append(params, p) } sort.Sort(params) - doc2.Paths[path].Parameters = params + doc2.Paths.Value(path).Parameters = params } for name, param := range doc3.Components.Parameters { @@ -1155,9 +1158,9 @@ func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*o return nil, nil } - result.Scopes = make(map[string]string, len(flow.Scopes)) - for scope, desc := range flow.Scopes { - result.Scopes[scope] = desc + result.Scopes = make(map[string]string, flow.Scopes.Len()) + for pair := flow.Scopes.Oldest(); pair != nil; pair = pair.Next() { + result.Scopes[pair.Key] = pair.Value } } default: @@ -1183,12 +1186,12 @@ func stripNonExtensions(extensions map[string]interface{}) map[string]interface{ func addPathExtensions(doc2 *openapi2.T, path string, extensions map[string]interface{}) { if doc2.Paths == nil { - doc2.Paths = make(map[string]*openapi2.PathItem) + doc2.Paths = openapi2.NewPaths() } - pathItem := doc2.Paths[path] + pathItem := doc2.Paths.Value(path) if pathItem == nil { pathItem = &openapi2.PathItem{} - doc2.Paths[path] = pathItem + doc2.Paths.Set(path, pathItem) } pathItem.Extensions = extensions } diff --git a/openapi3/callback.go b/openapi3/callback.go index 62cea72d8..7590761f2 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -2,20 +2,67 @@ package openapi3 import ( "context" + "encoding/json" "fmt" - "sort" "github.com/go-openapi/jsonpointer" + orderedmap "github.com/wk8/go-ordered-map/v2" ) -type Callbacks map[string]*CallbackRef +type Callbacks struct { + om *orderedmap.OrderedMap[string, *CallbackRef] +} + +// MarshalJSON returns the JSON encoding of Callbacks. +func (c *Callbacks) MarshalJSON() ([]byte, error) { + return c.om.MarshalJSON() +} + +// UnmarshalJSON sets Callbacks to a copy of data. +func (c *Callbacks) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &c.om) +} + +func (c *Callbacks) Value(key string) *CallbackRef { + // if c == nil || c.om == nil { + // return nil + // } + return c.om.Value(key) +} + +func (c *Callbacks) Set(key string, value *CallbackRef) { + // if c != nil || c.om != nil { + _, _ = c.om.Set(key, value) + // } +} + +func (c *Callbacks) Len() int { + if c == nil || c.om == nil { + return 0 + } + return c.om.Len() +} + +func (c *Callbacks) Iter() *callbacksKV { + if c == nil || c.om == nil { + return nil + } + return (*callbacksKV)(c.om.Oldest()) +} + +type callbacksKV orderedmap.Pair[string, *CallbackRef] //FIXME: pub? + +func (pair *callbacksKV) Next() *callbacksKV { + ompair := (*orderedmap.Pair[string, *CallbackRef])(pair) + return (*callbacksKV)(ompair.Next()) +} var _ jsonpointer.JSONPointable = (*Callbacks)(nil) // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (c Callbacks) JSONLookup(token string) (interface{}, error) { - ref, ok := c[token] - if ref == nil || !ok { +func (c *Callbacks) JSONLookup(token string) (interface{}, error) { + ref := c.Value(token) + if ref == nil { return nil, fmt.Errorf("object has no field %q", token) } @@ -27,19 +74,60 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) { // 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 map[string]*PathItem +type Callback struct { + om *orderedmap.OrderedMap[string, *PathItem] +} + +// MarshalJSON returns the JSON encoding of Callback. +func (callback *Callback) MarshalJSON() ([]byte, error) { + return callback.om.MarshalJSON() +} + +// UnmarshalJSON sets Callback to a copy of data. +func (callback *Callback) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &callback.om) +} + +func (callback *Callback) Value(key string) *PathItem { + // if callback == nil || callback.om == nil { + // return nil + // } + return callback.om.Value(key) +} + +func (callback *Callback) Set(key string, value *PathItem) { + // if callback != nil || callback.om != nil { + _, _ = callback.om.Set(key, value) + // } +} + +func (callback *Callback) Len() int { + if callback == nil || callback.om == nil { + return 0 + } + return callback.om.Len() +} + +func (callback *Callback) Iter() *callbackKV { + if callback == nil || callback.om == nil { + return nil + } + return (*callbackKV)(callback.om.Oldest()) +} + +type callbackKV orderedmap.Pair[string, *PathItem] //FIXME: pub? + +func (pair *callbackKV) Next() *callbackKV { + ompair := (*orderedmap.Pair[string, *PathItem])(pair) + return (*callbackKV)(ompair.Next()) +} // Validate returns an error if Callback does not comply with the OpenAPI spec. -func (callback Callback) Validate(ctx context.Context, opts ...ValidationOption) error { +func (callback *Callback) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - keys := make([]string, 0, len(callback)) - for key := range callback { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - v := callback[key] + for pair := callback.Iter(); pair != nil; pair = pair.Next() { + v := pair.Value if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/components.go b/openapi3/components.go index 0981e8bfe..a06b2979b 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -21,7 +21,7 @@ type Components struct { SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` Links Links `json:"links,omitempty" yaml:"links,omitempty"` - Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Callbacks *Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` } func NewComponents() Components { @@ -58,8 +58,8 @@ func (components Components) MarshalJSON() ([]byte, error) { if x := components.Links; len(x) != 0 { m["links"] = x } - if x := components.Callbacks; len(x) != 0 { - m["callbacks"] = x + if x := components.Callbacks; x.Len() != 0 { + m["callbacks"] = x.om } return json.Marshal(m) } @@ -209,13 +209,8 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp } } - callbacks := make([]string, 0, len(components.Callbacks)) - for name := range components.Callbacks { - callbacks = append(callbacks, name) - } - sort.Strings(callbacks) - for _, k := range callbacks { - v := components.Callbacks[k] + for pair := components.Callbacks.Iter(); pair != nil; pair = pair.Next() { + k, v := pair.Key, pair.Value if err = ValidateIdentifier(k); err != nil { return fmt.Errorf("callback %q: %w", k, err) } diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index acb83cd0c..b6ac00361 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -4,6 +4,8 @@ import ( "context" "path/filepath" "strings" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) type RefNameResolver func(string) string @@ -198,11 +200,8 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver, return false } name := refNameResolver(c.Ref) - if doc.Components.Callbacks == nil { - doc.Components.Callbacks = make(Callbacks) - } c.Ref = "#/components/callbacks/" + name - doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value} + doc.Components.Callbacks.Set(name, &CallbackRef{Value: c.Value}) return true } @@ -291,8 +290,9 @@ func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver, p doc.derefContent(r.Content, refNameResolver, parentIsExternal) } -func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver, parentIsExternal bool) { - for _, ops := range paths { +func (doc *T) derefPaths(paths *orderedmap.OrderedMap[string, *PathItem], refNameResolver RefNameResolver, parentIsExternal bool) { + for pair := paths.Oldest(); pair != nil; pair = pair.Next() { + ops := pair.Value // inline full operations ops.Ref = "" @@ -305,10 +305,12 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso if op.RequestBody != nil && op.RequestBody.Value != nil { doc.derefRequestBody(*op.RequestBody.Value, refNameResolver, parentIsExternal || isExternal) } - for _, cb := range op.Callbacks { + for pair := op.Callbacks.Iter(); pair != nil; pair = pair.Next() { + cb := pair.Value isExternal := doc.addCallbackToSpec(cb, refNameResolver, parentIsExternal) if cb.Value != nil { - doc.derefPaths(*cb.Value, refNameResolver, parentIsExternal || isExternal) + cbValue := (*orderedmap.OrderedMap[string, *PathItem])(cb.Value.om) + doc.derefPaths(cbValue, refNameResolver, parentIsExternal || isExternal) } } doc.derefResponses(op.Responses, refNameResolver, parentIsExternal) @@ -373,14 +375,17 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri } doc.derefExamples(components.Examples, refNameResolver, false) doc.derefLinks(components.Links, refNameResolver, false) - for _, cb := range components.Callbacks { + + for pair := components.Callbacks.Iter(); pair != nil; pair = pair.Next() { + cb := pair.Value isExternal := doc.addCallbackToSpec(cb, refNameResolver, false) if cb != nil && cb.Value != nil { cb.Ref = "" // always dereference the top level - doc.derefPaths(*cb.Value, refNameResolver, isExternal) + cbValue := (*orderedmap.OrderedMap[string, *PathItem])(cb.Value.om) + doc.derefPaths(cbValue, refNameResolver, isExternal) } } } - doc.derefPaths(doc.Paths, refNameResolver, false) + doc.derefPaths(doc.Paths.om, refNameResolver, false) } diff --git a/openapi3/issue301_test.go b/openapi3/issue301_test.go index a0225fdb8..0911778bd 100644 --- a/openapi3/issue301_test.go +++ b/openapi3/issue301_test.go @@ -16,13 +16,19 @@ func TestIssue301(t *testing.T) { err = doc.Validate(sl.Context) require.NoError(t, err) - transCallbacks := doc.Paths["/trans"].Post.Callbacks["transactionCallback"].Value - require.Equal(t, "object", (*transCallbacks)["http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}"].Post.RequestBody. - Value.Content["application/json"].Schema. - Value.Type) + require.Equal(t, "object", doc.Paths.Value("/trans"). + Post.Callbacks. + Value("transactionCallback").Value. + Value("http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}"). + Post.RequestBody.Value. + Content["application/json"].Schema.Value. + Type) - otherCallbacks := doc.Paths["/other"].Post.Callbacks["myEvent"].Value - require.Equal(t, "boolean", (*otherCallbacks)["{$request.query.queryUrl}"].Post.RequestBody. - Value.Content["application/json"].Schema. - Value.Type) + require.Equal(t, "boolean", doc.Paths.Value("/other"). + Post.Callbacks. + Value("myEvent").Value. + Value("{$request.query.queryUrl}"). + Post.RequestBody.Value. + Content["application/json"].Schema.Value. + Type) } diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go index 15ea9d48c..8fd3360e3 100644 --- a/openapi3/issue341_test.go +++ b/openapi3/issue341_test.go @@ -22,5 +22,10 @@ func TestIssue341(t *testing.T) { require.NoError(t, err) require.JSONEq(t, `{"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`, string(bs)) - require.Equal(t, "string", doc.Paths["/testpath"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, "string", doc.Paths. + Value("/testpath").Get. + Responses["200"].Value. + Content["application/json"]. + Schema.Value. + Type) } diff --git a/openapi3/issue376_test.go b/openapi3/issue376_test.go index 825f1d1ac..fd9286041 100644 --- a/openapi3/issue376_test.go +++ b/openapi3/issue376_test.go @@ -38,7 +38,7 @@ info: require.Equal(t, "An API", doc.Info.Title) require.Equal(t, 2, len(doc.Components.Schemas)) - require.Equal(t, 0, len(doc.Paths)) + require.Equal(t, 0, doc.Paths.Len()) require.Equal(t, "string", doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type) } diff --git a/openapi3/issue513_test.go b/openapi3/issue513_test.go index 332b9226e..d37af9bc6 100644 --- a/openapi3/issue513_test.go +++ b/openapi3/issue513_test.go @@ -87,7 +87,10 @@ components: sl := NewLoader() doc, err := sl.LoadFromData([]byte(spec)) require.NoError(t, err) - require.Contains(t, doc.Paths["/v1/operation"].Delete.Responses["default"].Value.Extensions, `x-my-extension`) + require.Contains(t, doc.Paths.Value("/v1/operation"). + Delete. + Responses["default"].Value. + Extensions, `x-my-extension`) err = doc.Validate(sl.Context) require.ErrorContains(t, err, `extra sibling fields: [schema]`) } @@ -126,48 +129,13 @@ components: type: string `[1:] sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) require.NoError(t, err) + err = doc.Validate(sl.Context) require.ErrorContains(t, err, `extra sibling fields: [description]`) -} - -func TestIssue513KOMixesRefAlongWithOtherFieldsAllowed(t *testing.T) { - spec := ` -openapi: "3.0.3" -info: - title: 'My app' - version: 1.0.0 - description: 'An API' -paths: - /v1/operation: - delete: - summary: Delete something - responses: - 200: - description: A sibling field that the spec says is ignored - $ref: '#/components/responses/SomeResponseBody' -components: - responses: - SomeResponseBody: - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - schemas: - Error: - type: object - description: An error response body. - properties: - message: - description: A detailed message describing the error. - type: string -`[1:] - sl := NewLoader() - doc, err := sl.LoadFromData([]byte(spec)) - require.NoError(t, err) err = doc.Validate(sl.Context, AllowExtraSiblingFields("description")) require.NoError(t, err) } diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go index 9bcaaf77f..8c2de7819 100644 --- a/openapi3/load_cicular_ref_with_external_file_test.go +++ b/openapi3/load_cicular_ref_with_external_file_test.go @@ -1,6 +1,3 @@ -//go:build go1.16 -// +build go1.16 - package openapi3_test import ( diff --git a/openapi3/load_with_go_embed_test.go b/openapi3/load_with_go_embed_test.go index e0fb915ba..88100a333 100644 --- a/openapi3/load_with_go_embed_test.go +++ b/openapi3/load_with_go_embed_test.go @@ -1,6 +1,3 @@ -//go:build go1.16 -// +build go1.16 - package openapi3_test import ( @@ -30,6 +27,6 @@ func Example() { panic(err) } - fmt.Println(doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Type) + fmt.Println(doc.Paths.Value("/foo").Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Type) // Output: string } diff --git a/openapi3/loader.go b/openapi3/loader.go index 72ab8c46a..92384f05d 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -235,7 +235,8 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { } } - for _, component := range components.Callbacks { + for pair := components.Callbacks.Iter(); pair != nil; pair = pair.Next() { + component := pair.Value if err = loader.resolveCallbackRef(doc, component, location); err != nil { return } @@ -243,7 +244,8 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { } // Visit all operations - for entrypoint, pathItem := range doc.Paths { + for pair := doc.Paths.Iter(); pair != nil; pair = pair.Next() { + entrypoint, pathItem := pair.Key, pair.Value if pathItem == nil { continue } @@ -878,8 +880,8 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen return nil } - for entrypoint, pathItem := range *value { - entrypoint, pathItem := entrypoint, pathItem + for pair := value.Iter(); pair != nil; pair = pair.Next() { + entrypoint, pathItem := pair.Key, pair.Value err = func() (err error) { key := "-" if documentPath != nil { @@ -916,15 +918,11 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen if doc.Components == nil || doc.Components.Callbacks == nil { return failedToResolveRefFragmentPart(ref, "callbacks") } - resolved := doc.Components.Callbacks[id] + resolved := doc.Components.Callbacks.Value(id) if resolved == nil { return failedToResolveRefFragmentPart(ref, id) } - - for _, p := range *resolved.Value { - *pathItem = *p - break - } + *pathItem = *resolved.Value.Iter().Value } } return loader.resolvePathItemRefContinued(doc, pathItem, documentPath) @@ -1009,7 +1007,7 @@ func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *Pa if doc.Paths == nil { return failedToResolveRefFragmentPart(ref, "paths") } - resolved := doc.Paths[id] + resolved := doc.Paths.Value(id) if resolved == nil { return failedToResolveRefFragmentPart(ref, id) } @@ -1041,7 +1039,8 @@ func (loader *Loader) resolvePathItemRefContinued(doc *T, pathItem *PathItem, do return } } - for _, callback := range operation.Callbacks { + for pair := operation.Callbacks.Iter(); pair != nil; pair = pair.Next() { + callback := pair.Value if err = loader.resolveCallbackRef(doc, callback, documentPath); err != nil { return } diff --git a/openapi3/loader_empty_response_description_test.go b/openapi3/loader_empty_response_description_test.go index 3c4b6bffd..5d614a5aa 100644 --- a/openapi3/loader_empty_response_description_test.go +++ b/openapi3/loader_empty_response_description_test.go @@ -36,7 +36,7 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) - got := doc.Paths["/path1"].Get.Responses["200"].Value.Description + got := doc.Paths.Value("/path1").Get.Responses["200"].Value.Description expected := "" require.Equal(t, &expected, got) t.Log("Empty description provided: valid spec") @@ -49,7 +49,7 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) - got := doc.Paths["/path1"].Get.Responses["200"].Value.Description + got := doc.Paths.Value("/path1").Get.Responses["200"].Value.Description expected := "My response" require.Equal(t, &expected, got) t.Log("Non-empty description provided: valid spec") @@ -61,7 +61,7 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { loader := NewLoader() doc, err := loader.LoadFromData(data) require.NoError(t, err) - got := doc.Paths["/path1"].Get.Responses["200"].Value.Description + got := doc.Paths.Value("/path1").Get.Responses["200"].Value.Description require.Nil(t, got) t.Log("No description provided: invalid spec") err = doc.Validate(loader.Context) diff --git a/openapi3/loader_issue220_test.go b/openapi3/loader_issue220_test.go index 57a44d5d0..717cf7c87 100644 --- a/openapi3/loader_issue220_test.go +++ b/openapi3/loader_issue220_test.go @@ -22,6 +22,12 @@ func TestIssue220(t *testing.T) { err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "integer", doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["bar"].Value.Type) + require.Equal(t, "integer", doc.Paths. + Value("/foo").Get. + Responses["200"].Value. + Content["application/json"]. + Schema.Value. + Properties["bar"].Value. + Type) } } diff --git a/openapi3/loader_outside_refs_test.go b/openapi3/loader_outside_refs_test.go index 5cec93452..b40656d9c 100644 --- a/openapi3/loader_outside_refs_test.go +++ b/openapi3/loader_outside_refs_test.go @@ -16,5 +16,5 @@ func TestLoadOutsideRefs(t *testing.T) { err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "string", doc.Paths["/service"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Items.Value.AllOf[0].Value.Properties["created_at"].Value.Type) + require.Equal(t, "string", doc.Paths.Value("/service").Get.Responses["200"].Value.Content["application/json"].Schema.Value.Items.Value.AllOf[0].Value.Properties["created_at"].Value.Type) } diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go index 8fee2f4c2..c43206ad0 100644 --- a/openapi3/loader_read_from_uri_func_test.go +++ b/openapi3/loader_read_from_uri_func_test.go @@ -20,7 +20,7 @@ func TestLoaderReadFromURIFunc(t *testing.T) { require.NoError(t, err) require.NotNil(t, doc) require.NoError(t, doc.Validate(loader.Context)) - require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example) + require.Equal(t, "bar", doc.Paths.Value("/foo").Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example) } type multipleSourceLoaderExample struct { diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go index 924cb6be8..0ce6d9952 100644 --- a/openapi3/loader_recursive_ref_test.go +++ b/openapi3/loader_recursive_ref_test.go @@ -13,9 +13,9 @@ func TestLoaderSupportsRecursiveReference(t *testing.T) { require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example) - require.Equal(t, "ErrorDetails", doc.Paths["/foo"].Get.Responses.Get(400).Value.Content.Get("application/json").Schema.Value.Title) - require.Equal(t, "ErrorDetails", doc.Paths["/double-ref-foo"].Get.Responses.Get(400).Value.Content.Get("application/json").Schema.Value.Title) + require.Equal(t, "bar", doc.Paths.Value("/foo").Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example) + require.Equal(t, "ErrorDetails", doc.Paths.Value("/foo").Get.Responses.Get(400).Value.Content.Get("application/json").Schema.Value.Title) + require.Equal(t, "ErrorDetails", doc.Paths.Value("/double-ref-foo").Get.Responses.Get(400).Value.Content.Get("application/json").Schema.Value.Title) } func TestIssue447(t *testing.T) { diff --git a/openapi3/loader_relative_refs_test.go b/openapi3/loader_relative_refs_test.go index 50d2c7d24..a5c6e1753 100644 --- a/openapi3/loader_relative_refs_test.go +++ b/openapi3/loader_relative_refs_test.go @@ -81,42 +81,42 @@ var refTestDataEntries = []refTestDataEntry{ name: "PathParameterRef", contentTemplate: externalPathParameterRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test/{id}"].Parameters[0].Value.Name) - require.Equal(t, "id", doc.Paths["/test/{id}"].Parameters[0].Value.Name) + require.NotNil(t, doc.Paths.Value("/test/{id}").Parameters[0].Value.Name) + require.Equal(t, "id", doc.Paths.Value("/test/{id}").Parameters[0].Value.Name) }, }, { name: "PathOperationParameterRef", contentTemplate: externalPathOperationParameterRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test/{id}"].Get.Parameters[0].Value) - require.Equal(t, "id", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Name) + require.NotNil(t, doc.Paths.Value("/test/{id}").Get.Parameters[0].Value) + require.Equal(t, "id", doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Name) }, }, { name: "PathOperationRequestBodyRef", contentTemplate: externalPathOperationRequestBodyRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value) - require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content) + require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value) + require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value.Content) }, }, { name: "PathOperationResponseRef", contentTemplate: externalPathOperationResponseRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses["default"].Value) desc := "description" - require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) + require.Equal(t, &desc, doc.Paths.Value("/test").Post.Responses["default"].Value.Description) }, }, { name: "PathOperationParameterSchemaRef", contentTemplate: externalPathOperationParameterSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value) - require.Equal(t, "string", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value.Type) - require.Equal(t, "id", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Name) + require.NotNil(t, doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Schema.Value) + require.Equal(t, "string", doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Schema.Value.Type) + require.Equal(t, "id", doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Name) }, }, @@ -124,7 +124,7 @@ var refTestDataEntries = []refTestDataEntry{ name: "PathOperationParameterRefWithContentInQuery", contentTemplate: externalPathOperationParameterWithContentInQueryTemplate, testFunc: func(t *testing.T, doc *T) { - schemaRef := doc.Paths["/test/{id}"].Get.Parameters[0].Value.Content["application/json"].Schema + schemaRef := doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Content["application/json"].Schema require.NotNil(t, schemaRef.Value) require.Equal(t, "string", schemaRef.Value.Type) }, @@ -134,36 +134,36 @@ var refTestDataEntries = []refTestDataEntry{ name: "PathOperationRequestBodyExampleRef", contentTemplate: externalPathOperationRequestBodyExampleRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value) - require.Equal(t, "description", doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description) + require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value) + require.Equal(t, "description", doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description) }, }, { name: "PathOperationReqestBodyContentSchemaRef", contentTemplate: externalPathOperationReqestBodyContentSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value) - require.Equal(t, "string", doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) + require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Schema.Value) + require.Equal(t, "string", doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) }, }, { name: "PathOperationResponseExampleRef", contentTemplate: externalPathOperationResponseExampleRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses["default"].Value) desc := "testdescription" - require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) - require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Examples["application/json"].Value.Description) + require.Equal(t, &desc, doc.Paths.Value("/test").Post.Responses["default"].Value.Description) + require.Equal(t, "description", doc.Paths.Value("/test").Post.Responses["default"].Value.Content["application/json"].Examples["application/json"].Value.Description) }, }, { name: "PathOperationResponseSchemaRef", contentTemplate: externalPathOperationResponseSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses["default"].Value) desc := "testdescription" - require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) - require.Equal(t, "string", doc.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, &desc, doc.Paths.Value("/test").Post.Responses["default"].Value.Description) + require.Equal(t, "string", doc.Paths.Value("/test").Post.Responses["default"].Value.Content["application/json"].Schema.Value.Type) }, }, { @@ -178,8 +178,8 @@ var refTestDataEntries = []refTestDataEntry{ name: "RequestResponseHeaderRef", contentTemplate: externalRequestResponseHeaderRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) - require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + require.Equal(t, "description", doc.Paths.Value("/test").Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) }, }, } @@ -792,9 +792,9 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ name: "PathRef", contentTemplate: relativePathDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/pets"]) - require.NotNil(t, doc.Paths["/pets"].Get.Responses["200"]) - require.NotNil(t, doc.Paths["/pets"].Get.Responses["200"].Value.Content["application/json"]) + require.NotNil(t, doc.Paths.Value("/pets")) + require.NotNil(t, doc.Paths.Value("/pets").Get.Responses["200"]) + require.NotNil(t, doc.Paths.Value("/pets").Get.Responses["200"].Value.Content["application/json"]) }, }, } @@ -914,7 +914,7 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // path in nested directory // check parameter - nestedDirPath := doc.Paths["/pets/{id}"] + nestedDirPath := doc.Paths.Value("/pets/{id}") require.Equal(t, "param", nestedDirPath.Patch.Parameters[0].Value.Name) require.Equal(t, "path", nestedDirPath.Patch.Parameters[0].Value.In) require.Equal(t, true, nestedDirPath.Patch.Parameters[0].Value.Required) @@ -934,7 +934,7 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // path in more nested directory // check parameter - moreNestedDirPath := doc.Paths["/pets/{id}/{city}"] + moreNestedDirPath := doc.Paths.Value("/pets/{id}/{city}") require.Equal(t, "param", moreNestedDirPath.Patch.Parameters[0].Value.Name) require.Equal(t, "path", moreNestedDirPath.Patch.Parameters[0].Value.In) require.Equal(t, true, moreNestedDirPath.Patch.Parameters[0].Value.Required) diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 3515586a0..d63f2ff6f 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -59,8 +59,8 @@ paths: require.NoError(t, err) require.Equal(t, "An API", doc.Info.Title) require.Equal(t, 2, len(doc.Components.Schemas)) - require.Equal(t, 1, len(doc.Paths)) - def := doc.Paths["/items"].Put.Responses.Default().Value + require.Equal(t, 1, doc.Paths.Len()) + def := doc.Paths.Value("/items").Put.Responses.Default().Value desc := "unexpected error" require.Equal(t, &desc, def.Description) err = doc.Validate(loader.Context) @@ -119,7 +119,7 @@ paths: err = doc.Validate(loader.Context) require.NoError(t, err) - example := doc.Paths["/"].Get.Responses.Get(200).Value.Content.Get("application/json").Examples["test"] + example := doc.Paths.Value("/").Get.Responses.Get(200).Value.Content.Get("application/json").Examples["test"] require.NotNil(t, example.Value) require.Equal(t, example.Value.Value.(map[string]interface{})["error"].(bool), false) } @@ -182,7 +182,7 @@ paths: doc, err := loader.LoadFromData(spec) require.NoError(t, err) - require.NotNil(t, doc.Paths["/"].Parameters[0].Value) + require.NotNil(t, doc.Paths.Value("/").Parameters[0].Value) } func TestLoadRequestExampleRef(t *testing.T) { @@ -214,7 +214,7 @@ paths: doc, err := loader.LoadFromData(spec) require.NoError(t, err) - require.NotNil(t, doc.Paths["/"].Post.RequestBody.Value.Content.Get("application/json").Examples["test"]) + require.NotNil(t, doc.Paths.Value("/").Post.RequestBody.Value.Content.Get("application/json").Examples["test"]) } func createTestServer(t *testing.T, handler http.Handler) *httptest.Server { @@ -251,7 +251,7 @@ func TestLoadWithReferenceInReference(t *testing.T) { require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "string", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) + require.Equal(t, "string", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) } func TestLoadWithRecursiveReferenceInLocalReferenceInParentSubdir(t *testing.T) { @@ -262,7 +262,7 @@ func TestLoadWithRecursiveReferenceInLocalReferenceInParentSubdir(t *testing.T) require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "object", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) + require.Equal(t, "object", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) } func TestLoadWithRecursiveReferenceInRefrerenceInLocalReference(t *testing.T) { @@ -273,8 +273,8 @@ func TestLoadWithRecursiveReferenceInRefrerenceInLocalReference(t *testing.T) { require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "integer", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Type) - require.Equal(t, "int64", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Format) + require.Equal(t, "integer", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Type) + require.Equal(t, "int64", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Format) } func TestLoadWithReferenceInReferenceInProperty(t *testing.T) { @@ -285,7 +285,7 @@ func TestLoadWithReferenceInReferenceInProperty(t *testing.T) { require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "Problem details", doc.Paths["/api/test/ref/in/ref/in/property"].Post.Responses["401"].Value.Content["application/json"].Schema.Value.Properties["error"].Value.Title) + require.Equal(t, "Problem details", doc.Paths.Value("/api/test/ref/in/ref/in/property").Post.Responses["401"].Value.Content["application/json"].Schema.Value.Properties["error"].Value.Title) } func TestLoadFileWithExternalSchemaRef(t *testing.T) { @@ -344,8 +344,8 @@ func TestLoadRequestResponseHeaderRef(t *testing.T) { doc, err := loader.LoadFromData(spec) require.NoError(t, err) - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) - require.Equal(t, "testheader", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + require.Equal(t, "testheader", doc.Paths.Value("/test").Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) } func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) { @@ -384,8 +384,8 @@ func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) { doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) - require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + require.Equal(t, "description", doc.Paths.Value("/test").Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) } func TestLoadYamlFile(t *testing.T) { @@ -412,8 +412,8 @@ func TestLoadYamlFileWithExternalPathRef(t *testing.T) { doc, err := loader.LoadFromFile("testdata/pathref.openapi.yml") require.NoError(t, err) - require.NotNil(t, doc.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) - require.Equal(t, "string", doc.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) + require.NotNil(t, doc.Paths.Value("/test").Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, "string", doc.Paths.Value("/test").Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) } func TestResolveResponseLinkRef(t *testing.T) { @@ -455,7 +455,7 @@ paths: err = doc.Validate(loader.Context) require.NoError(t, err) - response := doc.Paths[`/users/{id}`].Get.Responses.Get(200).Value + response := doc.Paths.Value(`/users/{id}`).Get.Responses.Get(200).Value link := response.Links[`father`].Value require.NotNil(t, link) require.Equal(t, "getUserById", link.OperationID) @@ -468,7 +468,7 @@ func TestLinksFromOAISpec(t *testing.T) { require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) - response := doc.Paths[`/2.0/repositories/{username}/{slug}`].Get.Responses.Get(200).Value + response := doc.Paths.Value(`/2.0/repositories/{username}/{slug}`).Get.Responses.Get(200).Value link := response.Links[`repositoryPullRequests`].Value require.Equal(t, map[string]interface{}{ "username": "$response.body#/owner/username", diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 8b8f71bb7..7e0ab5226 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -15,7 +15,7 @@ type T struct { OpenAPI string `json:"openapi" yaml:"openapi"` // Required Components *Components `json:"components,omitempty" yaml:"components,omitempty"` Info *Info `json:"info" yaml:"info"` // Required - Paths Paths `json:"paths" yaml:"paths"` // Required + Paths *Paths `json:"paths" yaml:"paths"` // Required Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -73,12 +73,12 @@ func (doc *T) UnmarshalJSON(data []byte) error { func (doc *T) AddOperation(path string, method string, operation *Operation) { if doc.Paths == nil { - doc.Paths = make(Paths) + doc.Paths = NewPaths() } - pathItem := doc.Paths[path] + pathItem := doc.Paths.Value(path) if pathItem == nil { pathItem = &PathItem{} - doc.Paths[path] = pathItem + doc.Paths.Set(path, pathItem) } pathItem.SetOperation(method, operation) } diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index e01af82ba..3016caf8a 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -271,8 +271,8 @@ func spec() *T { Title: "MyAPI", Version: "0.1", }, - Paths: Paths{ - "/hello": &PathItem{ + Paths: NewPaths( + WithPath("/hello", &PathItem{ Post: &Operation{ Parameters: Parameters{ { @@ -297,8 +297,8 @@ func spec() *T { Value: parameter, }, }, - }, - }, + }), + ), Components: &Components{ Parameters: ParametersMap{ "someParameter": { diff --git a/openapi3/operation.go b/openapi3/operation.go index 645c0805f..dd49d2d9a 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -81,8 +81,8 @@ func (operation Operation) MarshalJSON() ([]byte, error) { m["requestBody"] = x } m["responses"] = operation.Responses - if x := operation.Callbacks; len(x) != 0 { - m["callbacks"] = x + if x := operation.Callbacks; x.Len() != 0 { + m["callbacks"] = x.om } if x := operation.Deprecated; x { m["deprecated"] = x diff --git a/openapi3/paths.go b/openapi3/paths.go index 0986b0557..fcbb82e39 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -2,35 +2,104 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "sort" "strings" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) // Paths is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object -type Paths map[string]*PathItem +type Paths struct { + om *orderedmap.OrderedMap[string, *PathItem] +} + +// MarshalJSON returns the JSON encoding of Paths. +func (paths *Paths) MarshalJSON() ([]byte, error) { + if paths == nil || paths.om == nil { + return []byte("{}"), nil + } + return paths.om.MarshalJSON() +} + +// UnmarshalJSON sets Paths to a copy of data. +func (paths *Paths) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &paths.om) +} + +func (paths *Paths) Value(key string) *PathItem { + // if paths == nil || paths.om == nil { + // return nil + // } + return paths.om.Value(key) +} + +func (paths *Paths) Set(key string, value *PathItem) { + // if paths != nil || paths.om != nil { + _, _ = paths.om.Set(key, value) + // } +} + +func (paths *Paths) Len() int { + if paths == nil || paths.om == nil { + return 0 + } + return paths.om.Len() +} + +func (paths *Paths) Iter() *pathsKV { + if paths == nil || paths.om == nil { + return nil + } + return (*pathsKV)(paths.om.Oldest()) +} + +type pathsKV orderedmap.Pair[string, *PathItem] //FIXME: pub? + +func (pair *pathsKV) Next() *pathsKV { + ompair := (*orderedmap.Pair[string, *PathItem])(pair) + return (*pathsKV)(ompair.Next()) +} + +// NewPathsWithCapacity builds a paths object of the given capacity. +func NewPathsWithCapacity(cap int) *Paths { + return &Paths{om: orderedmap.New[string, *PathItem](cap)} +} + +// NewPaths builds a paths object with path items in insertion order. +func NewPaths(opts ...NewPathsOption) *Paths { + paths := NewPathsWithCapacity(len(opts)) + for _, opt := range opts { + opt(paths) + } + return paths +} + +// NewPathsOption describes options to NewPaths func +type NewPathsOption func(*Paths) + +// WithPath adds paths as an option to NewPaths +func WithPath(path string, pathItem *PathItem) NewPathsOption { + return func(paths *Paths) { paths.Set(path, pathItem) } +} // Validate returns an error if Paths does not comply with the OpenAPI spec. -func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error { +func (paths *Paths) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - normalizedPaths := make(map[string]string, len(paths)) + normalizedPaths := make(map[string]string, paths.Len()) - keys := make([]string, 0, len(paths)) - for key := range paths { - keys = append(keys, key) - } - sort.Strings(keys) - for _, path := range keys { - pathItem := paths[path] + for pair := paths.Iter(); pair != nil; pair = pair.Next() { + path, pathItem := pair.Key, pair.Value if path == "" || path[0] != '/' { return fmt.Errorf("path %q does not start with a forward slash (/)", path) } if pathItem == nil { pathItem = &PathItem{} - paths[path] = pathItem + paths.Set(path, pathItem) } normalizedPath, _, varsInPath := normalizeTemplatedPath(path) @@ -113,7 +182,7 @@ func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object // When matching URLs, concrete (non-templated) paths would be matched // before their templated counterparts. -func (paths Paths) InMatchingOrder() []string { +func (paths *Paths) InMatchingOrder() []string { // NOTE: sorting by number of variables ASC then by descending lexicographical // order seems to be a good heuristic. if paths == nil { @@ -122,7 +191,8 @@ func (paths Paths) InMatchingOrder() []string { vars := make(map[int][]string) max := 0 - for path := range paths { + for pair := paths.Iter(); pair != nil; pair = pair.Next() { + path := pair.Key count := strings.Count(path, "}") vars[count] = append(vars[count], path) if count > max { @@ -130,7 +200,7 @@ func (paths Paths) InMatchingOrder() []string { } } - ordered := make([]string, 0, len(paths)) + ordered := make([]string, 0, paths.Len()) for c := 0; c <= max; c++ { if ps, ok := vars[c]; ok { sort.Sort(sort.Reverse(sort.StringSlice(ps))) @@ -152,15 +222,15 @@ func (paths Paths) InMatchingOrder() []string { // pathItem := path.Find("/person/{name}") // // would return the correct path item. -func (paths Paths) Find(key string) *PathItem { - // Try directly access the map - pathItem := paths[key] +func (paths *Paths) Find(key string) *PathItem { + pathItem := paths.Value(key) if pathItem != nil { return pathItem } normalizedPath, expected, _ := normalizeTemplatedPath(key) - for path, pathItem := range paths { + for pair := paths.Iter(); pair != nil; pair = pair.Next() { + path, pathItem := pair.Key, pair.Value pathNormalized, got, _ := normalizeTemplatedPath(path) if got == expected && pathNormalized == normalizedPath { return pathItem @@ -169,9 +239,10 @@ func (paths Paths) Find(key string) *PathItem { return nil } -func (paths Paths) validateUniqueOperationIDs() error { +func (paths *Paths) validateUniqueOperationIDs() error { operationIDs := make(map[string]string) - for urlPath, pathItem := range paths { + for pair := paths.Iter(); pair != nil; pair = pair.Next() { + urlPath, pathItem := pair.Key, pair.Value if pathItem == nil { continue } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index f9a08385b..2d784abb0 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -8,6 +8,7 @@ import ( "net/url" "github.com/go-openapi/jsonpointer" + orderedmap "github.com/wk8/go-ordered-map/v2" ) type SecuritySchemes map[string]*SecuritySchemeRef @@ -312,10 +313,10 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) type OAuthFlow struct { Extensions map[string]interface{} `json:"-" yaml:"-"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes map[string]string `json:"scopes" yaml:"scopes"` // required + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes *orderedmap.OrderedMap[string, string] `json:"scopes" yaml:"scopes"` // required } // MarshalJSON returns the JSON encoding of OAuthFlow. @@ -363,7 +364,7 @@ func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) e } } - if v := flow.Scopes; len(v) == 0 { + if v := flow.Scopes; v == nil || v.Len() == 0 { return errors.New("field 'scopes' is empty or missing") } diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index d3a1b45bb..0bb71c335 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -57,8 +57,8 @@ func TestFilter(t *testing.T) { URL: "http://example.com/api/", }, }, - Paths: openapi3.Paths{ - "/prefix/{pathArg}/suffix": &openapi3.PathItem{ + Paths: openapi3.NewPaths( + openapi3.WithPath("/prefix/{pathArg}/suffix", &openapi3.PathItem{ Post: &openapi3.Operation{ Parameters: openapi3.Parameters{ { @@ -137,9 +137,9 @@ func TestFilter(t *testing.T) { }, Responses: openapi3.NewResponses(), }, - }, + }), - "/issue151": &openapi3.PathItem{ + openapi3.WithPath("/issue151", &openapi3.PathItem{ Get: &openapi3.Operation{ Responses: openapi3.NewResponses(), }, @@ -153,8 +153,8 @@ func TestFilter(t *testing.T) { }, }, }, - }, - }, + }), + ), } err := doc.Validate(context.Background()) @@ -535,7 +535,7 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test Title: "MyAPI", Version: "0.1", }, - Paths: map[string]*openapi3.PathItem{}, + Paths: openapi3.NewPaths(), Security: openapi3.SecurityRequirements{ { securitySchemes[1].Name: {}, @@ -563,12 +563,12 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test } securityRequirements = tempS } - doc.Paths[tc.name] = &openapi3.PathItem{ + doc.Paths.Set(tc.name, &openapi3.PathItem{ Get: &openapi3.Operation{ Security: securityRequirements, Responses: openapi3.NewResponses(), }, - } + }) } err := doc.Validate(context.Background()) @@ -669,7 +669,7 @@ func TestAnySecurityRequirementMet(t *testing.T) { Title: "MyAPI", Version: "0.1", }, - Paths: map[string]*openapi3.PathItem{}, + Paths: openapi3.NewPaths(), Components: &openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, @@ -694,12 +694,12 @@ func TestAnySecurityRequirementMet(t *testing.T) { } // Create the path with the security requirements - doc.Paths[tc.name] = &openapi3.PathItem{ + doc.Paths.Set(tc.name, &openapi3.PathItem{ Get: &openapi3.Operation{ Security: securityRequirements, Responses: openapi3.NewResponses(), }, - } + }) } err := doc.Validate(context.Background()) @@ -766,7 +766,7 @@ func TestAllSchemesMet(t *testing.T) { Title: "MyAPI", Version: "0.1", }, - Paths: map[string]*openapi3.PathItem{}, + Paths: openapi3.NewPaths(), Components: &openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, @@ -794,14 +794,14 @@ func TestAllSchemesMet(t *testing.T) { } } - doc.Paths[tc.name] = &openapi3.PathItem{ + doc.Paths.Set(tc.name, &openapi3.PathItem{ Get: &openapi3.Operation{ Security: &openapi3.SecurityRequirements{ securityRequirement, }, Responses: openapi3.NewResponses(), }, - } + }) } err := doc.Validate(context.Background()) diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index bbf81cea8..21f034e01 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -57,7 +57,8 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { muxRouter := mux.NewRouter().UseEncodedPath() r := &Router{} for _, path := range doc.Paths.InMatchingOrder() { - pathItem := doc.Paths[path] + pathItem := doc.Paths.Value(path) + if len(pathItem.Servers) > 0 { if servers, err = makeServers(pathItem.Servers); err != nil { return nil, err @@ -113,7 +114,7 @@ func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string } route := *r.routes[i] route.Method = req.Method - route.Operation = route.Spec.Paths[route.Path].GetOperation(route.Method) + route.Operation = route.Spec.Paths.Value(route.Path).GetOperation(route.Method) return &route, vars, nil } switch match.MatchErr { diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index 5fb9be2b0..fdfe87de1 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -32,8 +32,8 @@ func TestRouter(t *testing.T) { Title: "MyAPI", Version: "0.1", }, - Paths: openapi3.Paths{ - "/hello": &openapi3.PathItem{ + Paths: openapi3.NewPaths( + openapi3.WithPath("/hello", &openapi3.PathItem{ Connect: helloCONNECT, Delete: helloDELETE, Get: helloGET, @@ -43,34 +43,34 @@ func TestRouter(t *testing.T) { Post: helloPOST, Put: helloPUT, Trace: helloTRACE, - }, - "/onlyGET": &openapi3.PathItem{ + }), + openapi3.WithPath("/onlyGET", &openapi3.PathItem{ Get: helloGET, - }, - "/params/{x}/{y}/{z:.*}": &openapi3.PathItem{ + }), + openapi3.WithPath("/params/{x}/{y}/{z:.*}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")}, }, - }, - "/books/{bookid}": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, - }, - "/books/{bookid}.json": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}.json", &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, - }, - "/partial": &openapi3.PathItem{ + }), + openapi3.WithPath("/partial", &openapi3.PathItem{ Get: partialGET, - }, - }, + }), + ), } expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { @@ -80,7 +80,7 @@ func TestRouter(t *testing.T) { route, pathParams, err := r.FindRoute(req) if err != nil { if operation == nil { - pathItem := doc.Paths[uri] + pathItem := doc.Paths.Value(uri) if pathItem == nil { if err.Error() != routers.ErrPathNotFound.Error() { t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrPathNotFound, err) @@ -277,16 +277,16 @@ func TestServerOverrideAtPathLevel(t *testing.T) { URL: "https://example.com", }, }, - Paths: openapi3.Paths{ - "/hello": &openapi3.PathItem{ + Paths: openapi3.NewPaths( + openapi3.WithPath("/hello", &openapi3.PathItem{ Servers: openapi3.Servers{ &openapi3.Server{ URL: "https://another.com", }, }, Get: helloGET, - }, - }, + }), + ), } err := doc.Validate(context.Background()) require.NoError(t, err) @@ -318,11 +318,11 @@ func TestRelativeURL(t *testing.T) { URL: "/api/v1", }, }, - Paths: openapi3.Paths{ - "/hello": &openapi3.PathItem{ + Paths: openapi3.NewPaths( + openapi3.WithPath("/hello", &openapi3.PathItem{ Get: helloGET, - }, - }, + }), + ), } err := doc.Validate(context.Background()) require.NoError(t, err) diff --git a/routers/legacy/router.go b/routers/legacy/router.go index 911422b85..96b8af2f0 100644 --- a/routers/legacy/router.go +++ b/routers/legacy/router.go @@ -64,7 +64,8 @@ func NewRouter(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Rout } router := &Router{doc: doc} root := router.node() - for path, pathItem := range doc.Paths { + for pair := doc.Paths.Iter(); pair != nil; pair = pair.Next() { + path, pathItem := pair.Key, pair.Value for method, operation := range pathItem.Operations() { method = strings.ToUpper(method) if err := root.Add(method+" "+path, &routers.Route{ @@ -143,7 +144,7 @@ func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]s route, _ = node.Value.(*routers.Route) } if route == nil { - pathItem := doc.Paths[remainingPath] + pathItem := doc.Paths.Value(remainingPath) if pathItem == nil { return nil, nil, &routers.RouteError{Reason: routers.ErrPathNotFound.Error()} } diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go index e9b875986..0da746cd1 100644 --- a/routers/legacy/router_test.go +++ b/routers/legacy/router_test.go @@ -31,8 +31,8 @@ func TestRouter(t *testing.T) { Title: "MyAPI", Version: "0.1", }, - Paths: openapi3.Paths{ - "/hello": &openapi3.PathItem{ + Paths: openapi3.NewPaths( + openapi3.WithPath("/hello", &openapi3.PathItem{ Connect: helloCONNECT, Delete: helloDELETE, Get: helloGET, @@ -42,34 +42,34 @@ func TestRouter(t *testing.T) { Post: helloPOST, Put: helloPUT, Trace: helloTRACE, - }, - "/onlyGET": &openapi3.PathItem{ + }), + openapi3.WithPath("/onlyGET", &openapi3.PathItem{ Get: helloGET, - }, - "/params/{x}/{y}/{z.*}": &openapi3.PathItem{ + }), + openapi3.WithPath("/params/{x}/{y}/{z.*}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")}, }, - }, - "/books/{bookid}": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, - }, - "/books/{bookid2}.json": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid2}.json", &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")}, }, - }, - "/partial": &openapi3.PathItem{ + }), + openapi3.WithPath("/partial", &openapi3.PathItem{ Get: partialGET, - }, - }, + }), + ), } expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { @@ -78,7 +78,7 @@ func TestRouter(t *testing.T) { route, pathParams, err := r.FindRoute(req) if err != nil { if operation == nil { - pathItem := doc.Paths[uri] + pathItem := doc.Paths.Value(uri) if pathItem == nil { if err.Error() != routers.ErrPathNotFound.Error() { t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrPathNotFound, err) @@ -201,9 +201,9 @@ func TestRouter(t *testing.T) { content := openapi3.NewContentWithJSONSchema(schema) responses := openapi3.NewResponses() responses["default"].Value.Content = content - doc.Paths["/withExamples"] = &openapi3.PathItem{ + doc.Paths.Set("/withExamples", &openapi3.PathItem{ Get: &openapi3.Operation{Responses: responses}, - } + }) err = doc.Validate(context.Background()) require.Error(t, err) r, err = NewRouter(doc)