From b381c568d9d9ff82bee603ae50b4de1e54c7ed8b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 23 Jan 2023 16:38:29 +0100 Subject: [PATCH] openapi3: add support for extensions on the few types left Signed-off-by: Pierre Fenoll --- README.md | 6 + openapi2conv/issue558_test.go | 2 +- openapi2conv/issue573_test.go | 4 +- openapi2conv/openapi2_conv.go | 28 +-- openapi3/callback.go | 131 ++++++++++++- openapi3/components.go | 10 +- openapi3/internalize_refs.go | 19 +- openapi3/issue301_test.go | 22 ++- openapi3/issue341_test.go | 21 ++- openapi3/issue376_test.go | 2 +- openapi3/issue513_test.go | 2 +- openapi3/issue753_test.go | 16 +- openapi3/load_with_go_embed_test.go | 11 +- openapi3/loader.go | 34 ++-- .../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 | 82 ++++---- openapi3/loader_test.go | 40 ++-- openapi3/openapi3.go | 9 +- openapi3/openapi3_test.go | 24 ++- openapi3/operation.go | 14 +- openapi3/operation_test.go | 19 +- openapi3/paths.go | 168 +++++++++++++++-- openapi3/refs_test.go | 16 +- openapi3/response.go | 175 ++++++++++++++++-- openapi3filter/req_resp_decoder_test.go | 4 +- openapi3filter/validate_response.go | 4 +- openapi3filter/validation_test.go | 40 ++-- routers/gorillamux/router.go | 4 +- routers/gorillamux/router_test.go | 78 ++++---- routers/legacy/router.go | 4 +- routers/legacy/router_test.go | 62 +++---- 35 files changed, 801 insertions(+), 316 deletions(-) diff --git a/README.md b/README.md index d272d0d64..2b5adc0f0 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,12 @@ This will change the schema validation errors to return only the `Reason` field, ## Sub-v0 breaking API changes +### next +* `(openapi3.Responses).Get(int)` renamed to `(*openapi3.Responses).Status(int)` +* `Responses` field of `openapi3.Components` is now a pointer +* `Paths` field of `openapi3.T` is now a pointer +* Package `openapi3`'s `NewResponses() *Responses` function was renamed to `NewEmptyResponses` + ### v0.116.0 * Dropped `openapi3filter.DefaultOptions`. Use `&openapi3filter.Options{}` directly instead. 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/openapi2_conv.go b/openapi2conv/openapi2_conv.go index c80e67201..253f017a2 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -67,25 +67,24 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { } 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 = openapi3.NewResponsesWithCapacity(len(responses)) for k, response := range responses { r, err := ToV3Response(response, doc2.Produces) if err != nil { return nil, err } - doc3.Components.Responses[k] = r + doc3.Components.Responses.Set(k, r) } } @@ -186,15 +185,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 } @@ -605,13 +603,16 @@ 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 { + + // TODO: add paths extensions to doc2 + for path, pathItem := range doc3.Paths.Map() { if pathItem == nil { continue } @@ -1041,9 +1042,10 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components return result, nil } -func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) { - v2Responses := make(map[string]*openapi2.Response, len(responses)) - for k, response := range responses { +func FromV3Responses(responses *openapi3.Responses, components *openapi3.Components) (map[string]*openapi2.Response, error) { + // TODO: add responses extensions to doc2 + v2Responses := make(map[string]*openapi2.Response, responses.Len()) + for k, response := range responses.Map() { r, err := FromV3Response(response, components) if err != nil { return nil, err diff --git a/openapi3/callback.go b/openapi3/callback.go index 39a5955cd..ed6065dd5 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -2,8 +2,10 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "sort" + "strings" "github.com/go-openapi/jsonpointer" ) @@ -27,19 +29,138 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) { // Callback is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object -type Callback map[string]*PathItem +type Callback struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + m map[string]*PathItem +} + +var _ jsonpointer.JSONPointable = (*Callback)(nil) + +// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable +func (callback *Callback) JSONLookup(token string) (interface{}, error) { + pathItem, ok := callback.Get(token) + if !ok { + return nil, fmt.Errorf("invalid token reference: %q", token) + } + + if pathItem != nil { + if pathItem.Ref != "" { + return &Ref{Ref: pathItem.Ref}, nil + } + return pathItem, nil + } + + v, _, err := jsonpointer.GetForToken(callback.Extensions, token) + return v, err +} + +// Get returns the callback for key and the presence bit +func (callback *Callback) Get(key string) (*PathItem, bool) { + if callback == nil || callback.m == nil { + return nil, false + } + v, ok := callback.m[key] + return v, ok +} + +// Value returns the callback for key or nil +func (callback *Callback) Value(key string) *PathItem { + if callback == nil || callback.m == nil { + return nil + } + return callback.m[key] +} + +// Set adds or replaces the callback value for key +func (callback *Callback) Set(key string, value *PathItem) { + if callback == nil { + callback = &Callback{} + } + if callback.m == nil { + callback.m = make(map[string]*PathItem) + } + callback.m[key] = value +} + +// Len returns the amount of callbacks +func (callback *Callback) Len() int { + if callback == nil || callback.m == nil { + return 0 + } + return len(callback.m) +} + +// Map returns callbacks as an unordered map +func (callback *Callback) Map() map[string]*PathItem { + if callback == nil { + return nil + } + return callback.m +} + +// 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 pathItem PathItem + if err = pathItem.UnmarshalJSON(data); err != nil { + return + } + x.m[k] = &pathItem + } + *callback = x + return +} // 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.m[key] if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/components.go b/openapi3/components.go index c99edf0a8..31e8f031c 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -17,7 +17,7 @@ type Components struct { Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` RequestBodies RequestBodies `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` - Responses Responses `json:"responses,omitempty" yaml:"responses,omitempty"` + Responses *Responses `json:"responses,omitempty" yaml:"responses,omitempty"` SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` Links Links `json:"links,omitempty" yaml:"links,omitempty"` @@ -46,7 +46,7 @@ func (components Components) MarshalJSON() ([]byte, error) { if x := components.RequestBodies; len(x) != 0 { m["requestBodies"] = x } - if x := components.Responses; len(x) != 0 { + if x := components.Responses; x.Len() != 0 { m["responses"] = x } if x := components.SecuritySchemes; len(x) != 0 { @@ -134,13 +134,13 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp } } - responses := make([]string, 0, len(components.Responses)) - for name := range components.Responses { + responses := make([]string, 0, components.Responses.Len()) + for name := range components.Responses.Map() { responses = append(responses, name) } sort.Strings(responses) for _, k := range responses { - v := components.Responses[k] + v := components.Responses.Value(k) if err = ValidateIdentifier(k); err != nil { return fmt.Errorf("response %q: %w", k, err) } diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index cf212c6f3..48dfc19b8 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -148,7 +148,7 @@ func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver, } name := refNameResolver(r.Ref) if doc.Components != nil { - if _, ok := doc.Components.Responses[name]; ok { + if _, ok := doc.Components.Responses.Get(name); ok { r.Ref = "#/components/responses/" + name return true } @@ -157,10 +157,7 @@ func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver, if doc.Components == nil { doc.Components = &Components{} } - if doc.Components.Responses == nil { - doc.Components.Responses = make(Responses) - } - doc.Components.Responses[name] = &ResponseRef{Value: r.Value} + doc.Components.Responses.Set(name, &ResponseRef{Value: r.Value}) r.Ref = "#/components/responses/" + name return true } @@ -313,8 +310,8 @@ func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver, parentIsExte } } -func (doc *T) derefResponses(es Responses, refNameResolver RefNameResolver, parentIsExternal bool) { - for _, e := range es { +func (doc *T) derefResponses(es *Responses, refNameResolver RefNameResolver, parentIsExternal bool) { + for _, e := range es.Map() { isExternal := doc.addResponseToSpec(e, refNameResolver, parentIsExternal) if e.Value != nil { doc.derefHeaders(e.Value.Headers, refNameResolver, isExternal || parentIsExternal) @@ -356,7 +353,8 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso for _, cb := range op.Callbacks { isExternal := doc.addCallbackToSpec(cb, refNameResolver, parentIsExternal) if cb.Value != nil { - doc.derefPaths(*cb.Value, refNameResolver, parentIsExternal || isExternal) + cbValue := (*cb.Value).Map() + doc.derefPaths(cbValue, refNameResolver, parentIsExternal || isExternal) } } doc.derefResponses(op.Responses, refNameResolver, parentIsExternal) @@ -425,10 +423,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 332b9226e..96c16a055 100644 --- a/openapi3/issue513_test.go +++ b/openapi3/issue513_test.go @@ -87,7 +87,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/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/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 de0a8132a..e52970172 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -208,7 +208,7 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { return } } - for _, component := range components.Responses { + for _, component := range components.Responses.Map() { if err = loader.resolveResponseRef(doc, component, location); err != nil { return } @@ -244,7 +244,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 } @@ -390,12 +390,25 @@ func readableType(x interface{}) string { } func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { - // Special case due to multijson - if s, ok := cursor.(*SchemaRef); ok && fieldName == "additionalProperties" { - if ap := s.Value.AdditionalProperties.Has; ap != nil { - return *ap, nil + switch c := cursor.(type) { + // ge -F ' m map[string]*' + 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 + + // default: + // fmt.Printf(">>> %T \n", cursor) + + case *SchemaRef: + if fieldName == "additionalProperties" { + if ap := c.Value.AdditionalProperties.Has; ap != nil { + return *ap, nil + } + return c.Value.AdditionalProperties.Schema, nil } - return s.Value.AdditionalProperties.Schema, nil } switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { @@ -650,7 +663,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen loader.visitedResponse[component.Value] = struct{}{} } - if component == nil { + if component == nil || (component.Ref == "" && component.Value == nil) { return errors.New("invalid response: value MUST be an object") } ref := component.Ref @@ -913,7 +926,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 } @@ -991,7 +1004,6 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat } *pathItem = resolved } - pathItem.Ref = ref } for _, parameter := range pathItem.Parameters { @@ -1010,7 +1022,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 5cec93452..f08843248 100644 --- a/openapi3/loader_outside_refs_test.go +++ b/openapi3/loader_outside_refs_test.go @@ -16,5 +16,14 @@ 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) } diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go index 8fee2f4c2..ce70bcce3 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 50d2c7d24..b62dc5021 100644 --- a/openapi3/loader_relative_refs_test.go +++ b/openapi3/loader_relative_refs_test.go @@ -35,7 +35,7 @@ var refTestDataEntries = []refTestDataEntry{ contentTemplate: externalResponseRefTemplate, testFunc: func(t *testing.T, doc *T) { desc := "description" - require.Equal(t, &desc, doc.Components.Responses["TestResponse"].Value.Description) + require.Equal(t, &desc, doc.Components.Responses.Value("TestResponse").Value.Description) }, }, { @@ -81,42 +81,42 @@ var refTestDataEntries = []refTestDataEntry{ name: "PathParameterRef", contentTemplate: externalPathParameterRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test/{id}"].Parameters[0].Value.Name) - require.Equal(t, "id", doc.Paths["/test/{id}"].Parameters[0].Value.Name) + require.NotNil(t, doc.Paths.Value("/test/{id}").Parameters[0].Value.Name) + require.Equal(t, "id", doc.Paths.Value("/test/{id}").Parameters[0].Value.Name) }, }, { name: "PathOperationParameterRef", contentTemplate: externalPathOperationParameterRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test/{id}"].Get.Parameters[0].Value) - require.Equal(t, "id", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Name) + require.NotNil(t, doc.Paths.Value("/test/{id}").Get.Parameters[0].Value) + require.Equal(t, "id", doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Name) }, }, { name: "PathOperationRequestBodyRef", contentTemplate: externalPathOperationRequestBodyRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value) - require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content) + require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value) + require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value.Content) }, }, { name: "PathOperationResponseRef", contentTemplate: externalPathOperationResponseRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses.Default().Value) desc := "description" - require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) + require.Equal(t, &desc, doc.Paths.Value("/test").Post.Responses.Default().Value.Description) }, }, { name: "PathOperationParameterSchemaRef", contentTemplate: externalPathOperationParameterSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value) - require.Equal(t, "string", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value.Type) - require.Equal(t, "id", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Name) + require.NotNil(t, doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Schema.Value) + require.Equal(t, "string", doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Schema.Value.Type) + require.Equal(t, "id", doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Name) }, }, @@ -124,7 +124,7 @@ var refTestDataEntries = []refTestDataEntry{ name: "PathOperationParameterRefWithContentInQuery", contentTemplate: externalPathOperationParameterWithContentInQueryTemplate, testFunc: func(t *testing.T, doc *T) { - schemaRef := doc.Paths["/test/{id}"].Get.Parameters[0].Value.Content["application/json"].Schema + schemaRef := doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Content["application/json"].Schema require.NotNil(t, schemaRef.Value) require.Equal(t, "string", schemaRef.Value.Type) }, @@ -134,36 +134,36 @@ var refTestDataEntries = []refTestDataEntry{ name: "PathOperationRequestBodyExampleRef", contentTemplate: externalPathOperationRequestBodyExampleRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value) - require.Equal(t, "description", doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description) + require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value) + require.Equal(t, "description", doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description) }, }, { name: "PathOperationReqestBodyContentSchemaRef", contentTemplate: externalPathOperationReqestBodyContentSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value) - require.Equal(t, "string", doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) + require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Schema.Value) + require.Equal(t, "string", doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) }, }, { name: "PathOperationResponseExampleRef", contentTemplate: externalPathOperationResponseExampleRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses.Default().Value) desc := "testdescription" - require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) - require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Examples["application/json"].Value.Description) + require.Equal(t, &desc, doc.Paths.Value("/test").Post.Responses.Default().Value.Description) + require.Equal(t, "description", doc.Paths.Value("/test").Post.Responses.Default().Value.Content["application/json"].Examples["application/json"].Value.Description) }, }, { name: "PathOperationResponseSchemaRef", contentTemplate: externalPathOperationResponseSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses.Default().Value) desc := "testdescription" - require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) - require.Equal(t, "string", doc.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, &desc, doc.Paths.Value("/test").Post.Responses.Default().Value.Description) + require.Equal(t, "string", doc.Paths.Value("/test").Post.Responses.Default().Value.Content["application/json"].Schema.Value.Type) }, }, { @@ -178,8 +178,8 @@ var refTestDataEntries = []refTestDataEntry{ name: "RequestResponseHeaderRef", contentTemplate: externalRequestResponseHeaderRefTemplate, testFunc: func(t *testing.T, doc *T) { - require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) - require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + require.NotNil(t, doc.Paths.Value("/test").Post.Responses.Default().Value.Headers["X-TEST-HEADER"].Value.Description) + require.Equal(t, "description", doc.Paths.Value("/test").Post.Responses.Default().Value.Headers["X-TEST-HEADER"].Value.Description) }, }, } @@ -733,7 +733,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ contentTemplate: relativeResponseDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { desc := "description" - require.Equal(t, &desc, doc.Components.Responses["TestResponse"].Value.Description) + require.Equal(t, &desc, doc.Components.Responses.Value("TestResponse").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"]) }, }, } @@ -914,40 +914,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 4c6f8c841..94ee152e3 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 TestLoadWithRecursiveReferenceInRefrerenceInLocalReference(t *testing.T) { @@ -322,8 +322,8 @@ func TestLoadWithRecursiveReferenceInRefrerenceInLocalReference(t *testing.T) { require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "integer", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Type) - require.Equal(t, "int64", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Format) + require.Equal(t, "integer", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Type) + require.Equal(t, "int64", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Format) } func TestLoadWithReferenceInReferenceInProperty(t *testing.T) { @@ -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) { @@ -351,9 +351,9 @@ func TestLoadFileWithExternalSchemaRefSingleComponent(t *testing.T) { doc, err := loader.LoadFromFile("testdata/testrefsinglecomponent.openapi.json") require.NoError(t, err) - require.NotNil(t, doc.Components.Responses["SomeResponse"]) + require.NotNil(t, doc.Components.Responses.Value("SomeResponse")) desc := "this is a single response definition" - require.Equal(t, &desc, doc.Components.Responses["SomeResponse"].Value.Description) + require.Equal(t, &desc, doc.Components.Responses.Value("SomeResponse").Value.Description) } func TestLoadRequestResponseHeaderRef(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/openapi3.go b/openapi3/openapi3.go index e488a59e8..74e3972ae 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -15,7 +15,7 @@ type T struct { OpenAPI string `json:"openapi" yaml:"openapi"` // Required Components *Components `json:"components,omitempty" yaml:"components,omitempty"` Info *Info `json:"info" yaml:"info"` // Required - Paths Paths `json:"paths" yaml:"paths"` // Required + Paths *Paths `json:"paths" yaml:"paths"` // Required Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -72,13 +72,10 @@ 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 + doc.Paths.Set(path, pathItem) } pathItem.SetOperation(method, operation) } diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index e01af82ba..97a547d21 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -271,8 +271,8 @@ func spec() *T { Title: "MyAPI", Version: "0.1", }, - Paths: Paths{ - "/hello": &PathItem{ + Paths: NewPaths( + WithPath("/hello", &PathItem{ Post: &Operation{ Parameters: Parameters{ { @@ -284,12 +284,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{ { @@ -297,8 +297,8 @@ func spec() *T { Value: parameter, }, }, - }, - }, + }), + ), Components: &Components{ Parameters: ParametersMap{ "someParameter": { @@ -310,11 +310,9 @@ func spec() *T { Value: requestBody, }, }, - Responses: Responses{ - "someResponse": { - Value: response, - }, - }, + Responses: NewResponses( + WithName("someResponse", response), + ), Schemas: Schemas{ "someSchema": { Value: schema, diff --git a/openapi3/operation.go b/openapi3/operation.go index f811f9d02..b4d1ad7b1 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"` @@ -166,18 +166,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 = &Responses{} } + 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..38b6c7574 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -2,35 +2,179 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "sort" "strings" + + "github.com/go-openapi/jsonpointer" ) // 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 +} + +var _ jsonpointer.JSONPointable = (*Responses)(nil) + +// Get returns the path for key and the presence bit +func (paths *Paths) Get(key string) (*PathItem, bool) { + if paths == nil || paths.m == nil { + return nil, false + } + v, ok := paths.m[key] + return v, ok +} + +// Value returns the path for key or nil +func (paths *Paths) Value(key string) *PathItem { + if paths == nil || paths.m == nil { + return nil + } + return paths.m[key] +} + +// Set adds or replaces the path value for key +func (paths *Paths) Set(key string, value *PathItem) { + if paths == nil { + paths = &Paths{} + } + if paths.m == nil { + paths.m = make(map[string]*PathItem) + } + paths.m[key] = value +} + +// Len returns the amount of paths +func (paths *Paths) Len() int { + if paths == nil || paths.m == nil { + return 0 + } + return len(paths.m) +} + +// Map returns paths as an unordered map +func (paths *Paths) Map() map[string]*PathItem { + if paths == nil { + return nil + } + return paths.m +} + +// 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 pathItem PathItem + if err = pathItem.UnmarshalJSON(data); err != nil { + return + } + x.m[k] = &pathItem + } + *paths = x + return +} + +// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable +func (paths *Paths) JSONLookup(token string) (interface{}, error) { + pathItem, ok := paths.Get(token) + if !ok { + return nil, fmt.Errorf("invalid token reference: %q", token) + } + + if pathItem != nil { + if pathItem.Ref != "" { + return &Ref{Ref: pathItem.Ref}, nil + } + return pathItem, nil + } + + v, _, err := jsonpointer.GetForToken(paths.Extensions, token) + return v, err +} + +// 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 paths as an option to NewPaths +func WithPath(path string, pathItem *PathItem) NewPathsOption { + return func(paths *Paths) { paths.Set(path, pathItem) } +} // Validate returns an error if Paths does not comply with the OpenAPI spec. func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error { 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) @@ -116,13 +260,13 @@ func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error 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 +274,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))) @@ -154,13 +298,13 @@ func (paths Paths) InMatchingOrder() []string { // would return the correct path item. 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 @@ -171,7 +315,7 @@ func (paths Paths) Find(key string) *PathItem { 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 545c610b8..8af975402 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -230,22 +230,22 @@ 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.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.IsType(t, &PathItem{}, v) - require.Equal(t, reflect.TypeOf(&PathItem{}).Kind(), kind) + require.IsType(t, PathItem{}, v) + require.Equal(t, reflect.TypeOf(PathItem{}).Kind(), kind) ptr, err = jsonpointer.New("/paths/~1pet/put") require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) - require.IsType(t, &Operation{}, v) - require.Equal(t, reflect.TypeOf(&Operation{}).Kind(), kind) + require.IsType(t, Operation{}, v) + require.Equal(t, reflect.TypeOf(Operation{}).Kind(), kind) ptr, err = jsonpointer.New("/paths/~1pet/put/responses") require.NoError(t, err) @@ -258,8 +258,8 @@ components: require.NoError(t, err) v, kind, err = ptr.Get(doc) require.NoError(t, err) - require.IsType(t, &Response{}, v) - require.Equal(t, reflect.TypeOf(&Response{}).Kind(), kind) + require.IsType(t, Response{}, v) + require.Equal(t, reflect.TypeOf(Response{}).Kind(), kind) ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content") require.NoError(t, err) diff --git a/openapi3/response.go b/openapi3/response.go index 2035f4687..f1f58aa54 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -7,45 +7,177 @@ import ( "fmt" "sort" "strconv" + "strings" "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:"-"` + + m map[string]*ResponseRef +} var _ jsonpointer.JSONPointable = (*Responses)(nil) -func NewResponses() Responses { - r := make(Responses) - r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")} - return r +// NewEmptyResponses creates an empty yet valid responses object +func NewEmptyResponses() *Responses { + return &Responses{ + m: map[string]*ResponseRef{ + "default": &ResponseRef{Value: NewResponse().WithDescription("")}, + }, + } +} + +// 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. +func NewResponses(opts ...NewResponsesOption) *Responses { + responses := NewResponsesWithCapacity(len(opts)) + for _, opt := range opts { + opt(responses) + } + return responses +} + +// NewResponsesOption describes options to NewResponses func +type NewResponsesOption func(*Responses) + +// WithStatus adds a status code keyed ResponseRef as an option to NewResponses +func WithStatus(status int, responseRef *ResponseRef) NewResponsesOption { + code := strconv.FormatInt(int64(status), 10) + return func(responses *Responses) { responses.Set(code, responseRef) } +} + +// WithName adds a name-keyed response as an option to NewResponses +func WithName(name string, response *Response) NewResponsesOption { + return func(responses *Responses) { responses.Set(name, &ResponseRef{Value: response}) } +} + +// Default returns the default response +func (responses *Responses) Default() *ResponseRef { + return responses.Value("default") +} + +// Status returns the response that exactly matches the given status, or nil. +func (responses *Responses) Status(status int) *ResponseRef { + return responses.Value(strconv.FormatInt(int64(status), 10)) +} + +// FIXME Any HTTP status code can be used as the property name, but only one property per code, to describe the expected response for that HTTP status code. A Reference Object can link to a response that is defined in the OpenAPI Object's components/responses section. This field MUST be enclosed in quotation marks (for example, "200") for compatibility between JSON and YAML. To define a range of response codes, this field MAY contain the uppercase wildcard character X. For example, 2XX represents all response codes between [200-299]. Only the following range definitions are allowed: 1XX, 2XX, 3XX, 4XX, and 5XX. If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code. + +// Get returns the responses for key and the presence bit +func (responses *Responses) Get(key string) (*ResponseRef, bool) { + if responses == nil || responses.m == nil { + return nil, false + } + v, ok := responses.m[key] + return v, ok +} + +// Value returns the responses for key or nil +func (responses *Responses) Value(key string) *ResponseRef { + if responses == nil || responses.m == nil { + return nil + } + return responses.m[key] +} + +// Set adds or replaces the responses value for key +func (responses *Responses) Set(key string, value *ResponseRef) { + if responses.m == nil { + responses.m = make(map[string]*ResponseRef) + } + responses.m[key] = value } -func (responses Responses) Default() *ResponseRef { - return responses["default"] +// Len returns the amount of responsess +func (responses *Responses) Len() int { + if responses == nil { + return 0 + } + return len(responses.m) +} + +// Map returns responsess as an unordered map +func (responses *Responses) Map() map[string]*ResponseRef { + if responses == nil { + return nil + } + return responses.m +} + +// 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) } -func (responses Responses) Get(status int) *ResponseRef { - return responses[strconv.FormatInt(int64(status), 10)] +// 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 responseRef ResponseRef + if err = responseRef.UnmarshalJSON(data); err != nil { + return + } + x.m[k] = &responseRef + } + *responses = x + return } // 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 } @@ -54,16 +186,21 @@ func (responses Responses) Validate(ctx context.Context, opts ...ValidationOptio } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (responses Responses) JSONLookup(token string) (interface{}, error) { - ref, ok := responses[token] +func (responses *Responses) JSONLookup(token string) (interface{}, error) { + ref, ok := responses.Get(token) if !ok { return nil, fmt.Errorf("invalid token reference: %q", token) } - if ref != nil && ref.Ref != "" { - return &Ref{Ref: ref.Ref}, nil + if ref != nil { + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil } - return ref.Value, nil + + v, _, err := jsonpointer.GetForToken(responses.Extensions, token) + return v, err } // 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 21e23fae2..3d960c6e0 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -1044,11 +1044,11 @@ 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}}, - Responses: openapi3.NewResponses(), + Responses: openapi3.NewEmptyResponses(), } doc.AddOperation(path, http.MethodGet, op) err = doc.Validate(context.Background()) diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index dca13380a..b7a6df36a 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 } diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index 311556036..aef6ed186 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -57,8 +57,8 @@ func TestFilter(t *testing.T) { URL: "http://example.com/api/", }, }, - Paths: openapi3.Paths{ - "/prefix/{pathArg}/suffix": &openapi3.PathItem{ + Paths: openapi3.NewPaths( + openapi3.WithPath("/prefix/{pathArg}/suffix", &openapi3.PathItem{ Post: &openapi3.Operation{ Parameters: openapi3.Parameters{ { @@ -135,13 +135,13 @@ func TestFilter(t *testing.T) { }, }, }, - Responses: openapi3.NewResponses(), + Responses: openapi3.NewEmptyResponses(), }, - }, + }), - "/issue151": &openapi3.PathItem{ + openapi3.WithPath("/issue151", &openapi3.PathItem{ Get: &openapi3.Operation{ - Responses: openapi3.NewResponses(), + Responses: openapi3.NewEmptyResponses(), }, Parameters: openapi3.Parameters{ { @@ -153,8 +153,8 @@ func TestFilter(t *testing.T) { }, }, }, - }, - }, + }), + ), } err := doc.Validate(context.Background()) @@ -528,7 +528,7 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test Title: "MyAPI", Version: "0.1", }, - Paths: map[string]*openapi3.PathItem{}, + Paths: openapi3.NewPaths(), Security: openapi3.SecurityRequirements{ { securitySchemes[1].Name: {}, @@ -556,12 +556,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(), + Responses: openapi3.NewEmptyResponses(), }, - } + }) } err := doc.Validate(context.Background()) @@ -662,7 +662,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{}, }, @@ -687,12 +687,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(), + Responses: openapi3.NewEmptyResponses(), }, - } + }) } err := doc.Validate(context.Background()) @@ -759,7 +759,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{}, }, @@ -787,14 +787,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(), + Responses: openapi3.NewEmptyResponses(), }, - } + }) } 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 5fb9be2b0..c3e789a85 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -14,26 +14,26 @@ import ( ) func TestRouter(t *testing.T) { - helloCONNECT := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloDELETE := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloHEAD := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloPATCH := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloPUT := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloTRACE := &openapi3.Operation{Responses: openapi3.NewResponses()} - paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()} - booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} - partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()} + helloCONNECT := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloDELETE := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloGET := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloHEAD := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloPATCH := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloPOST := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloPUT := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloTRACE := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + paramsGET := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + booksPOST := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + partialGET := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", Version: "0.1", }, - Paths: openapi3.Paths{ - "/hello": &openapi3.PathItem{ + Paths: openapi3.NewPaths( + openapi3.WithPath("/hello", &openapi3.PathItem{ Connect: helloCONNECT, Delete: helloDELETE, Get: helloGET, @@ -43,34 +43,34 @@ func TestRouter(t *testing.T) { Post: helloPOST, Put: helloPUT, Trace: helloTRACE, - }, - "/onlyGET": &openapi3.PathItem{ + }), + openapi3.WithPath("/onlyGET", &openapi3.PathItem{ Get: helloGET, - }, - "/params/{x}/{y}/{z:.*}": &openapi3.PathItem{ + }), + openapi3.WithPath("/params/{x}/{y}/{z:.*}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")}, }, - }, - "/books/{bookid}": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, - }, - "/books/{bookid}.json": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}.json", &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, - }, - "/partial": &openapi3.PathItem{ + }), + openapi3.WithPath("/partial", &openapi3.PathItem{ Get: partialGET, - }, - }, + }), + ), } expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { @@ -80,7 +80,7 @@ func TestRouter(t *testing.T) { route, pathParams, err := r.FindRoute(req) if err != nil { if operation == nil { - pathItem := doc.Paths[uri] + pathItem := doc.Paths.Value(uri) if pathItem == nil { if err.Error() != routers.ErrPathNotFound.Error() { t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrPathNotFound, err) @@ -259,13 +259,15 @@ func TestServerPath(t *testing.T) { newServerWithVariables( "/", nil, - )}, + ), + }, + Paths: openapi3.NewPaths(), }) require.NoError(t, err) } func TestServerOverrideAtPathLevel(t *testing.T) { - helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} + helloGET := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ @@ -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) @@ -306,7 +308,7 @@ func TestServerOverrideAtPathLevel(t *testing.T) { } func TestRelativeURL(t *testing.T) { - helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} + helloGET := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ @@ -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 e9b875986..89377ce5f 100644 --- a/routers/legacy/router_test.go +++ b/routers/legacy/router_test.go @@ -13,26 +13,26 @@ import ( ) func TestRouter(t *testing.T) { - helloCONNECT := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloDELETE := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloHEAD := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloPATCH := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloPUT := &openapi3.Operation{Responses: openapi3.NewResponses()} - helloTRACE := &openapi3.Operation{Responses: openapi3.NewResponses()} - paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()} - booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} - partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()} + helloCONNECT := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloDELETE := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloGET := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloHEAD := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloPATCH := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloPOST := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloPUT := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + helloTRACE := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + paramsGET := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + booksPOST := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} + partialGET := &openapi3.Operation{Responses: openapi3.NewEmptyResponses()} doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", Version: "0.1", }, - Paths: openapi3.Paths{ - "/hello": &openapi3.PathItem{ + Paths: openapi3.NewPaths( + openapi3.WithPath("/hello", &openapi3.PathItem{ Connect: helloCONNECT, Delete: helloDELETE, Get: helloGET, @@ -42,34 +42,34 @@ func TestRouter(t *testing.T) { Post: helloPOST, Put: helloPUT, Trace: helloTRACE, - }, - "/onlyGET": &openapi3.PathItem{ + }), + openapi3.WithPath("/onlyGET", &openapi3.PathItem{ Get: helloGET, - }, - "/params/{x}/{y}/{z.*}": &openapi3.PathItem{ + }), + openapi3.WithPath("/params/{x}/{y}/{z.*}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")}, }, - }, - "/books/{bookid}": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid}", &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, - }, - "/books/{bookid2}.json": &openapi3.PathItem{ + }), + openapi3.WithPath("/books/{bookid2}.json", &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")}, }, - }, - "/partial": &openapi3.PathItem{ + }), + openapi3.WithPath("/partial", &openapi3.PathItem{ Get: partialGET, - }, - }, + }), + ), } expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { @@ -78,7 +78,7 @@ func TestRouter(t *testing.T) { route, pathParams, err := r.FindRoute(req) if err != nil { if operation == nil { - pathItem := doc.Paths[uri] + pathItem := doc.Paths.Value(uri) if pathItem == nil { if err.Error() != routers.ErrPathNotFound.Error() { t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrPathNotFound, err) @@ -199,11 +199,11 @@ func TestRouter(t *testing.T) { Example: 3, } content := openapi3.NewContentWithJSONSchema(schema) - responses := openapi3.NewResponses() - responses["default"].Value.Content = content - doc.Paths["/withExamples"] = &openapi3.PathItem{ + responses := openapi3.NewEmptyResponses() + 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)