From 6740cd2b25853647fafc2e40c33d60f6f78b4fd2 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 28 Nov 2023 21:35:55 +0100 Subject: [PATCH] openapi3: add support for extensions on the few types left (#763) --- .github/docs/openapi3.txt | 151 ++++++++- .github/workflows/go.yml | 14 +- README.md | 12 +- maps.sh | 195 +++++++++++ openapi2conv/issue558_test.go | 2 +- openapi2conv/issue573_test.go | 4 +- openapi2conv/issue847_test.go | 6 +- openapi2conv/openapi2_conv.go | 20 +- openapi3/callback.go | 43 ++- openapi3/internalize_refs.go | 32 +- openapi3/issue301_test.go | 22 +- openapi3/issue341_test.go | 21 +- openapi3/issue376_test.go | 2 +- openapi3/issue513_test.go | 2 +- openapi3/issue594_test.go | 2 +- openapi3/issue753_test.go | 16 +- openapi3/issue819_test.go | 6 +- openapi3/load_with_go_embed_test.go | 11 +- openapi3/loader.go | 38 ++- .../loader_empty_response_description_test.go | 11 +- openapi3/loader_issue220_test.go | 7 +- openapi3/loader_outside_refs_test.go | 11 +- openapi3/loader_read_from_uri_func_test.go | 11 +- openapi3/loader_recursive_ref_test.go | 26 +- openapi3/loader_relative_refs_test.go | 78 ++--- openapi3/loader_test.go | 36 +- openapi3/maplike.go | 309 ++++++++++++++++++ openapi3/maplike_test.go | 74 +++++ openapi3/openapi3.go | 12 +- openapi3/openapi3_test.go | 16 +- openapi3/operation.go | 14 +- openapi3/operation_test.go | 19 +- openapi3/paths.go | 64 +++- openapi3/refs_test.go | 19 +- openapi3/response.go | 105 +++--- openapi3filter/req_resp_decoder_test.go | 2 +- openapi3filter/validate_response.go | 6 +- openapi3filter/validation_test.go | 30 +- routers/gorillamux/router.go | 4 +- routers/gorillamux/router_test.go | 50 +-- routers/legacy/router.go | 4 +- routers/legacy/router_test.go | 36 +- 42 files changed, 1213 insertions(+), 330 deletions(-) create mode 100755 maps.sh create mode 100644 openapi3/maplike.go create mode 100644 openapi3/maplike_test.go diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 02d22a102..3afa393a1 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -136,13 +136,46 @@ func (addProps AdditionalProperties) MarshalJSON() ([]byte, error) func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error UnmarshalJSON sets AdditionalProperties to a copy of data. -type Callback map[string]*PathItem +type Callback struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + // Has unexported fields. +} 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 -func (callback Callback) Validate(ctx context.Context, opts ...ValidationOption) error +func NewCallback(opts ...NewCallbackOption) *Callback + NewCallback builds a Callback object with path items in insertion order. + +func NewCallbackWithCapacity(cap int) *Callback + NewCallbackWithCapacity builds a Callback object of the given capacity. + +func (callback Callback) JSONLookup(token string) (interface{}, error) + JSONLookup implements + https://github.com/go-openapi/jsonpointer#JSONPointable + +func (callback *Callback) Len() int + Len returns the amount of keys in callback excluding callback.Extensions. + +func (callback *Callback) Map() map[string]*PathItem + Map returns callback as a 'map'. Note: iteration on Go maps is not ordered. + +func (callback Callback) MarshalJSON() ([]byte, error) + MarshalJSON returns the JSON encoding of Callback. + +func (callback *Callback) Set(key string, value *PathItem) + Set adds or replaces key 'key' of 'callback' with 'value'. Note: 'callback' + MUST be non-nil + +func (callback *Callback) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Callback to a copy of data. + +func (callback *Callback) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Callback does not comply with the OpenAPI spec. +func (callback *Callback) Value(key string) *PathItem + Value returns the callback for key or nil + type CallbackRef struct { Ref string Value *Callback @@ -609,6 +642,27 @@ func (me MultiError) Is(target error) bool `errors.Is()` It will also return true if any of the contained errors match target +type NewCallbackOption func(*Callback) + NewCallbackOption describes options to NewCallback func + +func WithCallback(cb string, pathItem *PathItem) NewCallbackOption + WithCallback adds Callback as an option to NewCallback + +type NewPathsOption func(*Paths) + NewPathsOption describes options to NewPaths func + +func WithPath(path string, pathItem *PathItem) NewPathsOption + WithPath adds a named path item + +type NewResponsesOption func(*Responses) + NewResponsesOption describes options to NewResponses func + +func WithName(name string, response *Response) NewResponsesOption + WithName adds a name-keyed Response + +func WithStatus(status int, responseRef *ResponseRef) NewResponsesOption + WithStatus adds a status code keyed ResponseRef + type OAuthFlow struct { Extensions map[string]interface{} `json:"-" yaml:"-"` @@ -673,7 +727,7 @@ type Operation struct { RequestBody *RequestBodyRef `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` // Responses. - Responses Responses `json:"responses" yaml:"responses"` // Required + Responses *Responses `json:"responses" yaml:"responses"` // Required // Optional callbacks Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` @@ -847,11 +901,21 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if PathItem does not comply with the OpenAPI spec. -type Paths map[string]*PathItem +type Paths struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + // Has unexported fields. +} 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 -func (paths Paths) Find(key string) *PathItem +func NewPaths(opts ...NewPathsOption) *Paths + NewPaths builds a paths object with path items in insertion order. + +func NewPathsWithCapacity(cap int) *Paths + NewPathsWithCapacity builds a paths object of the given capacity. + +func (paths *Paths) Find(key string) *PathItem Find returns a path that matches the key. The method ignores differences in template variable names (except possible @@ -866,16 +930,39 @@ func (paths Paths) Find(key string) *PathItem would return the correct path item. -func (paths Paths) InMatchingOrder() []string +func (paths *Paths) InMatchingOrder() []string InMatchingOrder returns paths in the order they are matched against URLs. 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) Validate(ctx context.Context, opts ...ValidationOption) error +func (paths Paths) JSONLookup(token string) (interface{}, error) + JSONLookup implements + https://github.com/go-openapi/jsonpointer#JSONPointable + +func (paths *Paths) Len() int + Len returns the amount of keys in paths excluding paths.Extensions. + +func (paths *Paths) Map() map[string]*PathItem + Map returns paths as a 'map'. Note: iteration on Go maps is not ordered. + +func (paths Paths) MarshalJSON() ([]byte, error) + MarshalJSON returns the JSON encoding of Paths. + +func (paths *Paths) Set(key string, value *PathItem) + Set adds or replaces key 'key' of 'paths' with 'value'. Note: 'paths' MUST + be non-nil + +func (paths *Paths) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Paths to a copy of data. + +func (paths *Paths) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Paths does not comply with the OpenAPI spec. +func (paths *Paths) Value(key string) *PathItem + Value returns the paths for key or nil + type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error) ReadFromURIFunc defines a function which reads the contents of a resource located at a URI. @@ -1039,28 +1126,58 @@ func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) er Validate returns an error if ResponseRef does not comply with the OpenAPI spec. -type Responses map[string]*ResponseRef +type Responses struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + // Has unexported fields. +} Responses is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responses-object -func NewResponses() Responses +func NewResponses(opts ...NewResponsesOption) *Responses + NewResponses builds a responses object with response objects in insertion + order. Given no arguments, NewResponses returns a valid responses object + containing a default match-all reponse. -func (responses Responses) Default() *ResponseRef +func NewResponsesWithCapacity(cap int) *Responses + NewResponsesWithCapacity builds a responses object of the given capacity. -func (responses Responses) Get(status int) *ResponseRef - Get returns a ResponseRef for the given status If an exact +func (responses *Responses) Default() *ResponseRef + Default returns the default response + +func (responses Responses) JSONLookup(token string) (interface{}, error) + JSONLookup implements + https://github.com/go-openapi/jsonpointer#JSONPointable + +func (responses *Responses) Len() int + Len returns the amount of keys in responses excluding responses.Extensions. + +func (responses *Responses) Map() map[string]*ResponseRef + Map returns responses as a 'map'. Note: iteration on Go maps is not ordered. + +func (responses Responses) MarshalJSON() ([]byte, error) + MarshalJSON returns the JSON encoding of Responses. + +func (responses *Responses) Set(key string, value *ResponseRef) + Set adds or replaces key 'key' of 'responses' with 'value'. Note: + 'responses' MUST be non-nil + +func (responses *Responses) Status(status int) *ResponseRef + Status returns a ResponseRef for the given status If an exact match isn't initially found a patterned field is checked using the first digit to determine the range (eg: 201 to 2XX) See https://spec.openapis.org/oas/v3.0.3#patterned-fields-0 -func (responses Responses) JSONLookup(token string) (interface{}, error) - JSONLookup implements - https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (responses *Responses) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Responses to a copy of data. -func (responses Responses) Validate(ctx context.Context, opts ...ValidationOption) error +func (responses *Responses) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Responses does not comply with the OpenAPI spec. +func (responses *Responses) Value(key string) *ResponseRef + Value returns the responses for key or nil + type Schema struct { Extensions map[string]interface{} `json:"-" yaml:"-"` @@ -1519,7 +1636,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"` diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2df5de06a..70272cd57 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -52,13 +52,13 @@ jobs: - uses: actions/checkout@v2 - - name: Check codegen - run: | - ./refs.sh | tee openapi3/refs.go - git --no-pager diff --exit-code + - run: ./refs.sh | tee openapi3/refs.go + - run: git --no-pager diff --exit-code + + - run: ./maps.sh + - run: git --no-pager diff --exit-code - - name: Check docsgen - run: ./docs.sh + - run: ./docs.sh - run: go mod download && go mod tidy && go mod verify - run: git --no-pager diff --exit-code @@ -119,7 +119,7 @@ jobs: - if: runner.os == 'Linux' name: Ensure non-pointer MarshalJSON run: | - ! git grep -InE 'func.+[*].+[)].MarshalJSON[(][)]' + ! git grep -InE 'func[^{}]+[*][^{}]+[)].MarshalJSON[(][)]' - if: runner.os == 'Linux' name: Use `loader := NewLoader(); loader.Load ...` diff --git a/README.md b/README.md index 1e4051462..95360bb47 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ go run github.com/getkin/kin-openapi/cmd/validate@latest [--circular] [--default Use `openapi3.Loader`, which resolves all references: ```go loader := openapi3.NewLoader() -doc, err := loader.LoadFromFile("swagger.json") +doc, err := loader.LoadFromFile("my-openapi-spec.json") ``` ## Getting OpenAPI operation that matches request @@ -275,7 +275,15 @@ func safeErrorMessage(err *openapi3.SchemaError) string { This will change the schema validation errors to return only the `Reason` field, which is guaranteed to not include the original value. -## CHANGELOG: Sub-v0 breaking API changes +## CHANGELOG: Sub-v1 breaking API changes + +### v0.122.0 +* `Paths` field of `openapi3.T` is now a pointer +* `Responses` field of `openapi3.Operation` is now a pointer +* `openapi3.Paths` went from `map[string]*PathItem` to a struct with an `Extensions` field and methods: `Set`, `Value`, `Len`, `Map`, and `New*`. +* `openapi3.Callback` went from `map[string]*PathItem` to a struct with an `Extensions` field and methods: `Set`, `Value`, `Len`, `Map`, and `New*`. +* `openapi3.Responses` went from `map[string]*ResponseRef` to a struct with an `Extensions` field and methods: `Set`, `Value`, `Len`, `Map`, and `New*`. +* `(openapi3.Responses).Get(int)` renamed to `(*openapi3.Responses).Status(int)` ### v0.121.0 * Introduce `openapi3.RequestBodies` (an alias on `map[string]*openapi3.ResponseRef`) and use it in place of `openapi3.Responses` for field `openapi3.Components.Responses`. diff --git a/maps.sh b/maps.sh new file mode 100755 index 000000000..4f2d92063 --- /dev/null +++ b/maps.sh @@ -0,0 +1,195 @@ +#!/bin/bash -eux +set -o pipefail + +maplike=./openapi3/maplike.go +maplike_test=./openapi3/maplike_test.go + +types=() +types+=('*Responses') +types+=('*Callback') +types+=('*Paths') + +value_types=() +value_types+=('*ResponseRef') +value_types+=('*PathItem') +value_types+=('*PathItem') + +deref_vs=() +deref_vs+=('*Response = v.Value') +deref_vs+=('*PathItem = v') +deref_vs+=('*PathItem = v') + +names=() +names+=('responses') +names+=('callback') +names+=('paths') + +[[ "${#types[@]}" = "${#value_types[@]}" ]] +[[ "${#types[@]}" = "${#deref_vs[@]}" ]] +[[ "${#types[@]}" = "${#names[@]}" ]] +[[ "${#types[@]}" = "$(git grep -InF ' m map[string]*' -- openapi3/loader.go | wc -l)" ]] + +cat <"$maplike" +package openapi3 + +import ( + "encoding/json" + "sort" + "strings" + + "github.com/go-openapi/jsonpointer" +) +EOF + + +cat <"$maplike_test" +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMaplikeMethods(t *testing.T) { + t.Parallel() +EOF + +for i in "${!types[@]}"; do + type=${types[$i]} + value_type=${value_types[$i]} + deref_v=${deref_vs[$i]} + name=${names[$i]} + + cat <>"$maplike" + +// Value returns the ${name} for key or nil +func (${name} ${type}) Value(key string) ${value_type} { + if ${name}.Len() == 0 { + return nil + } + return ${name}.m[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}) + } + ${name}.m[key] = value +} + +// Len returns the amount of keys in ${name} excluding ${name}.Extensions. +func (${name} ${type}) Len() int { + if ${name} == nil { + return 0 + } + return len(${name}.m) +} + +// Map returns ${name} as a 'map'. +// Note: iteration on Go maps is not ordered. +func (${name} ${type}) Map() map[string]${value_type} { + if ${name}.Len() == 0 { + return nil + } + return ${name}.m +} + +var _ jsonpointer.JSONPointable = (${type})(nil) + +// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable +func (${name} ${type#'*'}) JSONLookup(token string) (interface{}, error) { + if v := ${name}.Value(token); v == nil { + vv, _, err := jsonpointer.GetForToken(${name}.Extensions, token) + return vv, err + } else if ref := v.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } else { + var vv ${deref_v} + return vv, nil + } +} + +// 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 + } + for k, v := range ${name}.Map() { + m[k] = v + } + return json.Marshal(m) +} + +// 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 { + 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)), + } + + for _, k := range ks { + v := m[k] + if strings.HasPrefix(k, "x-") { + x.Extensions[k] = v + continue + } + + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + var vv ${value_type#'*'} + if err = vv.UnmarshalJSON(data); err != nil { + return + } + x.m[k] = &vv + } + *${name} = x + return +} +EOF + + cat <>"$maplike_test" + + t.Run("${type}", func(t *testing.T) { + t.Parallel() + t.Run("nil", func(t *testing.T) { + x := (${type})(nil) + require.Equal(t, 0, x.Len()) + require.Equal(t, (map[string]${value_type})(nil), x.Map()) + require.Equal(t, (${value_type})(nil), x.Value("key")) + require.Panics(t, func() { x.Set("key", &${value_type#'*'}{}) }) + }) + t.Run("nonnil", func(t *testing.T) { + x := &${type#'*'}{} + require.Equal(t, 0, x.Len()) + require.Equal(t, (map[string]${value_type})(nil), x.Map()) + 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")) + }) + }) +EOF + +done + + cat <>"$maplike_test" +} +EOF diff --git a/openapi2conv/issue558_test.go b/openapi2conv/issue558_test.go index 78661bf78..206730120 100644 --- a/openapi2conv/issue558_test.go +++ b/openapi2conv/issue558_test.go @@ -27,7 +27,7 @@ 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) diff --git a/openapi2conv/issue573_test.go b/openapi2conv/issue573_test.go index cefac409e..0f9a35fb6 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.Value("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.Value("200").Value.Content require.Len(t, pingPostContent, 1) require.Contains(t, pingPostContent, "application/json") } diff --git a/openapi2conv/issue847_test.go b/openapi2conv/issue847_test.go index b4ea35d7c..79dbb21ee 100644 --- a/openapi2conv/issue847_test.go +++ b/openapi2conv/issue847_test.go @@ -35,9 +35,7 @@ paths: err = v3.Validate(context.Background()) require.NoError(t, err) - schemaRequired := v3.Paths["/ping"].Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Required - require.Equal(t, schemaRequired, []string{"file"}) + require.Equal(t, []string{"file"}, v3.Paths.Value("/ping").Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Required) - fieldRequired := v3.Paths["/ping"].Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Properties["file"].Value.Required - require.Nil(t, fieldRequired) + require.Nil(t, v3.Paths.Value("/ping").Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Properties["file"].Value.Required) } diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index ebd272ae1..2404e9027 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -71,19 +71,18 @@ func ToV3WithLoader(doc2 *openapi2.T, loader *openapi3.Loader, location *url.URL } if paths := doc2.Paths; len(paths) != 0 { - doc3Paths := make(map[string]*openapi3.PathItem, len(paths)) + doc3.Paths = openapi3.NewPathsWithCapacity(len(paths)) for path, pathItem := range paths { 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 { - doc3.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses)) + doc3.Components.Responses = make(openapi3.ResponseBodies, len(responses)) for k, response := range responses { r, err := ToV3Response(response, doc2.Produces) if err != nil { @@ -189,15 +188,14 @@ func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem * } if responses := operation.Responses; responses != nil { - doc3Responses := make(openapi3.Responses, len(responses)) + doc3.Responses = openapi3.NewResponsesWithCapacity(len(responses)) for k, response := range responses { - doc3, err := ToV3Response(response, operation.Produces) + responseRef3, err := ToV3Response(response, operation.Produces) if err != nil { return nil, err } - doc3Responses[k] = doc3 + doc3.Responses.Set(k, responseRef3) } - doc3.Responses = doc3Responses } return doc3, nil } @@ -614,13 +612,15 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { } } } + if isHTTPS { doc2.Schemes = append(doc2.Schemes, "https") } if isHTTP { doc2.Schemes = append(doc2.Schemes, "http") } - for path, pathItem := range doc3.Paths { + + for path, pathItem := range doc3.Paths.Map() { if pathItem == nil { continue } @@ -983,7 +983,7 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 sort.Sort(result.Parameters) if responses := operation.Responses; responses != nil { - resultResponses, err := FromV3Responses(responses, doc3.Components) + resultResponses, err := FromV3Responses(responses.Map(), doc3.Components) if err != nil { return nil, err } diff --git a/openapi3/callback.go b/openapi3/callback.go index bf5dc83dc..10e084ef0 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -7,22 +7,53 @@ import ( // 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 { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + m map[string]*PathItem +} + +// NewCallbackWithCapacity builds a Callback object of the given capacity. +func NewCallbackWithCapacity(cap int) *Callback { + return &Callback{m: make(map[string]*PathItem, cap)} +} + +// NewCallback builds a Callback object with path items in insertion order. +func NewCallback(opts ...NewCallbackOption) *Callback { + Callback := NewCallbackWithCapacity(len(opts)) + for _, opt := range opts { + opt(Callback) + } + return Callback +} + +// NewCallbackOption describes options to NewCallback func +type NewCallbackOption func(*Callback) + +// WithCallback adds Callback as an option to NewCallback +func WithCallback(cb string, pathItem *PathItem) NewCallbackOption { + return func(callback *Callback) { + if p := pathItem; p != nil && cb != "" { + callback.Set(cb, p) + } + } +} // 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 := make([]string, 0, callback.Len()) + for key := range callback.Map() { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { - v := callback[key] + v := callback.Value(key) if err := v.Validate(ctx); err != nil { return err } } - return nil + + return validateExtensions(ctx, callback.Extensions) } diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index 98c0e7ecf..b54a40687 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -313,14 +313,22 @@ func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver, parentIsExte } } -func (doc *T) derefResponses(es map[string]*ResponseRef, refNameResolver RefNameResolver, parentIsExternal bool) { +func (doc *T) derefResponse(r *ResponseRef, refNameResolver RefNameResolver, parentIsExternal bool) { + isExternal := doc.addResponseToSpec(r, refNameResolver, parentIsExternal) + if v := r.Value; v != nil { + doc.derefHeaders(v.Headers, refNameResolver, isExternal || parentIsExternal) + doc.derefContent(v.Content, refNameResolver, isExternal || parentIsExternal) + doc.derefLinks(v.Links, refNameResolver, isExternal || parentIsExternal) + } +} + +func (doc *T) derefResponses(rs *Responses, refNameResolver RefNameResolver, parentIsExternal bool) { + doc.derefResponseBodies(rs.Map(), refNameResolver, parentIsExternal) +} + +func (doc *T) derefResponseBodies(es ResponseBodies, refNameResolver RefNameResolver, parentIsExternal bool) { for _, e := range es { - isExternal := doc.addResponseToSpec(e, refNameResolver, parentIsExternal) - if e.Value != nil { - doc.derefHeaders(e.Value.Headers, refNameResolver, isExternal || parentIsExternal) - doc.derefContent(e.Value.Content, refNameResolver, isExternal || parentIsExternal) - doc.derefLinks(e.Value.Links, refNameResolver, isExternal || parentIsExternal) - } + doc.derefResponse(e, refNameResolver, parentIsExternal) } } @@ -354,7 +362,8 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso for _, cb := range op.Callbacks { isExternal := doc.addCallbackToSpec(cb, refNameResolver, pathIsExternal) if cb.Value != nil { - doc.derefPaths(*cb.Value, refNameResolver, pathIsExternal || isExternal) + cbValue := (*cb.Value).Map() + doc.derefPaths(cbValue, refNameResolver, pathIsExternal || isExternal) } } doc.derefResponses(op.Responses, refNameResolver, pathIsExternal) @@ -413,7 +422,7 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri doc.derefRequestBody(*req.Value, refNameResolver, isExternal) } } - doc.derefResponses(components.Responses, refNameResolver, false) + doc.derefResponseBodies(components.Responses, refNameResolver, false) for _, ss := range components.SecuritySchemes { doc.addSecuritySchemeToSpec(ss, refNameResolver, false) } @@ -423,10 +432,11 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri 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 := (*cb.Value).Map() + doc.derefPaths(cbValue, refNameResolver, isExternal) } } } - doc.derefPaths(doc.Paths, refNameResolver, false) + doc.derefPaths(doc.Paths.Map(), refNameResolver, false) } diff --git a/openapi3/issue301_test.go b/openapi3/issue301_test.go index a0225fdb8..ea14c3a76 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["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["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 ba9bed76b..2ceb45964 100644 --- a/openapi3/issue341_test.go +++ b/openapi3/issue341_test.go @@ -21,9 +21,26 @@ func TestIssue341(t *testing.T) { bs, err := doc.MarshalJSON() require.NoError(t, err) - require.JSONEq(t, `{"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"$ref":"testpath.yaml#/paths/~1testpath"}}}`, string(bs)) + require.JSONEq(t, `{ + "info": { + "title": "test file", + "version": "n/a" + }, + "openapi": "3.0.0", + "paths": { + "/testpath": { + "$ref": "testpath.yaml#/paths/~1testpath" + } + } +}`, 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.Value("200").Value. + Content["application/json"]. + Schema.Value. + Type) doc.InternalizeRefs(context.Background(), nil) bs, err = doc.MarshalJSON() 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 442eac848..38454f672 100644 --- a/openapi3/issue513_test.go +++ b/openapi3/issue513_test.go @@ -152,7 +152,7 @@ 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]`) } diff --git a/openapi3/issue594_test.go b/openapi3/issue594_test.go index b36b73bbc..42bc7e797 100644 --- a/openapi3/issue594_test.go +++ b/openapi3/issue594_test.go @@ -23,7 +23,7 @@ func TestIssue594(t *testing.T) { require.NoError(t, err) doc.Info.Version = "1.2.3" - doc.Paths["/marketing/contacts/search/emails"].Post = nil + doc.Paths.Value("/marketing/contacts/search/emails").Post = nil doc.Components.Schemas["full-segment"].Value.Example = nil err = doc.Validate(sl.Context) diff --git a/openapi3/issue753_test.go b/openapi3/issue753_test.go index 4390641a4..46c18f7aa 100644 --- a/openapi3/issue753_test.go +++ b/openapi3/issue753_test.go @@ -15,6 +15,18 @@ func TestIssue753(t *testing.T) { err = doc.Validate(loader.Context) require.NoError(t, err) - require.NotNil(t, (*doc.Paths["/test1"].Post.Callbacks["callback1"].Value)["{$request.body#/callback}"].Post.RequestBody.Value.Content["application/json"].Schema.Value) - require.NotNil(t, (*doc.Paths["/test2"].Post.Callbacks["callback2"].Value)["{$request.body#/callback}"].Post.RequestBody.Value.Content["application/json"].Schema.Value) + require.NotNil(t, doc. + Paths.Value("/test1"). + Post.Callbacks["callback1"].Value. + Value("{$request.body#/callback}"). + Post.RequestBody.Value. + Content["application/json"]. + Schema.Value) + require.NotNil(t, doc. + Paths.Value("/test2"). + Post.Callbacks["callback2"].Value. + Value("{$request.body#/callback}"). + Post.RequestBody.Value. + Content["application/json"]. + Schema.Value) } diff --git a/openapi3/issue819_test.go b/openapi3/issue819_test.go index 80f213687..121b50d98 100644 --- a/openapi3/issue819_test.go +++ b/openapi3/issue819_test.go @@ -33,7 +33,7 @@ paths: err = doc.Validate(sl.Context) require.NoError(t, err) - require.NotNil(t, doc.Paths["/v1/operation"].Get.Responses.Get(201)) - require.Nil(t, doc.Paths["/v1/operation"].Get.Responses.Get(404)) - require.Nil(t, doc.Paths["/v1/operation"].Get.Responses.Get(999)) + require.NotNil(t, doc.Paths.Value("/v1/operation").Get.Responses.Status(201)) + require.Nil(t, doc.Paths.Value("/v1/operation").Get.Responses.Status(404)) + require.Nil(t, doc.Paths.Value("/v1/operation").Get.Responses.Status(999)) } diff --git a/openapi3/load_with_go_embed_test.go b/openapi3/load_with_go_embed_test.go index e0fb915ba..3b77e5fe4 100644 --- a/openapi3/load_with_go_embed_test.go +++ b/openapi3/load_with_go_embed_test.go @@ -30,6 +30,15 @@ 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.Value("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 dd5a105a7..1128aaa78 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -238,7 +238,7 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { } // Visit all operations - for _, pathItem := range doc.Paths { + for _, pathItem := range doc.Paths.Map() { if pathItem == nil { continue } @@ -311,21 +311,32 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv pathPart = unescapeRefString(pathPart) attempted := false + switch c := cursor.(type) { // Special case of T // See issue856: a ref to doc => we assume that doc is a T => things live in T.Extensions - if t, ok := cursor.(*T); ok && pathPart == "" { - cursor = t.Extensions - attempted = true - } + case *T: + if pathPart == "" { + cursor = c.Extensions + attempted = true + } // Special case due to multijson - if s, ok := cursor.(*SchemaRef); ok && pathPart == "additionalProperties" { - if ap := s.Value.AdditionalProperties.Has; ap != nil { - cursor = *ap - } else { - cursor = s.Value.AdditionalProperties.Schema + case *SchemaRef: + if pathPart == "additionalProperties" { + if ap := c.Value.AdditionalProperties.Has; ap != nil { + cursor = *ap + } else { + cursor = c.Value.AdditionalProperties.Schema + } + attempted = true } - attempted = true + + case *Responses: + cursor = c.m // m map[string]*ResponseRef + case *Callback: + cursor = c.m // m map[string]*PathItem + case *Paths: + cursor = c.m // m map[string]*PathItem } if !attempted { @@ -417,6 +428,7 @@ func readableType(x interface{}) string { func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { + case reflect.Map: elementValue := val.MapIndex(reflect.ValueOf(fieldName)) if !elementValue.IsValid() { @@ -972,7 +984,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen return nil } - for _, pathItem := range *value { + for _, pathItem := range value.Map() { if err = loader.resolvePathItemRef(doc, pathItem, documentPath); err != nil { return err } @@ -1065,7 +1077,7 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat return } } - for _, response := range operation.Responses { + for _, response := range operation.Responses.Map() { if err = loader.resolveResponseRef(doc, response, documentPath); err != nil { return } diff --git a/openapi3/loader_empty_response_description_test.go b/openapi3/loader_empty_response_description_test.go index 3c4b6bffd..9c2225e5f 100644 --- a/openapi3/loader_empty_response_description_test.go +++ b/openapi3/loader_empty_response_description_test.go @@ -36,9 +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 - expected := "" - require.Equal(t, &expected, got) + require.Equal(t, "", *doc.Paths.Value("/path1").Get.Responses.Value("200").Value.Description) t.Log("Empty description provided: valid spec") err = doc.Validate(loader.Context) require.NoError(t, err) @@ -49,9 +47,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 - expected := "My response" - require.Equal(t, &expected, got) + require.Equal(t, "My response", *doc.Paths.Value("/path1").Get.Responses.Value("200").Value.Description) t.Log("Non-empty description provided: valid spec") err = doc.Validate(loader.Context) require.NoError(t, err) @@ -61,8 +57,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 - require.Nil(t, got) + require.Nil(t, doc.Paths.Value("/path1").Get.Responses.Value("200").Value.Description) t.Log("No description provided: invalid spec") err = doc.Validate(loader.Context) require.Error(t, err) diff --git a/openapi3/loader_issue220_test.go b/openapi3/loader_issue220_test.go index 57a44d5d0..0b2569783 100644 --- a/openapi3/loader_issue220_test.go +++ b/openapi3/loader_issue220_test.go @@ -22,6 +22,11 @@ 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.Value("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 fc6a2a135..3f2cf7cd7 100644 --- a/openapi3/loader_outside_refs_test.go +++ b/openapi3/loader_outside_refs_test.go @@ -16,7 +16,16 @@ 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.Value("200").Value. + Content["application/json"]. + Schema.Value. + Items.Value. + AllOf[0].Value. + Properties["created_at"].Value. + Type) } func TestIssue423(t *testing.T) { diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go index 50c060b5f..8269c8ee6 100644 --- a/openapi3/loader_read_from_uri_func_test.go +++ b/openapi3/loader_read_from_uri_func_test.go @@ -20,7 +20,16 @@ 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.Status(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..85655ef3e 100644 --- a/openapi3/loader_recursive_ref_test.go +++ b/openapi3/loader_recursive_ref_test.go @@ -13,9 +13,24 @@ 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.Status(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.Status(400).Value. + Content.Get("application/json"). + Schema.Value.Title) + + require.Equal(t, "ErrorDetails", doc. + Paths.Value("/double-ref-foo"). + Get.Responses.Status(400).Value. + Content.Get("application/json"). + Schema.Value.Title) } func TestIssue447(t *testing.T) { @@ -38,14 +53,9 @@ components: err = doc.Validate(loader.Context) require.NoError(t, err) require.Equal(t, "object", doc.Components. - // Complex Schemas["Complex"]. - // parent Value.Properties["parent"]. - // parent Value.Properties["parent"]. - // parent Value.Properties["parent"]. - // type Value.Type) } diff --git a/openapi3/loader_relative_refs_test.go b/openapi3/loader_relative_refs_test.go index 2efb0c7e8..2ebd91b6d 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: "PathOperationRequestBodyContentSchemaRef", contentTemplate: externalPathOperationRequestBodyContentSchemaRefTemplate, 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.Value("200")) + require.NotNil(t, doc.Paths.Value("/pets").Get.Responses.Value("200").Value.Content["application/json"]) }, }, } @@ -913,40 +913,40 @@ 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) // check header - require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description) - require.Equal(t, "header1", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Another"].Value.Description) - require.Equal(t, "header2", nestedDirPath.Patch.Responses["200"].Value.Headers["X-And-Another"].Value.Description) + require.Equal(t, "header", nestedDirPath.Patch.Responses.Value("200").Value.Headers["X-Rate-Limit-Reset"].Value.Description) + require.Equal(t, "header1", nestedDirPath.Patch.Responses.Value("200").Value.Headers["X-Another"].Value.Description) + require.Equal(t, "header2", nestedDirPath.Patch.Responses.Value("200").Value.Headers["X-And-Another"].Value.Description) // check request body require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description) // check response schema and example - require.Equal(t, nestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Schema.Value.Type, "string") + require.Equal(t, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type, "string") expectedExample := "hello" - require.Equal(t, expectedExample, nestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Examples["CustomTestExample"].Value.Value) + require.Equal(t, expectedExample, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value) // 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) // check header - require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description) - require.Equal(t, "header1", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Another"].Value.Description) - require.Equal(t, "header2", nestedDirPath.Patch.Responses["200"].Value.Headers["X-And-Another"].Value.Description) + require.Equal(t, "header", nestedDirPath.Patch.Responses.Value("200").Value.Headers["X-Rate-Limit-Reset"].Value.Description) + require.Equal(t, "header1", nestedDirPath.Patch.Responses.Value("200").Value.Headers["X-Another"].Value.Description) + require.Equal(t, "header2", nestedDirPath.Patch.Responses.Value("200").Value.Headers["X-And-Another"].Value.Description) // check request body require.Equal(t, "example request", moreNestedDirPath.Patch.RequestBody.Value.Description) // check response schema and example - require.Equal(t, "string", moreNestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Schema.Value.Type) - require.Equal(t, moreNestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Examples["CustomTestExample"].Value.Value, expectedExample) + require.Equal(t, "string", moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value, expectedExample) } diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 2c1d4198f..32cb52432 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -60,8 +60,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)) - require.Equal(t, "unexpected error", *doc.Paths["/items"].Put.Responses.Default().Value.Description) + require.Equal(t, 1, doc.Paths.Len()) + require.Equal(t, "unexpected error", *doc.Paths.Value("/items").Put.Responses.Default().Value.Description) err = doc.Validate(loader.Context) require.NoError(t, err) @@ -168,7 +168,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.Status(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) } @@ -231,7 +231,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) { @@ -263,7 +263,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 { @@ -300,7 +300,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) { @@ -311,7 +311,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 TestLoadWithRecursiveReferenceInReferenceInLocalReference(t *testing.T) { @@ -322,8 +322,8 @@ func TestLoadWithRecursiveReferenceInReferenceInLocalReference(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) { @@ -334,7 +334,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.Value("401").Value.Content["application/json"].Schema.Value.Properties["error"].Value.Title) } func TestLoadFileWithExternalSchemaRef(t *testing.T) { @@ -393,8 +393,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) { @@ -433,8 +433,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) { @@ -461,8 +461,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.Value("200").Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, "string", doc.Paths.Value("/test").Get.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) } func TestResolveResponseLinkRef(t *testing.T) { @@ -504,7 +504,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.Status(200).Value link := response.Links[`father`].Value require.NotNil(t, link) require.Equal(t, "getUserById", link.OperationID) @@ -517,7 +517,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.Status(200).Value link := response.Links[`repositoryPullRequests`].Value require.Equal(t, map[string]interface{}{ "username": "$response.body#/owner/username", diff --git a/openapi3/maplike.go b/openapi3/maplike.go new file mode 100644 index 000000000..a35272d05 --- /dev/null +++ b/openapi3/maplike.go @@ -0,0 +1,309 @@ +package openapi3 + +import ( + "encoding/json" + "sort" + "strings" + + "github.com/go-openapi/jsonpointer" +) + +// Value returns the responses for key or nil +func (responses *Responses) Value(key string) *ResponseRef { + if responses.Len() == 0 { + return nil + } + return responses.m[key] +} + +// Set adds or replaces key 'key' of 'responses' with 'value'. +// Note: 'responses' MUST be non-nil +func (responses *Responses) Set(key string, value *ResponseRef) { + if responses.m == nil { + responses.m = make(map[string]*ResponseRef) + } + responses.m[key] = value +} + +// Len returns the amount of keys in responses excluding responses.Extensions. +func (responses *Responses) Len() int { + if responses == nil { + return 0 + } + return len(responses.m) +} + +// Map returns responses as a 'map'. +// Note: iteration on Go maps is not ordered. +func (responses *Responses) Map() map[string]*ResponseRef { + if responses.Len() == 0 { + return nil + } + return responses.m +} + +var _ jsonpointer.JSONPointable = (*Responses)(nil) + +// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable +func (responses Responses) JSONLookup(token string) (interface{}, error) { + if v := responses.Value(token); v == nil { + vv, _, err := jsonpointer.GetForToken(responses.Extensions, token) + return vv, err + } else if ref := v.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } else { + var vv *Response = v.Value + return vv, nil + } +} + +// MarshalJSON returns the JSON encoding of Responses. +func (responses Responses) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, responses.Len()+len(responses.Extensions)) + for k, v := range responses.Extensions { + m[k] = v + } + for k, v := range responses.Map() { + m[k] = v + } + return json.Marshal(m) +} + +// UnmarshalJSON sets Responses to a copy of data. +func (responses *Responses) UnmarshalJSON(data []byte) (err error) { + var m map[string]interface{} + if err = json.Unmarshal(data, &m); err != nil { + return + } + + ks := make([]string, 0, len(m)) + for k := range m { + ks = append(ks, k) + } + sort.Strings(ks) + + x := Responses{ + Extensions: make(map[string]interface{}), + m: make(map[string]*ResponseRef, len(m)), + } + + for _, k := range ks { + v := m[k] + if strings.HasPrefix(k, "x-") { + x.Extensions[k] = v + continue + } + + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + var vv ResponseRef + if err = vv.UnmarshalJSON(data); err != nil { + return + } + x.m[k] = &vv + } + *responses = x + return +} + +// Value returns the callback for key or nil +func (callback *Callback) Value(key string) *PathItem { + if callback.Len() == 0 { + return nil + } + return callback.m[key] +} + +// Set adds or replaces key 'key' of 'callback' with 'value'. +// Note: 'callback' MUST be non-nil +func (callback *Callback) Set(key string, value *PathItem) { + if callback.m == nil { + callback.m = make(map[string]*PathItem) + } + callback.m[key] = value +} + +// Len returns the amount of keys in callback excluding callback.Extensions. +func (callback *Callback) Len() int { + if callback == nil { + return 0 + } + return len(callback.m) +} + +// Map returns callback as a 'map'. +// Note: iteration on Go maps is not ordered. +func (callback *Callback) Map() map[string]*PathItem { + if callback.Len() == 0 { + return nil + } + return callback.m +} + +var _ jsonpointer.JSONPointable = (*Callback)(nil) + +// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable +func (callback Callback) JSONLookup(token string) (interface{}, error) { + if v := callback.Value(token); v == nil { + vv, _, err := jsonpointer.GetForToken(callback.Extensions, token) + return vv, err + } else if ref := v.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } else { + var vv *PathItem = v + return vv, nil + } +} + +// MarshalJSON returns the JSON encoding of Callback. +func (callback Callback) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, callback.Len()+len(callback.Extensions)) + for k, v := range callback.Extensions { + m[k] = v + } + for k, v := range callback.Map() { + m[k] = v + } + return json.Marshal(m) +} + +// UnmarshalJSON sets Callback to a copy of data. +func (callback *Callback) UnmarshalJSON(data []byte) (err error) { + var m map[string]interface{} + if err = json.Unmarshal(data, &m); err != nil { + return + } + + ks := make([]string, 0, len(m)) + for k := range m { + ks = append(ks, k) + } + sort.Strings(ks) + + x := Callback{ + Extensions: make(map[string]interface{}), + m: make(map[string]*PathItem, len(m)), + } + + for _, k := range ks { + v := m[k] + if strings.HasPrefix(k, "x-") { + x.Extensions[k] = v + continue + } + + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + var vv PathItem + if err = vv.UnmarshalJSON(data); err != nil { + return + } + x.m[k] = &vv + } + *callback = x + return +} + +// Value returns the paths for key or nil +func (paths *Paths) Value(key string) *PathItem { + if paths.Len() == 0 { + return nil + } + return paths.m[key] +} + +// Set adds or replaces key 'key' of 'paths' with 'value'. +// Note: 'paths' MUST be non-nil +func (paths *Paths) Set(key string, value *PathItem) { + if paths.m == nil { + paths.m = make(map[string]*PathItem) + } + paths.m[key] = value +} + +// Len returns the amount of keys in paths excluding paths.Extensions. +func (paths *Paths) Len() int { + if paths == nil { + return 0 + } + return len(paths.m) +} + +// Map returns paths as a 'map'. +// Note: iteration on Go maps is not ordered. +func (paths *Paths) Map() map[string]*PathItem { + if paths.Len() == 0 { + return nil + } + return paths.m +} + +var _ jsonpointer.JSONPointable = (*Paths)(nil) + +// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable +func (paths Paths) JSONLookup(token string) (interface{}, error) { + if v := paths.Value(token); v == nil { + vv, _, err := jsonpointer.GetForToken(paths.Extensions, token) + return vv, err + } else if ref := v.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } else { + var vv *PathItem = v + return vv, nil + } +} + +// MarshalJSON returns the JSON encoding of Paths. +func (paths Paths) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, paths.Len()+len(paths.Extensions)) + for k, v := range paths.Extensions { + m[k] = v + } + for k, v := range paths.Map() { + m[k] = v + } + return json.Marshal(m) +} + +// UnmarshalJSON sets Paths to a copy of data. +func (paths *Paths) UnmarshalJSON(data []byte) (err error) { + var m map[string]interface{} + if err = json.Unmarshal(data, &m); err != nil { + return + } + + ks := make([]string, 0, len(m)) + for k := range m { + ks = append(ks, k) + } + sort.Strings(ks) + + x := Paths{ + Extensions: make(map[string]interface{}), + m: make(map[string]*PathItem, len(m)), + } + + for _, k := range ks { + v := m[k] + if strings.HasPrefix(k, "x-") { + x.Extensions[k] = v + continue + } + + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + var vv PathItem + if err = vv.UnmarshalJSON(data); err != nil { + return + } + x.m[k] = &vv + } + *paths = x + return +} diff --git a/openapi3/maplike_test.go b/openapi3/maplike_test.go new file mode 100644 index 000000000..7c502c461 --- /dev/null +++ b/openapi3/maplike_test.go @@ -0,0 +1,74 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMaplikeMethods(t *testing.T) { + t.Parallel() + + t.Run("*Responses", func(t *testing.T) { + t.Parallel() + t.Run("nil", func(t *testing.T) { + x := (*Responses)(nil) + require.Equal(t, 0, x.Len()) + require.Equal(t, (map[string]*ResponseRef)(nil), x.Map()) + require.Equal(t, (*ResponseRef)(nil), x.Value("key")) + require.Panics(t, func() { x.Set("key", &ResponseRef{}) }) + }) + t.Run("nonnil", func(t *testing.T) { + x := &Responses{} + require.Equal(t, 0, x.Len()) + require.Equal(t, (map[string]*ResponseRef)(nil), x.Map()) + require.Equal(t, (*ResponseRef)(nil), x.Value("key")) + x.Set("key", &ResponseRef{}) + require.Equal(t, 1, x.Len()) + require.Equal(t, map[string]*ResponseRef{"key": {}}, x.Map()) + require.Equal(t, &ResponseRef{}, x.Value("key")) + }) + }) + + t.Run("*Callback", func(t *testing.T) { + t.Parallel() + t.Run("nil", func(t *testing.T) { + x := (*Callback)(nil) + require.Equal(t, 0, x.Len()) + require.Equal(t, (map[string]*PathItem)(nil), x.Map()) + require.Equal(t, (*PathItem)(nil), x.Value("key")) + require.Panics(t, func() { x.Set("key", &PathItem{}) }) + }) + t.Run("nonnil", func(t *testing.T) { + x := &Callback{} + require.Equal(t, 0, x.Len()) + require.Equal(t, (map[string]*PathItem)(nil), x.Map()) + require.Equal(t, (*PathItem)(nil), x.Value("key")) + x.Set("key", &PathItem{}) + require.Equal(t, 1, x.Len()) + require.Equal(t, map[string]*PathItem{"key": {}}, x.Map()) + require.Equal(t, &PathItem{}, x.Value("key")) + }) + }) + + t.Run("*Paths", func(t *testing.T) { + t.Parallel() + t.Run("nil", func(t *testing.T) { + x := (*Paths)(nil) + require.Equal(t, 0, x.Len()) + require.Equal(t, (map[string]*PathItem)(nil), x.Map()) + require.Equal(t, (*PathItem)(nil), x.Value("key")) + require.Panics(t, func() { x.Set("key", &PathItem{}) }) + }) + t.Run("nonnil", func(t *testing.T) { + x := &Paths{} + require.Equal(t, 0, x.Len()) + require.Equal(t, (map[string]*PathItem)(nil), x.Map()) + require.Equal(t, (*PathItem)(nil), x.Value("key")) + x.Set("key", &PathItem{}) + require.Equal(t, 1, x.Len()) + require.Equal(t, map[string]*PathItem{"key": {}}, x.Map()) + require.Equal(t, &PathItem{}, x.Value("key")) + }) + }) +} diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 865837e0e..88d66d44b 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -17,7 +17,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"` @@ -104,13 +104,13 @@ 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) - } - pathItem := doc.Paths[path] + pathItem := doc.Paths.Value(path) if pathItem == nil { pathItem = &PathItem{} - doc.Paths[path] = pathItem + if doc.Paths == nil { + doc.Paths = NewPaths() + } + doc.Paths.Set(path, pathItem) } pathItem.SetOperation(method, operation) } diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 9cfd277bc..989294823 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -272,8 +272,8 @@ func spec() *T { Title: "MyAPI", Version: "0.1", }, - Paths: Paths{ - "/hello": &PathItem{ + Paths: NewPaths( + WithPath("/hello", &PathItem{ Post: &Operation{ Parameters: Parameters{ { @@ -285,12 +285,12 @@ func spec() *T { Ref: "#/components/requestBodies/someRequestBody", Value: requestBody, }, - Responses: Responses{ - "200": &ResponseRef{ + Responses: NewResponses( + WithStatus(200, &ResponseRef{ Ref: "#/components/responses/someResponse", Value: response, - }, - }, + }), + ), }, Parameters: Parameters{ { @@ -298,8 +298,8 @@ func spec() *T { Value: parameter, }, }, - }, - }, + }), + ), Components: &Components{ Parameters: ParametersMap{ "someParameter": {Value: parameter}, diff --git a/openapi3/operation.go b/openapi3/operation.go index 02df2d278..d859a437c 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -34,7 +34,7 @@ type Operation struct { RequestBody *RequestBodyRef `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` // Responses. - Responses Responses `json:"responses" yaml:"responses"` // Required + Responses *Responses `json:"responses" yaml:"responses"` // Required // Optional callbacks Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` @@ -169,18 +169,14 @@ func (operation *Operation) AddParameter(p *Parameter) { } func (operation *Operation) AddResponse(status int, response *Response) { - responses := operation.Responses - if responses == nil { - responses = NewResponses() - operation.Responses = responses - } code := "default" - if status != 0 { + if 0 < status && status < 1000 { code = strconv.FormatInt(int64(status), 10) } - responses[code] = &ResponseRef{ - Value: response, + if operation.Responses == nil { + operation.Responses = NewResponses() } + operation.Responses.Set(code, &ResponseRef{Value: response}) } // Validate returns an error if Operation does not comply with the OpenAPI spec. diff --git a/openapi3/operation_test.go b/openapi3/operation_test.go index 50684a3ae..4c3a7bde5 100644 --- a/openapi3/operation_test.go +++ b/openapi3/operation_test.go @@ -8,17 +8,16 @@ import ( "github.com/stretchr/testify/require" ) -var operation *Operation - -func initOperation() { - operation = NewOperation() +func initOperation() *Operation { + operation := NewOperation() operation.Description = "Some description" operation.Summary = "Some summary" operation.Tags = []string{"tag1", "tag2"} + return operation } func TestAddParameter(t *testing.T) { - initOperation() + operation := initOperation() operation.AddParameter(NewQueryParameter("param1")) operation.AddParameter(NewCookieParameter("param2")) require.Equal(t, "param1", operation.Parameters.GetByInAndName("query", "param1").Name) @@ -26,20 +25,20 @@ func TestAddParameter(t *testing.T) { } func TestAddResponse(t *testing.T) { - initOperation() + operation := initOperation() operation.AddResponse(200, NewResponse()) operation.AddResponse(400, NewResponse()) - require.NotNil(t, "status 200", operation.Responses.Get(200).Value) - require.NotNil(t, "status 400", operation.Responses.Get(400).Value) + require.NotNil(t, "status 200", operation.Responses.Status(200).Value) + require.NotNil(t, "status 400", operation.Responses.Status(400).Value) } func operationWithoutResponses() *Operation { - initOperation() + operation := initOperation() return operation } func operationWithResponses() *Operation { - initOperation() + operation := initOperation() operation.AddResponse(200, NewResponse().WithDescription("some response")) return operation } diff --git a/openapi3/paths.go b/openapi3/paths.go index 0986b0557..2c252a54e 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -9,28 +9,58 @@ import ( // 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 { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + m map[string]*PathItem +} + +// NewPathsWithCapacity builds a paths object of the given capacity. +func NewPathsWithCapacity(cap int) *Paths { + return &Paths{m: make(map[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 a named path item +func WithPath(path string, pathItem *PathItem) NewPathsOption { + return func(paths *Paths) { + if p := pathItem; p != nil && path != "" { + paths.Set(path, p) + } + } +} // 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 := make([]string, 0, paths.Len()) + for key := range paths.Map() { keys = append(keys, key) } sort.Strings(keys) for _, path := range keys { - pathItem := paths[path] + pathItem := paths.Value(path) 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) @@ -106,23 +136,23 @@ func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error return err } - return nil + return validateExtensions(ctx, paths.Extensions) } // InMatchingOrder returns paths in the order they are matched against URLs. // 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 { + if paths.Len() == 0 { return nil } vars := make(map[int][]string) max := 0 - for path := range paths { + for path := range paths.Map() { count := strings.Count(path, "}") vars[count] = append(vars[count], path) if count > max { @@ -130,7 +160,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 +182,15 @@ func (paths Paths) InMatchingOrder() []string { // pathItem := path.Find("/person/{name}") // // would return the correct path item. -func (paths Paths) Find(key string) *PathItem { +func (paths *Paths) Find(key string) *PathItem { // Try directly access the map - pathItem := paths[key] + pathItem := paths.Value(key) if pathItem != nil { return pathItem } normalizedPath, expected, _ := normalizeTemplatedPath(key) - for path, pathItem := range paths { + for path, pathItem := range paths.Map() { pathNormalized, got, _ := normalizeTemplatedPath(path) if got == expected && pathNormalized == normalizedPath { return pathItem @@ -169,9 +199,9 @@ 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 urlPath, pathItem := range paths.Map() { if pathItem == nil { continue } diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index 3f044303a..e328c33eb 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -233,13 +233,15 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) - require.IsType(t, Paths{}, v) - require.Equal(t, reflect.TypeOf(Paths{}).Kind(), kind) + require.NotNil(t, v) + require.IsType(t, &Paths{}, v) + require.Equal(t, reflect.TypeOf(&Paths{}).Kind(), kind) ptr, err = jsonpointer.New("/paths/~1pet") require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, &PathItem{}, v) require.Equal(t, reflect.TypeOf(&PathItem{}).Kind(), kind) @@ -247,6 +249,7 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, &Operation{}, v) require.Equal(t, reflect.TypeOf(&Operation{}).Kind(), kind) @@ -254,13 +257,15 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) - require.IsType(t, Responses{}, v) - require.Equal(t, reflect.TypeOf(Responses{}).Kind(), kind) + require.NotNil(t, v) + require.IsType(t, &Responses{}, v) + require.Equal(t, reflect.TypeOf(&Responses{}).Kind(), kind) ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200") require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, &Response{}, v) require.Equal(t, reflect.TypeOf(&Response{}).Kind(), kind) @@ -268,6 +273,7 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, Content{}, v) require.Equal(t, reflect.TypeOf(Content{}).Kind(), kind) @@ -275,6 +281,7 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, &Ref{}, v) require.Equal(t, reflect.Ptr, kind) require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) @@ -283,6 +290,7 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, &Ref{}, v) require.Equal(t, reflect.Ptr, kind) require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) @@ -291,6 +299,7 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, &Schema{}, v) require.Equal(t, reflect.Ptr, kind) require.Equal(t, "integer", v.(*Schema).Type) @@ -299,6 +308,7 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, &Schema{}, v) require.Equal(t, reflect.Ptr, kind) require.Equal(t, "string", v.(*Schema).Type) @@ -307,6 +317,7 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) + require.NotNil(t, v) require.IsType(t, &Schema{}, v) require.Equal(t, reflect.Ptr, kind) require.Equal(t, "integer", v.(*Schema).Type) diff --git a/openapi3/response.go b/openapi3/response.go index 24b3a5566..6cc26a5bd 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -4,88 +4,107 @@ import ( "context" "encoding/json" "errors" - "fmt" "sort" "strconv" - - "github.com/go-openapi/jsonpointer" ) // Responses is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responses-object -type Responses map[string]*ResponseRef +type Responses struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` -var _ jsonpointer.JSONPointable = (*Responses)(nil) + m map[string]*ResponseRef +} -// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (responses Responses) JSONLookup(token string) (interface{}, error) { - ref, ok := responses[token] - if !ok { - return nil, fmt.Errorf("invalid token reference: %q", token) - } +// NewResponsesWithCapacity builds a responses object of the given capacity. +func NewResponsesWithCapacity(cap int) *Responses { + return &Responses{m: make(map[string]*ResponseRef, cap)} +} + +// NewResponses builds a responses object with response objects in insertion order. +// Given no arguments, NewResponses returns a valid responses object containing a default match-all reponse. +func NewResponses(opts ...NewResponsesOption) *Responses { + var responses *Responses + if n := len(opts); n != 0 { + responses = NewResponsesWithCapacity(n) + } else { + responses = &Responses{m: map[string]*ResponseRef{ + "default": {Value: NewResponse().WithDescription("")}, + }} + } + for _, opt := range opts { + opt(responses) + } + return responses +} + +// NewResponsesOption describes options to NewResponses func +type NewResponsesOption func(*Responses) - if ref != nil && ref.Ref != "" { - return &Ref{Ref: ref.Ref}, nil +// WithStatus adds a status code keyed ResponseRef +func WithStatus(status int, responseRef *ResponseRef) NewResponsesOption { + return func(responses *Responses) { + if r := responseRef; r != nil { + code := strconv.FormatInt(int64(status), 10) + responses.Set(code, r) + } } - return ref.Value, nil } -func NewResponses() Responses { - r := make(Responses) - r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")} - return r +// WithName adds a name-keyed Response +func WithName(name string, response *Response) NewResponsesOption { + return func(responses *Responses) { + if r := response; r != nil && name != "" { + responses.Set(name, &ResponseRef{Value: r}) + } + } } -func (responses Responses) Default() *ResponseRef { - return responses["default"] +// Default returns the default response +func (responses *Responses) Default() *ResponseRef { + return responses.Value("default") } -// Get returns a ResponseRef for the given status +// Status returns a ResponseRef for the given status // If an exact match isn't initially found a patterned field is checked using // the first digit to determine the range (eg: 201 to 2XX) // See https://spec.openapis.org/oas/v3.0.3#patterned-fields-0 -func (responses Responses) Get(status int) *ResponseRef { +func (responses *Responses) Status(status int) *ResponseRef { st := strconv.FormatInt(int64(status), 10) - if rref, ok := responses[st]; ok { + if rref := responses.Value(st); rref != nil { return rref } - st = string(st[0]) + "XX" - switch st { - case "1XX": - return responses["1XX"] - case "2XX": - return responses["2XX"] - case "3XX": - return responses["3XX"] - case "4XX": - return responses["4XX"] - case "5XX": - return responses["5XX"] - default: - return nil + if 99 < status && status < 600 { + st = string(st[0]) + "XX" + switch st { + case "1XX", "2XX", "3XX", "4XX", "5XX": + return responses.Value(st) + } } + return nil } // Validate returns an error if Responses does not comply with the OpenAPI spec. -func (responses Responses) Validate(ctx context.Context, opts ...ValidationOption) error { +func (responses *Responses) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if len(responses) == 0 { + if responses.Len() == 0 { return errors.New("the responses object MUST contain at least one response code") } - keys := make([]string, 0, len(responses)) - for key := range responses { + keys := make([]string, 0, responses.Len()) + for key := range responses.Map() { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { - v := responses[key] + v := responses.Value(key) if err := v.Validate(ctx); err != nil { return err } } - return nil + + return validateExtensions(ctx, responses.Extensions) } // Response is specified by OpenAPI/Swagger 3.0 standard. diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 5347a9399..967f1bab8 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -1042,7 +1042,7 @@ func TestDecodeParameter(t *testing.T) { Title: "MyAPI", Version: "0.1", } - doc := &openapi3.T{OpenAPI: "3.0.0", Info: info, Paths: openapi3.Paths{}} + doc := &openapi3.T{OpenAPI: "3.0.0", Info: info, Paths: openapi3.NewPaths()} op := &openapi3.Operation{ OperationID: "test", Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index 67bff3cda..87a8a1119 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -44,10 +44,10 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error // Find input for the current status responses := route.Operation.Responses - if len(responses) == 0 { + if responses.Len() == 0 { return nil } - responseRef := responses.Get(status) // Response + responseRef := responses.Status(status) // Response if responseRef == nil { responseRef = responses.Default() // Default input } @@ -104,7 +104,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error if contentType == nil { return &ResponseError{ Input: input, - Reason: fmt.Sprintf("response header Content-Type has unexpected value: %q", inputMIME), + Reason: fmt.Sprintf("response %s: %q", prefixInvalidCT, inputMIME), } } diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index a95b08554..85ff0dc7a 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -56,8 +56,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{ { @@ -136,9 +136,9 @@ func TestFilter(t *testing.T) { }, Responses: openapi3.NewResponses(), }, - }, + }), - "/issue151": &openapi3.PathItem{ + openapi3.WithPath("/issue151", &openapi3.PathItem{ Get: &openapi3.Operation{ Responses: openapi3.NewResponses(), }, @@ -152,8 +152,8 @@ func TestFilter(t *testing.T) { }, }, }, - }, - }, + }), + ), } err := doc.Validate(context.Background()) @@ -527,7 +527,7 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test Title: "MyAPI", Version: "0.1", }, - Paths: map[string]*openapi3.PathItem{}, + Paths: openapi3.NewPaths(), Security: openapi3.SecurityRequirements{ { securitySchemes[1].Name: {}, @@ -555,12 +555,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()) @@ -655,7 +655,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{}, }, @@ -680,12 +680,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()) @@ -752,7 +752,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{}, }, @@ -780,14 +780,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..2d092426a 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -57,7 +57,7 @@ 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 +113,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 bebe05659..4f40719b6 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").WithSchema(openapi3.NewStringSchema())}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y").WithSchema(openapi3.NewFloat64Schema())}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z").WithSchema(openapi3.NewIntegerSchema())}, }, - }, - "/books/{bookid}": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid").WithSchema(openapi3.NewStringSchema())}, }, - }, - "/books/{bookid}.json": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}.json", &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2").WithSchema(openapi3.NewStringSchema())}, }, - }, - "/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) @@ -259,7 +259,9 @@ func TestServerPath(t *testing.T) { newServerWithVariables( "/", nil, - )}, + ), + }, + Paths: openapi3.NewPaths(), }) require.NoError(t, err) } @@ -277,16 +279,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 +320,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 cc4ccc25b..306449d3c 100644 --- a/routers/legacy/router.go +++ b/routers/legacy/router.go @@ -64,7 +64,7 @@ func NewRouter(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Rout } router := &Router{doc: doc} root := router.node() - for path, pathItem := range doc.Paths { + for path, pathItem := range doc.Paths.Map() { for method, operation := range pathItem.Operations() { method = strings.ToUpper(method) if err := root.Add(method+" "+path, &routers.Route{ @@ -143,7 +143,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 554ecd16a..99704b19f 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").WithSchema(openapi3.NewStringSchema())}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y").WithSchema(openapi3.NewFloat64Schema())}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z").WithSchema(openapi3.NewIntegerSchema())}, }, - }, - "/books/{bookid}": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid").WithSchema(openapi3.NewStringSchema())}, }, - }, - "/books/{bookid2}.json": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid2}.json", &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2").WithSchema(openapi3.NewStringSchema())}, }, - }, - "/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) @@ -200,10 +200,10 @@ func TestRouter(t *testing.T) { } content := openapi3.NewContentWithJSONSchema(schema) responses := openapi3.NewResponses() - responses["default"].Value.Content = content - doc.Paths["/withExamples"] = &openapi3.PathItem{ + responses.Value("default").Value.Content = content + doc.Paths.Set("/withExamples", &openapi3.PathItem{ Get: &openapi3.Operation{Responses: responses}, - } + }) err = doc.Validate(context.Background()) require.Error(t, err) r, err = NewRouter(doc)