From 76e05fcfa009e98a66ac3f0108ad968035f307b5 Mon Sep 17 00:00:00 2001 From: "Cody A. Ray" Date: Fri, 5 Jun 2020 02:58:25 -0500 Subject: [PATCH 001/260] Add support for `allOf`/etc in validation error handler (#217) --- openapi3/schema.go | 4 +- openapi3filter/fixtures/petstore.json | 145 ++++++++++++++++++++- openapi3filter/validation_error_encoder.go | 15 +-- openapi3filter/validation_error_test.go | 93 ++++++++----- 4 files changed, 209 insertions(+), 48 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index f06809743..427c376a4 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1108,12 +1108,12 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) ( if fast { return errSchema } - return &SchemaError{ + return markSchemaErrorKey(&SchemaError{ Value: value, Schema: schema, SchemaField: "required", Reason: fmt.Sprintf("Property '%s' is missing", k), - } + }, k) } } return diff --git a/openapi3filter/fixtures/petstore.json b/openapi3filter/fixtures/petstore.json index 00f4a926f..6c229a672 100644 --- a/openapi3filter/fixtures/petstore.json +++ b/openapi3filter/fixtures/petstore.json @@ -66,10 +66,10 @@ } ], "requestBody": { - "$ref": "#/components/requestBodies/Pet" + "$ref": "#/components/requestBodies/PetWithRequired" } }, - "put": { + "patch": { "tags": [ "pet" ], @@ -100,6 +100,32 @@ } } }, + "/pet2": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/PetAllOfRequiredProperties" + } + } + }, "/pet/findByStatus": { "get": { "tags": [ @@ -137,7 +163,10 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Pet" + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + {"$ref": "#/components/schemas/PetRequiredProperties"} + ] } } }, @@ -145,7 +174,10 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Pet" + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + {"$ref": "#/components/schemas/PetRequiredProperties"} + ] } } } @@ -1092,6 +1124,62 @@ } }, "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "readOnly": true + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "name": { + "type": "string", + "example": "doggie", + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "PetRequiredProperties": { + "type": "object", + "required": [ + "name", + "photoUrls" + ] + }, + "PetWithRequired": { "type": "object", "required": [ "name", @@ -1108,7 +1196,7 @@ }, "name": { "type": "string", - "example": "doggie" + "example": "doggie", }, "photoUrls": { "type": "array", @@ -1146,6 +1234,35 @@ } }, "requestBodies": { + "PetAllOfRequiredProperties": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "$ref": "#/components/schemas/PetRequiredProperties" + } + ] + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "$ref": "#/components/schemas/PetRequiredProperties" + } + ] + } + } + }, + "required": true + }, "Pet": { "content": { "application/json": { @@ -1162,6 +1279,22 @@ "description": "Pet object that needs to be added to the store", "required": true }, + "PetWithRequired": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PetWithRequired" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/PetWithRequired" + } + } + }, + "description": "Pet object that needs to be added to the store", + "required": true + }, "UserArray": { "content": { "application/json": { @@ -1197,4 +1330,4 @@ } } } -} +} \ No newline at end of file diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 556bcc8cf..49459ef9c 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -135,6 +135,11 @@ var propertyMissingNameRE = regexp.MustCompile(`Property '(?P[^']*)' is mi func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *ValidationError { cErr := &ValidationError{Title: innerErr.Reason} + // Handle "Origin" error + if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok { + cErr = convertSchemaError(e, originErr) + } + // Add http status code if e.Parameter != nil { cErr.Status = http.StatusBadRequest @@ -152,16 +157,6 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida } else if innerErr.JSONPointer() != nil { pointer := innerErr.JSONPointer() - // JSONPointer is rarely what you expect. - // 1. for "property is missing" errors, its an empty array - // 2. for nested attributes, it leaves off the last element from the JSONPointer - matches := propertyMissingNameRE.FindStringSubmatch(innerErr.Reason) - if matches != nil && len(matches) > 1 { - if len(pointer) == 0 || matches[1] != pointer[0] { - pointer = append(pointer, matches[1]) - } - } - cErr.Source = &ValidationErrorSource{ Pointer: toJSONPointer(pointer), } diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 74e5a55c8..420831dad 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -34,21 +34,24 @@ type validationArgs struct { r *http.Request } type validationTest struct { - name string - fields validationFields - args validationArgs - wantErr bool - wantErrBody string - wantErrReason string - wantErrSchemaReason string - wantErrSchemaPath string - wantErrSchemaValue interface{} - wantErrParam string - wantErrParamIn string - wantErrParseKind ParseErrorKind - wantErrParseValue interface{} - wantErrParseReason string - wantErrResponse *ValidationError + name string + fields validationFields + args validationArgs + wantErr bool + wantErrBody string + wantErrReason string + wantErrSchemaReason string + wantErrSchemaPath string + wantErrSchemaValue interface{} + wantErrSchemaOriginReason string + wantErrSchemaOriginPath string + wantErrSchemaOriginValue interface{} + wantErrParam string + wantErrParamIn string + wantErrParseKind ParseErrorKind + wantErrParseValue interface{} + wantErrParseReason string + wantErrResponse *ValidationError } func getValidationTests(t *testing.T) []*validationTest { @@ -239,9 +242,9 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrSchemaPath: "/1", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "JSON value is not one of the allowed values", + Title: "JSON value is not one of the allowed values", Detail: "Value 'watdis' at /1 must be one of: available, pending, sold", - Source: &ValidationErrorSource{Parameter: "status"}}, + Source: &ValidationErrorSource{Parameter: "status"}}, }, { name: "error - invalid enum value, allowing commas (without 'perhaps you intended' recommendation)", @@ -255,9 +258,9 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrSchemaPath: "/1", wantErrSchemaValue: "fish,with,commas", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "JSON value is not one of the allowed values", + Title: "JSON value is not one of the allowed values", Detail: "Value 'fish,with,commas' at /1 must be one of: dog, cat, turtle, bird,with,commas", - // No 'perhaps you intended' because its the right serialization format + // No 'perhaps you intended' because its the right serialization format Source: &ValidationErrorSource{Parameter: "kind"}}, }, { @@ -279,10 +282,10 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrReason: "doesn't match the schema", wantErrSchemaReason: "Property 'photoUrls' is missing", wantErrSchemaValue: map[string]string{"name": "Bahama"}, - wantErrSchemaPath: "/", + wantErrSchemaPath: "/photoUrls", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Property 'photoUrls' is missing", - Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, + Title: "Property 'photoUrls' is missing", + Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { name: "error - missing required nested object attribute", @@ -293,10 +296,10 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrReason: "doesn't match the schema", wantErrSchemaReason: "Property 'name' is missing", wantErrSchemaValue: map[string]string{}, - wantErrSchemaPath: "/category", + wantErrSchemaPath: "/category/name", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Property 'name' is missing", - Source: &ValidationErrorSource{Pointer: "/category/name"}}, + Title: "Property 'name' is missing", + Source: &ValidationErrorSource{Pointer: "/category/name"}}, }, { name: "error - missing required deeply nested object attribute", @@ -307,10 +310,10 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrReason: "doesn't match the schema", wantErrSchemaReason: "Property 'name' is missing", wantErrSchemaValue: map[string]string{}, - wantErrSchemaPath: "/category/tags/0", + wantErrSchemaPath: "/category/tags/0/name", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Property 'name' is missing", - Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"}}, + Title: "Property 'name' is missing", + Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"}}, }, { // TODO: Add support for validating readonly properties to upstream validator. @@ -334,8 +337,23 @@ func getValidationTests(t *testing.T) []*validationTest { // TODO: this shouldn't say "or not be present", but this requires recursively resolving // innerErr.JSONPointer() against e.RequestBody.Content["application/json"].Schema.Value (.Required, .Properties) wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Field must be set to array or not be present", - Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, + Title: "Field must be set to array or not be present", + Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, + }, + { + name: "error - missing required object attribute from allOf required overlay", + args: validationArgs{ + r: newPetstoreRequest(t, http.MethodPost, "/pet2", bytes.NewBufferString(`{"name":"Bahama"}`)), + }, + wantErrReason: "doesn't match the schema", + wantErrSchemaPath: "/", + wantErrSchemaValue: map[string]string{"name": "Bahama"}, + wantErrSchemaOriginReason: "Property 'photoUrls' is missing", + wantErrSchemaOriginValue: map[string]string{"name": "Bahama"}, + wantErrSchemaOriginPath: "/photoUrls", + wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, + Title: "Property 'photoUrls' is missing", + Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { name: "success - ignores unknown object attribute", @@ -351,6 +369,12 @@ func getValidationTests(t *testing.T) []*validationTest { bytes.NewBufferString(`{"name":"Bahama","photoUrls":[]}`)), }, }, + { + name: "success - required properties are not required on PATCH if required overlaid using allOf elsewhere", + args: validationArgs{ + r: newPetstoreRequest(t, http.MethodPatch, "/pet", bytes.NewBufferString(`{}`)), + }, + }, // // Path params @@ -429,9 +453,18 @@ func TestValidationHandler_validateRequest(t *testing.T) { pointer := toJSONPointer(innerErr.JSONPointer()) req.Equal(tt.wantErrSchemaPath, pointer) req.Equal(fmt.Sprintf("%v", tt.wantErrSchemaValue), fmt.Sprintf("%v", innerErr.Value)) + + if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok { + req.Equal(tt.wantErrSchemaOriginReason, originErr.Reason) + pointer := toJSONPointer(originErr.JSONPointer()) + req.Equal(tt.wantErrSchemaOriginPath, pointer) + req.Equal(fmt.Sprintf("%v", tt.wantErrSchemaOriginValue), fmt.Sprintf("%v", originErr.Value)) + } } else { req.False(tt.wantErrSchemaReason != "" || tt.wantErrSchemaPath != "", "error = %v, not a SchemaError -- %#v", e.Err, e.Err) + req.False(tt.wantErrSchemaOriginReason != "" || tt.wantErrSchemaOriginPath != "", + "error = %v, not a SchemaError with Origin -- %#v", e.Err, e.Err) } if innerErr, ok := e.Err.(*ParseError); ok { From 3fd16049f0f4d3ac3aed49ec8999675ba2f3163b Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Fri, 5 Jun 2020 00:59:20 -0700 Subject: [PATCH 002/260] feat: add Restish to README (#215) This links to [Restish](https://rest.sh/) in the README. The project supports OpenAPI out of the box via `kin-openapi`. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fd3db0fbe..1ff4ccb76 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Here's some projects that depend on _kin-openapi_: * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPI 3 spec * [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs" + * [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "...a CLI for interacting with REST-ish HTTP APIs with some nice features built-in" * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request) ## Alternative projects From ac200a167d49f5ce9158767bcbd54004b5283402 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 10 Jun 2020 12:11:48 +0200 Subject: [PATCH 003/260] Switch to Github Actions & test on Windows (#221) --- .github/workflows/go.yml | 35 ++++++++++++++++++++++ .travis.yml | 12 -------- README.md | 2 +- go.mod | 2 +- go.sum | 5 ++-- openapi3filter/validation_error_encoder.go | 2 +- openapi3filter/validation_handler.go | 9 ++---- 7 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/go.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 000000000..37e7d3825 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,35 @@ +name: go +on: + pull_request: + push: + +jobs: + build-and-test: + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GO111MODULE: 'on' + CGO_ENABLED: '0' + strategy: + fail-fast: true + matrix: + # Locked at https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on + os: + - ubuntu-18.04 + - windows-2019 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-go@v2 + with: + go-version: 1.x + - run: go version + + - run: go get ./... + - run: go test ./... + - run: go vet ./... + - run: go fmt ./... + - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + shell: bash + - run: go get -u -a -v ./... && go mod tidy && go mod verify + - run: git --no-pager diff diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5dd1e0251..000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: go -go: -- 1.11.x -env: - global: - - GO111MODULE: 'on' - - CGO_ENABLED: '0' -after_success: -- go mod tidy && git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] -notifications: - email: - on_success: never diff --git a/README.md b/README.md index 1ff4ccb76..fbd0f0873 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.com/getkin/kin-openapi.svg?branch=master)](https://travis-ci.com/getkin/kin-openapi) +[![CI](https://github.com/getkin/kin-openapi/workflows/go/badge.svg)](https://github.com/getkin/kin-openapi/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/getkin/kin-openapi)](https://goreportcard.com/report/github.com/getkin/kin-openapi) [![GoDoc](https://godoc.org/github.com/getkin/kin-openapi?status.svg)](https://godoc.org/github.com/getkin/kin-openapi) [![Join Gitter Chat Channel -](https://badges.gitter.im/getkin/kin.svg)](https://gitter.im/getkin/kin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/go.mod b/go.mod index 230138307..cdd681fc3 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,5 @@ go 1.14 require ( github.com/ghodss/yaml v1.0.0 github.com/stretchr/testify v1.5.1 - gopkg.in/yaml.v2 v2.2.8 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 22c1b575c..998d6cb72 100644 --- a/go.sum +++ b/go.sum @@ -7,7 +7,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 49459ef9c..6bde134a5 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -176,7 +176,7 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida (e.Parameter.Style == "" || e.Parameter.Style == "form") && strings.Contains(value, ",") { parts := strings.Split(value, ",") - cErr.Detail = cErr.Detail+"; "+ fmt.Sprintf("perhaps you intended '?%s=%s'", + cErr.Detail = cErr.Detail + "; " + fmt.Sprintf("perhaps you intended '?%s=%s'", e.Parameter.Name, strings.Join(parts, "&"+e.Parameter.Name+"=")) } } diff --git a/openapi3filter/validation_handler.go b/openapi3filter/validation_handler.go index f72ef1cec..f445fecaa 100644 --- a/openapi3filter/validation_handler.go +++ b/openapi3filter/validation_handler.go @@ -22,8 +22,7 @@ type ValidationHandler struct { func (h *ValidationHandler) Load() error { h.router = NewRouter() - err := h.router.AddSwaggerFromFile(h.SwaggerFile) - if err != nil { + if err := h.router.AddSwaggerFromFile(h.SwaggerFile); err != nil { return err } @@ -42,8 +41,7 @@ func (h *ValidationHandler) Load() error { } func (h *ValidationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - err := h.validateRequest(r) - if err != nil { + if err := h.validateRequest(r); err != nil { h.ErrorEncoder(r.Context(), err, w) return } @@ -69,8 +67,7 @@ func (h *ValidationHandler) validateRequest(r *http.Request) error { Route: route, Options: options, } - err = ValidateRequest(r.Context(), requestValidationInput) - if err != nil { + if err = ValidateRequest(r.Context(), requestValidationInput); err != nil { return err } From 2421d443a11015c4cf64688e918c684dfc3ff0df Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 15 Jun 2020 09:19:29 +0200 Subject: [PATCH 004/260] Responses Object MUST contain at least one response code (#226) --- openapi3/operation.go | 14 +- openapi3/response.go | 7 +- openapi3/response_issue224_test.go | 460 ++++++++++++++++++++++++ openapi3filter/req_resp_decoder_test.go | 6 +- openapi3filter/router_test.go | 22 +- openapi3filter/validation_test.go | 31 +- 6 files changed, 500 insertions(+), 40 deletions(-) create mode 100644 openapi3/response_issue224_test.go diff --git a/openapi3/operation.go b/openapi3/operation.go index 0841863be..5e367e0e8 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -71,14 +71,12 @@ func (operation *Operation) AddResponse(status int, response *Response) { responses = NewResponses() operation.Responses = responses } - if status == 0 { - responses["default"] = &ResponseRef{ - Value: response, - } - } else { - responses[strconv.FormatInt(int64(status), 10)] = &ResponseRef{ - Value: response, - } + code := "default" + if status != 0 { + code = strconv.FormatInt(int64(status), 10) + } + responses[code] = &ResponseRef{ + Value: response, } } diff --git a/openapi3/response.go b/openapi3/response.go index db83db71f..3e1a73227 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -12,7 +12,9 @@ import ( type Responses map[string]*ResponseRef func NewResponses() Responses { - return make(Responses, 8) + r := make(Responses) + r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")} + return r } func (responses Responses) Default() *ResponseRef { @@ -24,6 +26,9 @@ func (responses Responses) Get(status int) *ResponseRef { } func (responses Responses) Validate(c context.Context) error { + if len(responses) == 0 { + return errors.New("The Responses Object MUST contain at least one response code") + } for _, v := range responses { if err := v.Validate(c); err != nil { return err diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go new file mode 100644 index 000000000..6dae39d11 --- /dev/null +++ b/openapi3/response_issue224_test.go @@ -0,0 +1,460 @@ +package openapi3 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEmptyResponsesAreInvalid(t *testing.T) { + spec := `{ + "openapi": "3.0.0", + "servers": [ + { + "url": "http://petstore.swagger.io/v2" + } + ], + "info": { + "description": ":dog: :cat: :rabbit: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key to test the authorization filters.", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "responses": { + + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/Pet" + }, + "parameters": [] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "responses": { + + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/Pet" + }, + "parameters": [] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "name": { + "description": "Updated name of the pet", + "type": "string" + }, + "status": { + "description": "Updated status of the pet", + "type": "string" + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "parameters": [ + { + "name": "api_key", + "in": "header", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + } + }, + "externalDocs": { + "description": "See AsyncAPI example", + "url": "https://mermade.github.io/shins/asyncapi.html" + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "requestBodies": { + "Pet": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "description": "Pet object that needs to be added to the store", + "required": true + }, + "UserArray": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "description": "List of user object", + "required": true + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "links": {}, + "callbacks": {} + }, + "security": [] +} +` + + doc, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(context.Background()) + require.Error(t, err) +} diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 4a5cefc5e..1cf7be33e 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -924,7 +924,11 @@ func TestDecodeParameter(t *testing.T) { Version: "0.1", } spec := &openapi3.Swagger{OpenAPI: "3.0.0", Info: info} - op := &openapi3.Operation{OperationID: "test", Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, Responses: make(openapi3.Responses)} + op := &openapi3.Operation{ + OperationID: "test", + Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, + Responses: openapi3.NewResponses(), + } spec.AddOperation("/test"+path, http.MethodGet, op) router := NewRouter() require.NoError(t, router.AddSwagger(spec), "failed to create a router") diff --git a/openapi3filter/router_test.go b/openapi3filter/router_test.go index 4eba2c368..2865cb8b5 100644 --- a/openapi3filter/router_test.go +++ b/openapi3filter/router_test.go @@ -18,17 +18,17 @@ func TestRouter(t *testing.T) { ) // Build swagger - helloCONNECT := &openapi3.Operation{Responses: make(openapi3.Responses)} - helloDELETE := &openapi3.Operation{Responses: make(openapi3.Responses)} - helloGET := &openapi3.Operation{Responses: make(openapi3.Responses)} - helloHEAD := &openapi3.Operation{Responses: make(openapi3.Responses)} - helloOPTIONS := &openapi3.Operation{Responses: make(openapi3.Responses)} - helloPATCH := &openapi3.Operation{Responses: make(openapi3.Responses)} - helloPOST := &openapi3.Operation{Responses: make(openapi3.Responses)} - helloPUT := &openapi3.Operation{Responses: make(openapi3.Responses)} - helloTRACE := &openapi3.Operation{Responses: make(openapi3.Responses)} - paramsGET := &openapi3.Operation{Responses: make(openapi3.Responses)} - partialGET := &openapi3.Operation{Responses: make(openapi3.Responses)} + 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()} + partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()} swagger := &openapi3.Swagger{ OpenAPI: "3.0.0", Info: &openapi3.Info{ diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index 6ac05b331..c4e6e0f17 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -133,13 +133,13 @@ func TestFilter(t *testing.T) { }, }, }, - Responses: make(openapi3.Responses), + Responses: openapi3.NewResponses(), }, }, "/issue151": &openapi3.PathItem{ Get: &openapi3.Operation{ - Responses: make(openapi3.Responses), + Responses: openapi3.NewResponses(), }, Parameters: openapi3.Parameters{ { @@ -557,21 +557,16 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { for _, tc := range tc { var securityRequirements *openapi3.SecurityRequirements = nil if tc.schemes != nil { - tempS := make(openapi3.SecurityRequirements, 0) + tempS := openapi3.NewSecurityRequirements() for _, scheme := range *tc.schemes { - tempS = append( - tempS, - openapi3.SecurityRequirement{ - scheme.Name: {}, - }, - ) + tempS.With(openapi3.SecurityRequirement{scheme.Name: {}}) } - securityRequirements = &tempS + securityRequirements = tempS } swagger.Paths[tc.name] = &openapi3.PathItem{ Get: &openapi3.Operation{ Security: securityRequirements, - Responses: make(openapi3.Responses), + Responses: openapi3.NewResponses(), }, } } @@ -693,18 +688,16 @@ func TestAnySecurityRequirementMet(t *testing.T) { // Add the paths to the swagger for _, tc := range tc { // Create the security requirements from the test cases's schemes - securityRequirements := make(openapi3.SecurityRequirements, len(tc.schemes)) - for i, scheme := range tc.schemes { - securityRequirements[i] = openapi3.SecurityRequirement{ - scheme: {}, - } + securityRequirements := openapi3.NewSecurityRequirements() + for _, scheme := range tc.schemes { + securityRequirements.With(openapi3.SecurityRequirement{scheme: {}}) } // Create the path with the security requirements swagger.Paths[tc.name] = &openapi3.PathItem{ Get: &openapi3.Operation{ - Security: &securityRequirements, - Responses: make(openapi3.Responses), + Security: securityRequirements, + Responses: openapi3.NewResponses(), }, } } @@ -804,7 +797,7 @@ func TestAllSchemesMet(t *testing.T) { Security: &openapi3.SecurityRequirements{ securityRequirement, }, - Responses: make(openapi3.Responses), + Responses: openapi3.NewResponses(), }, } } From a78d0f6946ecee4e41b0af787d2c95dcceed0ac9 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 15 Jun 2020 12:14:32 +0200 Subject: [PATCH 005/260] validate path params against path (#227) --- openapi3/info.go | 11 ++- openapi3/operation.go | 2 +- openapi3/operation_test.go | 2 +- openapi3/parameter.go | 44 ++++------ openapi3/parameter_issue223_test.go | 116 ++++++++++++++++++++++++++ openapi3/paths.go | 72 +++++++++++----- openapi3/response.go | 4 +- openapi3/response_issue224_test.go | 2 +- openapi3/server.go | 4 +- openapi3/server_test.go | 2 +- openapi3/swagger.go | 69 ++++++++++----- openapi3/swagger_loader_paths_test.go | 2 +- openapi3/swagger_loader_test.go | 2 +- openapi3/swagger_test.go | 48 +++++------ openapi3filter/router_test.go | 5 ++ 15 files changed, 273 insertions(+), 112 deletions(-) create mode 100644 openapi3/parameter_issue223_test.go diff --git a/openapi3/info.go b/openapi3/info.go index 59e03cc13..25e675e66 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -3,7 +3,6 @@ package openapi3 import ( "context" "errors" - "fmt" "github.com/getkin/kin-openapi/jsoninfo" ) @@ -30,22 +29,22 @@ func (value *Info) UnmarshalJSON(data []byte) error { func (value *Info) Validate(c context.Context) error { if contact := value.Contact; contact != nil { if err := contact.Validate(c); err != nil { - return fmt.Errorf("Error when validating Contact: %s", err.Error()) + return err } } if license := value.License; license != nil { if err := license.Validate(c); err != nil { - return fmt.Errorf("Error when validating License: %s", err.Error()) + return err } } if value.Version == "" { - return errors.New("Variable 'version' must be a non-empty JSON string") + return errors.New("value of version must be a non-empty JSON string") } if value.Title == "" { - return errors.New("Variable 'title' must be a non-empty JSON string") + return errors.New("value of title must be a non-empty JSON string") } return nil @@ -88,7 +87,7 @@ func (value *License) UnmarshalJSON(data []byte) error { func (value *License) Validate(c context.Context) error { if value.Name == "" { - return errors.New("Variable 'name' must be a non-empty JSON string") + return errors.New("value of license name must be a non-empty JSON string") } return nil } diff --git a/openapi3/operation.go b/openapi3/operation.go index 5e367e0e8..9e64f031e 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -96,7 +96,7 @@ func (operation *Operation) Validate(c context.Context) error { return err } } else { - return errors.New("Variable 'Responses' must be a JSON object") + return errors.New("value of responses must be a JSON object") } return nil } diff --git a/openapi3/operation_test.go b/openapi3/operation_test.go index 22f325ec5..fe59c6031 100644 --- a/openapi3/operation_test.go +++ b/openapi3/operation_test.go @@ -53,7 +53,7 @@ func TestOperationValidation(t *testing.T) { { "when no Responses object is provided", operationWithoutResponses(), - errors.New("Variable 'Responses' must be a JSON object"), + errors.New("value of responses must be a JSON object"), }, { "when a Responses object is provided", diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 30ec868b6..1e2f55e17 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -27,22 +27,18 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter { } func (parameters Parameters) Validate(c context.Context) error { - m := make(map[string]struct{}) + dupes := make(map[string]struct{}) for _, item := range parameters { - if err := item.Validate(c); err != nil { - return err - } if v := item.Value; v != nil { - in := v.In - name := v.Name - key := in + ":" + name - if _, exists := m[key]; exists { - return fmt.Errorf("More than one '%s' parameter has name '%s'", in, name) - } - m[key] = struct{}{} - if err := item.Validate(c); err != nil { - return err + key := v.In + ":" + v.Name + if _, ok := dupes[key]; ok { + return fmt.Errorf("more than one %q parameter has name %q", v.In, v.Name) } + dupes[key] = struct{}{} + } + + if err := item.Validate(c); err != nil { + return err } } return nil @@ -163,7 +159,7 @@ func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error) func (parameter *Parameter) Validate(c context.Context) error { if parameter.Name == "" { - return errors.New("Parameter name can't be blank") + return errors.New("parameter name can't be blank") } in := parameter.In switch in { @@ -173,7 +169,7 @@ func (parameter *Parameter) Validate(c context.Context) error { ParameterInHeader, ParameterInCookie: default: - return fmt.Errorf("Parameter can't have 'in' value '%s'", parameter.In) + return fmt.Errorf("parameter can't have 'in' value %q", parameter.In) } // Validate a parameter's serialization method. @@ -206,26 +202,22 @@ func (parameter *Parameter) Validate(c context.Context) error { smSupported = true } if !smSupported { - e := fmt.Errorf("Serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in) - return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name, e) + e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in) + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) } - if parameter.Schema != nil && parameter.Content != nil { - return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name, - errors.New("Cannot contain both schema and content in a parameter")) - } - if parameter.Schema == nil && parameter.Content == nil { - return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name, - errors.New("A parameter MUST contain either a schema property, or a content property")) + if (parameter.Schema == nil) == (parameter.Content == nil) { + e := errors.New("parameter must contain exactly one of content and schema") + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) } if schema := parameter.Schema; schema != nil { if err := schema.Validate(c); err != nil { - return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name, err) + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err) } } if content := parameter.Content; content != nil { if err := content.Validate(c); err != nil { - return fmt.Errorf("Parameter content is invalid: %v", err) + return fmt.Errorf("parameter %q content is invalid: %v", parameter.Name, err) } } return nil diff --git a/openapi3/parameter_issue223_test.go b/openapi3/parameter_issue223_test.go new file mode 100644 index 000000000..ae1ddd2e9 --- /dev/null +++ b/openapi3/parameter_issue223_test.go @@ -0,0 +1,116 @@ +package openapi3 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPathParametersMatchPath(t *testing.T) { + spec := ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + # <------------------ no parameters + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string +` + + doc, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(context.Background()) + require.EqualError(t, err, `invalid paths: operation GET /pets/{petId} must define exactly all path parameters`) +} diff --git a/openapi3/paths.go b/openapi3/paths.go index d738e9dac..0bfeb2c79 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -12,14 +12,38 @@ type Paths map[string]*PathItem func (paths Paths) Validate(c context.Context) error { normalizedPaths := make(map[string]string) for path, pathItem := range paths { - normalizedPath := normalizePathKey(path) - if oldPath, exists := normalizedPaths[normalizedPath]; exists { - return fmt.Errorf("Conflicting paths '%v' and '%v'", path, oldPath) - } if path == "" || path[0] != '/' { - return fmt.Errorf("Path '%v' does not start with '/'", path) + return fmt.Errorf("path %q does not start with a forward slash (/)", path) + } + + normalizedPath, pathParamsCount := normalizeTemplatedPath(path) + if oldPath, ok := normalizedPaths[normalizedPath]; ok { + return fmt.Errorf("conflicting paths %q and %q", path, oldPath) } normalizedPaths[path] = path + + var globalCount uint + for _, parameterRef := range pathItem.Parameters { + if parameterRef != nil { + if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath { + globalCount++ + } + } + } + for method, operation := range pathItem.Operations() { + var count uint + for _, parameterRef := range operation.Parameters { + if parameterRef != nil { + if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath { + count++ + } + } + } + if count+globalCount != pathParamsCount { + return fmt.Errorf("operation %s %s must define exactly all path parameters", method, path) + } + } + if err := pathItem.Validate(c); err != nil { return err } @@ -46,37 +70,37 @@ func (paths Paths) Find(key string) *PathItem { return pathItem } - // Use normalized keys - normalizedSearchedPath := normalizePathKey(key) + normalizedPath, expected := normalizeTemplatedPath(key) for path, pathItem := range paths { - normalizedPath := normalizePathKey(path) - if normalizedPath == normalizedSearchedPath { + pathNormalized, got := normalizeTemplatedPath(path) + if got == expected && pathNormalized == normalizedPath { return pathItem } } return nil } -func normalizePathKey(key string) string { - // If the argument has no path variables, return the argument - if strings.IndexByte(key, '{') < 0 { - return key +func normalizeTemplatedPath(path string) (string, uint) { + if strings.IndexByte(path, '{') < 0 { + return path, 0 } - // Allocate buffer - buf := make([]byte, 0, len(key)) + var buf strings.Builder + buf.Grow(len(path)) - // Visit each byte - isVariable := false - for i := 0; i < len(key); i++ { - c := key[i] + var ( + cc rune + count uint + isVariable bool + ) + for i, c := range path { if isVariable { if c == '}' { // End path variables // First append possible '*' before this character // The character '}' will be appended - if i > 0 && key[i-1] == '*' { - buf = append(buf, '*') + if i > 0 && cc == '*' { + buf.WriteRune(cc) } isVariable = false } else { @@ -87,10 +111,12 @@ func normalizePathKey(key string) string { // Begin path variable // The character '{' will be appended isVariable = true + count++ } // Append the character - buf = append(buf, c) + buf.WriteRune(c) + cc = c } - return string(buf) + return buf.String(), count } diff --git a/openapi3/response.go b/openapi3/response.go index 3e1a73227..a89b28e2f 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -27,7 +27,7 @@ func (responses Responses) Get(status int) *ResponseRef { func (responses Responses) Validate(c context.Context) error { if len(responses) == 0 { - return errors.New("The Responses Object MUST contain at least one response code") + return errors.New("the responses object MUST contain at least one response code") } for _, v := range responses { if err := v.Validate(c); err != nil { @@ -80,7 +80,7 @@ func (response *Response) UnmarshalJSON(data []byte) error { func (response *Response) Validate(c context.Context) error { if response.Description == nil { - return errors.New("A short description of the response is required") + return errors.New("a short description of the response is required") } if content := response.Content; content != nil { diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go index 6dae39d11..2a175808e 100644 --- a/openapi3/response_issue224_test.go +++ b/openapi3/response_issue224_test.go @@ -456,5 +456,5 @@ func TestEmptyResponsesAreInvalid(t *testing.T) { doc, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(context.Background()) - require.Error(t, err) + require.EqualError(t, err, `invalid paths: the responses object MUST contain at least one response code`) } diff --git a/openapi3/server.go b/openapi3/server.go index 4392b09af..2594d2b30 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -114,7 +114,7 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) { func (server *Server) Validate(c context.Context) (err error) { if server.URL == "" { - return errors.New("Variable 'URL' must be a non-empty JSON string") + return errors.New("value of url must be a non-empty JSON string") } for _, v := range server.Variables { if err = v.Validate(c); err != nil { @@ -135,7 +135,7 @@ func (serverVariable *ServerVariable) Validate(c context.Context) error { switch serverVariable.Default.(type) { case float64, string: default: - return errors.New("Variable 'default' must be either JSON number or JSON string") + return errors.New("value of default must be either JSON number or JSON string") } for _, item := range serverVariable.Enum { switch item.(type) { diff --git a/openapi3/server_test.go b/openapi3/server_test.go index b877a3546..beafcaa63 100644 --- a/openapi3/server_test.go +++ b/openapi3/server_test.go @@ -70,7 +70,7 @@ func TestServerValidation(t *testing.T) { { "when no URL is provided", invalidServer(), - errors.New("Variable 'URL' must be a non-empty JSON string"), + errors.New("value of url must be a non-empty JSON string"), }, { "when a URL is provided", diff --git a/openapi3/swagger.go b/openapi3/swagger.go index 6ca8f1e02..06be8a343 100644 --- a/openapi3/swagger.go +++ b/openapi3/swagger.go @@ -11,12 +11,12 @@ import ( type Swagger struct { ExtensionProps OpenAPI string `json:"openapi" yaml:"openapi"` // Required - Info *Info `json:"info" yaml:"info"` // Required - Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` - Paths Paths `json:"paths" yaml:"paths"` // Required Components Components `json:"components,omitempty" yaml:"components,omitempty"` - Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + Info *Info `json:"info" yaml:"info"` // 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"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } @@ -48,34 +48,57 @@ func (swagger *Swagger) AddServer(server *Server) { func (swagger *Swagger) Validate(c context.Context) error { if swagger.OpenAPI == "" { - return errors.New("Variable 'openapi' must be a non-empty JSON string") + return errors.New("value of openapi must be a non-empty JSON string") } - if err := swagger.Components.Validate(c); err != nil { - return fmt.Errorf("Error when validating Components: %s", err.Error()) + + // NOTE: only mention info/components/paths/... key in this func's errors. + + { + wrap := func(e error) error { return fmt.Errorf("invalid components: %v", e) } + if err := swagger.Components.Validate(c); err != nil { + return wrap(err) + } } - if v := swagger.Security; v != nil { - if err := v.Validate(c); err != nil { - return fmt.Errorf("Error when validating Security: %s", err.Error()) + + { + wrap := func(e error) error { return fmt.Errorf("invalid info: %v", e) } + if v := swagger.Info; v != nil { + if err := v.Validate(c); err != nil { + return wrap(err) + } + } else { + return wrap(errors.New("must be a JSON object")) } } - if v := swagger.Servers; v != nil { - if err := v.Validate(c); err != nil { - return fmt.Errorf("Error when validating Servers: %s", err.Error()) + + { + wrap := func(e error) error { return fmt.Errorf("invalid paths: %v", e) } + if v := swagger.Paths; v != nil { + if err := v.Validate(c); err != nil { + return wrap(err) + } + } else { + return wrap(errors.New("must be a JSON object")) } } - if v := swagger.Paths; v != nil { - if err := v.Validate(c); err != nil { - return fmt.Errorf("Error when validating Paths: %s", err.Error()) + + { + wrap := func(e error) error { return fmt.Errorf("invalid security: %v", e) } + if v := swagger.Security; v != nil { + if err := v.Validate(c); err != nil { + return wrap(err) + } } - } else { - return errors.New("Variable 'paths' must be a JSON object") } - if v := swagger.Info; v != nil { - if err := v.Validate(c); err != nil { - return fmt.Errorf("Error when validating Info: %s", err.Error()) + + { + wrap := func(e error) error { return fmt.Errorf("invalid servers: %v", e) } + if v := swagger.Servers; v != nil { + if err := v.Validate(c); err != nil { + return wrap(err) + } } - } else { - return errors.New("Variable 'info' must be a JSON object") } + return nil } diff --git a/openapi3/swagger_loader_paths_test.go b/openapi3/swagger_loader_paths_test.go index e805d3ae3..9605b07a4 100644 --- a/openapi3/swagger_loader_paths_test.go +++ b/openapi3/swagger_loader_paths_test.go @@ -24,7 +24,7 @@ paths: ` for path, expectedErr := range map[string]string{ - "foo/bar": "Error when validating Paths: Path 'foo/bar' does not start with '/'", + "foo/bar": "invalid paths: path \"foo/bar\" does not start with a forward slash (/)", "/foo/bar": "", } { loader := openapi3.NewSwaggerLoader() diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go index 920911a17..960383260 100644 --- a/openapi3/swagger_loader_test.go +++ b/openapi3/swagger_loader_test.go @@ -97,7 +97,7 @@ func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) { doc, err := loader.LoadSwaggerFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) - require.EqualError(t, err, "Error when validating Paths: Found unresolved ref: ''") + require.EqualError(t, err, "invalid paths: Found unresolved ref: ''") } func TestResolveResponseExampleRef(t *testing.T) { diff --git a/openapi3/swagger_test.go b/openapi3/swagger_test.go index d4c433983..06c478c76 100644 --- a/openapi3/swagger_test.go +++ b/openapi3/swagger_test.go @@ -21,7 +21,7 @@ func TestRefsJSON(t *testing.T) { t.Log("Unmarshal *openapi3.Swagger from JSON") docA := &openapi3.Swagger{} - err = json.Unmarshal(specJSON, &docA) + err = json.Unmarshal([]byte(specJSON), &docA) require.NoError(t, err) require.NotEmpty(t, data) @@ -44,7 +44,7 @@ func TestRefsJSON(t *testing.T) { require.NoError(t, err) dataB, err := json.Marshal(docB) require.NoError(t, err) - require.JSONEq(t, string(data), string(specJSON)) + require.JSONEq(t, string(data), specJSON) require.JSONEq(t, string(data), string(dataA)) require.JSONEq(t, string(data), string(dataB)) } @@ -59,7 +59,7 @@ func TestRefsYAML(t *testing.T) { t.Log("Unmarshal *openapi3.Swagger from YAML") docA := &openapi3.Swagger{} - err = yaml.Unmarshal(specYAML, &docA) + err = yaml.Unmarshal([]byte(specYAML), &docA) require.NoError(t, err) require.NotEmpty(t, data) @@ -82,7 +82,7 @@ func TestRefsYAML(t *testing.T) { require.NoError(t, err) dataB, err := yaml.Marshal(docB) require.NoError(t, err) - eqYAML(t, data, specYAML) + eqYAML(t, data, []byte(specYAML)) eqYAML(t, data, dataA) eqYAML(t, data, dataB) } @@ -96,7 +96,7 @@ func eqYAML(t *testing.T, expected, actual []byte) { require.Equal(t, e, a) } -var specYAML = []byte(` +var specYAML = ` openapi: '3.0' info: title: MyAPI @@ -148,9 +148,9 @@ components: name: token someSecurityScheme: "$ref": "#/components/securitySchemes/otherSecurityScheme" -`) +` -var specJSON = []byte(` +var specJSON = ` { "openapi": "3.0", "info": { @@ -236,7 +236,7 @@ var specJSON = []byte(` } } } -`) +` func spec() *openapi3.Swagger { parameter := &openapi3.Parameter{ @@ -351,12 +351,12 @@ func spec() *openapi3.Swagger { func TestValidation(t *testing.T) { tests := []struct { name string - input []byte + input string expectedError error }{ { "when no OpenAPI property is supplied", - []byte(` + ` info: title: "Hello World REST APIs" version: "1.0" @@ -380,12 +380,12 @@ paths: responses: 200: description: "Get a single greeting object" -`), - errors.New("Variable 'openapi' must be a non-empty JSON string"), +`, + errors.New("value of openapi must be a non-empty JSON string"), }, { "when an empty OpenAPI property is supplied", - []byte(` + ` openapi: '' info: title: "Hello World REST APIs" @@ -410,12 +410,12 @@ paths: responses: 200: description: "Get a single greeting object" -`), - errors.New("Variable 'openapi' must be a non-empty JSON string"), +`, + errors.New("value of openapi must be a non-empty JSON string"), }, { "when the Info property is not supplied", - []byte(` + ` openapi: '1.0' paths: "/api/v2/greetings.json": @@ -437,22 +437,22 @@ paths: responses: 200: description: "Get a single greeting object" -`), - errors.New("Variable 'info' must be a JSON object"), +`, + errors.New("invalid info: must be a JSON object"), }, { "when the Paths property is not supplied", - []byte(` + ` openapi: '1.0' info: title: "Hello World REST APIs" version: "1.0" -`), - errors.New("Variable 'paths' must be a JSON object"), +`, + errors.New("invalid paths: must be a JSON object"), }, { "when a valid spec is supplied", - []byte(` + ` openapi: 3.0.2 info: title: "Hello World REST APIs" @@ -490,7 +490,7 @@ components: properties: description: type: string -`), +`, nil, }, } @@ -498,7 +498,7 @@ components: for _, test := range tests { t.Run(test.name, func(t *testing.T) { doc := &openapi3.Swagger{} - err := yaml.Unmarshal(test.input, &doc) + err := yaml.Unmarshal([]byte(test.input), &doc) require.NoError(t, err) c := context.Background() diff --git a/openapi3filter/router_test.go b/openapi3filter/router_test.go index 2865cb8b5..5a7241145 100644 --- a/openapi3filter/router_test.go +++ b/openapi3filter/router_test.go @@ -52,6 +52,11 @@ func TestRouter(t *testing.T) { }, "/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")}, + }, }, "/partial": &openapi3.PathItem{ Get: partialGET, From 26e9a3787488e3608f86913af5fdee929632e3e5 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 15 Jun 2020 13:07:01 +0200 Subject: [PATCH 006/260] Confluentinc middleware (#228) Co-authored-by: Cody A. Ray --- openapi3filter/validation_error_test.go | 62 +++++++++++++++++++++++-- openapi3filter/validation_handler.go | 22 ++++++++- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 420831dad..737ceeb9d 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -547,7 +547,7 @@ func (e *mockErrorEncoder) Encode(ctx context.Context, err error, w http.Respons e.W = w } -func runTest(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response { +func runTest_ServeHTTP(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response { h := &ValidationHandler{ Handler: handler, ErrorEncoder: encoder, @@ -560,6 +560,18 @@ func runTest(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http return w.Result() } +func runTest_Middleware(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response { + h := &ValidationHandler{ + ErrorEncoder: encoder, + SwaggerFile: "fixtures/petstore.json", + } + err := h.Load() + require.NoError(t, err) + w := httptest.NewRecorder() + h.Middleware(handler).ServeHTTP(w, req) + return w.Result() +} + func TestValidationHandler_ServeHTTP(t *testing.T) { t.Run("errors on invalid requests", func(t *testing.T) { httpCtx := context.WithValue(context.Background(), "pig", "tails") @@ -569,7 +581,49 @@ func TestValidationHandler_ServeHTTP(t *testing.T) { handler := &testHandler{} encoder := &mockErrorEncoder{} - runTest(t, handler, encoder.Encode, r) + runTest_ServeHTTP(t, handler, encoder.Encode, r) + + require.False(t, handler.Called) + require.True(t, encoder.Called) + require.Equal(t, httpCtx, encoder.Ctx) + require.NotNil(t, encoder.Err) + }) + + t.Run("passes valid requests through", func(t *testing.T) { + r := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil) + + handler := &testHandler{} + encoder := &mockErrorEncoder{} + runTest_ServeHTTP(t, handler, encoder.Encode, r) + + require.True(t, handler.Called) + require.False(t, encoder.Called) + }) + + t.Run("uses error encoder", func(t *testing.T) { + r := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)) + + handler := &testHandler{} + encoder := &ValidationErrorEncoder{Encoder: (ErrorEncoder)(DefaultErrorEncoder)} + resp := runTest_ServeHTTP(t, handler, encoder.Encode, r) + + body, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) + require.Equal(t, "[422][][] Field must be set to array or not be present [source pointer=/photoUrls]", string(body)) + }) +} + +func TestValidationHandler_Middleware(t *testing.T) { + t.Run("errors on invalid requests", func(t *testing.T) { + httpCtx := context.WithValue(context.Background(), "pig", "tails") + r, err := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil) + require.NoError(t, err) + r = r.WithContext(httpCtx) + + handler := &testHandler{} + encoder := &mockErrorEncoder{} + runTest_Middleware(t, handler, encoder.Encode, r) require.False(t, handler.Called) require.True(t, encoder.Called) @@ -582,7 +636,7 @@ func TestValidationHandler_ServeHTTP(t *testing.T) { handler := &testHandler{} encoder := &mockErrorEncoder{} - runTest(t, handler, encoder.Encode, r) + runTest_Middleware(t, handler, encoder.Encode, r) require.True(t, handler.Called) require.False(t, encoder.Called) @@ -593,7 +647,7 @@ func TestValidationHandler_ServeHTTP(t *testing.T) { handler := &testHandler{} encoder := &ValidationErrorEncoder{Encoder: (ErrorEncoder)(DefaultErrorEncoder)} - resp := runTest(t, handler, encoder.Encode, r) + resp := runTest_Middleware(t, handler, encoder.Encode, r) body, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) diff --git a/openapi3filter/validation_handler.go b/openapi3filter/validation_handler.go index f445fecaa..336187f49 100644 --- a/openapi3filter/validation_handler.go +++ b/openapi3filter/validation_handler.go @@ -41,14 +41,32 @@ func (h *ValidationHandler) Load() error { } func (h *ValidationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if err := h.validateRequest(r); err != nil { - h.ErrorEncoder(r.Context(), err, w) + if handled := h.before(w, r); handled { return } // TODO: validateResponse h.Handler.ServeHTTP(w, r) } +// Middleware implements gorilla/mux MiddlewareFunc +func (h *ValidationHandler) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if handled := h.before(w, r); handled { + return + } + // TODO: validateResponse + next.ServeHTTP(w, r) + }) +} + +func (h *ValidationHandler) before(w http.ResponseWriter, r *http.Request) (handled bool) { + if err := h.validateRequest(r); err != nil { + h.ErrorEncoder(r.Context(), err, w) + return true + } + return false +} + func (h *ValidationHandler) validateRequest(r *http.Request) error { // Find route route, pathParams, err := h.router.FindRoute(r.Method, r.URL) From 52172527b486de59b22791ad5404712897551d70 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 15 Jun 2020 14:54:24 +0200 Subject: [PATCH 007/260] handle the main ref dereferencing issues (#229) Signed-off-by: Pierre Fenoll --- openapi3/refs_test.go | 111 +++++++++++++++++++++++++++++++++++++ openapi3/swagger_loader.go | 46 +++++++++------ 2 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 openapi3/refs_test.go diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go new file mode 100644 index 000000000..ed12b894d --- /dev/null +++ b/openapi3/refs_test.go @@ -0,0 +1,111 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue222(t *testing.T) { + spec := ` +openapi: 3.0.0 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: 'http://petstore.swagger.io/v1' +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': # <--------------- PANIC HERE + + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '/pets/{petId}': + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string +` + + _, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + require.EqualError(t, err, `invalid response: value MUST be a JSON object`) +} diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 3925e0e6c..83767340b 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -355,15 +355,16 @@ func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref stri } func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, path *url.URL) error { - // Prevent infinite recursion visited := swaggerLoader.visited if _, isVisited := visited[component]; isVisited { return nil } visited[component] = struct{}{} - // Resolve ref const prefix = "#/components/headers/" + if component == nil { + return errors.New("invalid header: value MUST be a JSON object") + } if ref := component.Ref; len(ref) > 0 { if isSingleRefElement(ref) { var header Header @@ -400,15 +401,16 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component } func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) error { - // Prevent infinite recursion visited := swaggerLoader.visited if _, isVisited := visited[component]; isVisited { return nil } visited[component] = struct{}{} - // Resolve ref const prefix = "#/components/parameters/" + if component == nil { + return errors.New("invalid parameter: value MUST be a JSON object") + } ref := component.Ref if len(ref) > 0 { if isSingleRefElement(ref) { @@ -461,15 +463,16 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon } func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, path *url.URL) error { - // Prevent infinite recursion visited := swaggerLoader.visited if _, isVisited := visited[component]; isVisited { return nil } visited[component] = struct{}{} - // Resolve ref const prefix = "#/components/requestBodies/" + if component == nil { + return errors.New("invalid requestBody: value MUST be a JSON object") + } if ref := component.Ref; len(ref) > 0 { if isSingleRefElement(ref) { var requestBody RequestBody @@ -514,16 +517,17 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp } func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) error { - // Prevent infinite recursion visited := swaggerLoader.visited if _, isVisited := visited[component]; isVisited { return nil } visited[component] = struct{}{} - // Resolve ref - ref := component.Ref const prefix = "#/components/responses/" + if component == nil { + return errors.New("invalid response: value MUST be a JSON object") + } + ref := component.Ref if len(ref) > 0 { if isSingleRefElement(ref) { @@ -588,15 +592,16 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone } func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) error { - // Prevent infinite recursion visited := swaggerLoader.visited if _, isVisited := visited[component]; isVisited { return nil } visited[component] = struct{}{} - // Resolve ref const prefix = "#/components/schemas/" + if component == nil { + return errors.New("invalid schema: value MUST be a JSON object") + } ref := component.Ref if len(ref) > 0 { if isSingleRefElement(ref) { @@ -673,15 +678,16 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component } func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, path *url.URL) error { - // Prevent infinite recursion visited := swaggerLoader.visited if _, isVisited := visited[component]; isVisited { return nil } visited[component] = struct{}{} - // Resolve ref const prefix = "#/components/securitySchemes/" + if component == nil { + return errors.New("invalid securityScheme: value MUST be a JSON object") + } if ref := component.Ref; len(ref) > 0 { if isSingleRefElement(ref) { var scheme SecurityScheme @@ -709,7 +715,6 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c } func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, path *url.URL) error { - // Prevent infinite recursion visited := swaggerLoader.visited if _, isVisited := visited[component]; isVisited { return nil @@ -717,6 +722,9 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen visited[component] = struct{}{} const prefix = "#/components/examples/" + if component == nil { + return errors.New("invalid example: value MUST be a JSON object") + } if ref := component.Ref; len(ref) > 0 { if isSingleRefElement(ref) { var example Example @@ -744,7 +752,6 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen } func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, path *url.URL) error { - // Prevent infinite recursion visited := swaggerLoader.visited if _, isVisited := visited[component]; isVisited { return nil @@ -752,6 +759,9 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * visited[component] = struct{}{} const prefix = "#/components/links/" + if component == nil { + return errors.New("invalid link: value MUST be a JSON object") + } if ref := component.Ref; len(ref) > 0 { if isSingleRefElement(ref) { var link Link @@ -779,7 +789,6 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * } func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) { - // Prevent infinite recursion visited := swaggerLoader.visitedFiles key := "_" if documentPath != nil { @@ -791,6 +800,10 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo } visited[key] = struct{}{} + const prefix = "#/paths/" + if pathItem == nil { + return errors.New("invalid path item: value MUST be a JSON object") + } ref := pathItem.Ref if ref != "" { if isSingleRefElement(ref) { @@ -804,7 +817,6 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo return } - prefix := "#/paths/" if !strings.HasPrefix(ref, prefix) { err = fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref) return From d5e00c2b17495390e499e4deef36a23dda3284d2 Mon Sep 17 00:00:00 2001 From: FrancisLennon17 Date: Wed, 17 Jun 2020 14:01:24 +0100 Subject: [PATCH 008/260] Swagger2 Extensions (#225) Co-authored-by: Francis Lennon --- openapi2/openapi2.go | 55 ++++++++++++ openapi2conv/openapi2_conv.go | 130 +++++++++++++++++++++-------- openapi2conv/openapi2_conv_test.go | 20 ++++- 3 files changed, 166 insertions(+), 39 deletions(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 3da171ddd..f6b113fa1 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -11,10 +11,12 @@ import ( "fmt" "net/http" + "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" ) type Swagger struct { + openapi3.ExtensionProps Info openapi3.Info `json:"info"` ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"` Schemes []string `json:"schemes,omitempty"` @@ -29,6 +31,14 @@ type Swagger struct { Tags openapi3.Tags `json:"tags,omitempty"` } +func (swagger *Swagger) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(swagger) +} + +func (swagger *Swagger) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, swagger) +} + func (swagger *Swagger) AddOperation(path string, method string, operation *Operation) { paths := swagger.Paths if paths == nil { @@ -44,6 +54,7 @@ func (swagger *Swagger) AddOperation(path string, method string, operation *Oper } type PathItem struct { + openapi3.ExtensionProps Ref string `json:"$ref,omitempty"` Delete *Operation `json:"delete,omitempty"` Get *Operation `json:"get,omitempty"` @@ -55,6 +66,14 @@ type PathItem struct { Parameters Parameters `json:"parameters,omitempty"` } +func (pathItem *PathItem) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(pathItem) +} + +func (pathItem *PathItem) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, pathItem) +} + func (pathItem *PathItem) Operations() map[string]*Operation { operations := make(map[string]*Operation, 8) if v := pathItem.Delete; v != nil { @@ -124,6 +143,7 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { } type Operation struct { + openapi3.ExtensionProps Summary string `json:"summary,omitempty"` Description string `json:"description,omitempty"` ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"` @@ -136,9 +156,18 @@ type Operation struct { Security *SecurityRequirements `json:"security,omitempty"` } +func (operation *Operation) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(operation) +} + +func (operation *Operation) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, operation) +} + type Parameters []*Parameter type Parameter struct { + openapi3.ExtensionProps Ref string `json:"$ref,omitempty"` In string `json:"in,omitempty"` Name string `json:"name,omitempty"` @@ -162,7 +191,16 @@ type Parameter struct { Default interface{} `json:"default,omitempty"` } +func (parameter *Parameter) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(parameter) +} + +func (parameter *Parameter) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, parameter) +} + type Response struct { + openapi3.ExtensionProps Ref string `json:"$ref,omitempty"` Description string `json:"description,omitempty"` Schema *openapi3.SchemaRef `json:"schema,omitempty"` @@ -170,6 +208,14 @@ type Response struct { Examples map[string]interface{} `json:"examples,omitempty"` } +func (response *Response) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(response) +} + +func (response *Response) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, response) +} + type Header struct { Ref string `json:"$ref,omitempty"` Description string `json:"description,omitempty"` @@ -179,6 +225,7 @@ type Header struct { type SecurityRequirements []map[string][]string type SecurityScheme struct { + openapi3.ExtensionProps Ref string `json:"$ref,omitempty"` Description string `json:"description,omitempty"` Type string `json:"type,omitempty"` @@ -190,3 +237,11 @@ type SecurityScheme struct { Scopes map[string]string `json:"scopes,omitempty"` Tags openapi3.Tags `json:"tags,omitempty"` } + +func (securityScheme *SecurityScheme) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(securityScheme) +} + +func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, securityScheme) +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 6e0b10e7e..14a77a1c9 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -13,11 +13,14 @@ import ( // ToV3Swagger converts an OpenAPIv2 spec to an OpenAPIv3 spec func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { + stripNonCustomExtensions(swagger.Extensions) + result := &openapi3.Swagger{ - OpenAPI: "3.0.2", - Info: &swagger.Info, - Components: openapi3.Components{}, - Tags: swagger.Tags, + OpenAPI: "3.0.2", + Info: &swagger.Info, + Components: openapi3.Components{}, + Tags: swagger.Tags, + ExtensionProps: swagger.ExtensionProps, } host := swagger.Host if len(host) > 0 { @@ -93,7 +96,10 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { } func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*openapi3.PathItem, error) { - result := &openapi3.PathItem{} + stripNonCustomExtensions(pathItem.Extensions) + result := &openapi3.PathItem{ + ExtensionProps: pathItem.ExtensionProps, + } for method, operation := range pathItem.Operations() { resultOperation, err := ToV3Operation(swagger, pathItem, operation) if err != nil { @@ -118,11 +124,13 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera if operation == nil { return nil, nil } + stripNonCustomExtensions(operation.Extensions) result := &openapi3.Operation{ - OperationID: operation.OperationID, - Summary: operation.Summary, - Description: operation.Description, - Tags: operation.Tags, + OperationID: operation.OperationID, + Summary: operation.Summary, + Description: operation.Description, + Tags: operation.Tags, + ExtensionProps: operation.ExtensionProps, } if v := operation.Security; v != nil { resultSecurity := ToV3SecurityRequirements(*v) @@ -162,11 +170,13 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open Ref: ToV3Ref(ref), }, nil, nil } + stripNonCustomExtensions(parameter.Extensions) in := parameter.In if in == "body" { result := &openapi3.RequestBody{ - Description: parameter.Description, - Required: parameter.Required, + Description: parameter.Description, + Required: parameter.Required, + ExtensionProps: parameter.ExtensionProps, } if schemaRef := parameter.Schema; schemaRef != nil { // Assume it's JSON @@ -177,10 +187,11 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open }, nil } result := &openapi3.Parameter{ - In: in, - Name: parameter.Name, - Description: parameter.Description, - Required: parameter.Required, + In: in, + Name: parameter.Name, + Description: parameter.Description, + Required: parameter.Required, + ExtensionProps: parameter.ExtensionProps, } if parameter.Type != "" { @@ -214,8 +225,10 @@ func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) { Ref: ToV3Ref(ref), }, nil } + stripNonCustomExtensions(response.Extensions) result := &openapi3.Response{ - Description: &response.Description, + Description: &response.Description, + ExtensionProps: response.ExtensionProps, } if schemaRef := response.Schema; schemaRef != nil { result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef)) @@ -293,8 +306,10 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu if securityScheme == nil { return nil, nil } + stripNonCustomExtensions(securityScheme.Extensions) result := &openapi3.SecurityScheme{ - Description: securityScheme.Description, + Description: securityScheme.Description, + ExtensionProps: securityScheme.ExtensionProps, } switch securityScheme.Type { case "basic": @@ -339,12 +354,16 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { if err != nil { return nil, err } + stripNonCustomExtensions(swagger.Extensions) + result := &openapi2.Swagger{ - Info: *swagger.Info, - Definitions: FromV3Schemas(swagger.Components.Schemas), - Responses: resultResponses, - Tags: swagger.Tags, + Info: *swagger.Info, + Definitions: FromV3Schemas(swagger.Components.Schemas), + Responses: resultResponses, + Tags: swagger.Tags, + ExtensionProps: swagger.ExtensionProps, } + isHTTPS := false isHTTP := false servers := swagger.Servers @@ -375,6 +394,8 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { continue } result.AddOperation(path, "GET", nil) + stripNonCustomExtensions(pathItem.Extensions) + addPathExtensions(result, path, pathItem.ExtensionProps) for method, operation := range pathItem.Operations() { if operation == nil { continue @@ -457,7 +478,10 @@ func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) open } func FromV3PathItem(swagger *openapi3.Swagger, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) { - result := &openapi2.PathItem{} + stripNonCustomExtensions(pathItem.Extensions) + result := &openapi2.PathItem{ + ExtensionProps: pathItem.ExtensionProps, + } for method, operation := range pathItem.Operations() { r, err := FromV3Operation(swagger, operation) if err != nil { @@ -493,11 +517,13 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( if operation == nil { return nil, nil } + stripNonCustomExtensions(operation.Extensions) result := &openapi2.Operation{ - OperationID: operation.OperationID, - Summary: operation.Summary, - Description: operation.Description, - Tags: operation.Tags, + OperationID: operation.OperationID, + Summary: operation.Summary, + Description: operation.Description, + Tags: operation.Tags, + ExtensionProps: operation.ExtensionProps, } if v := operation.Security; v != nil { resultSecurity := FromV3SecurityRequirements(*v) @@ -542,11 +568,13 @@ func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation, if name == "" { return nil, errors.New("Could not find a name for request body") } + stripNonCustomExtensions(requestBody.Extensions) result := &openapi2.Parameter{ - In: "body", - Name: name, - Description: requestBody.Description, - Required: requestBody.Required, + In: "body", + Name: name, + Description: requestBody.Description, + Required: requestBody.Required, + ExtensionProps: requestBody.ExtensionProps, } // Add JSON schema @@ -567,11 +595,13 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) { if parameter == nil { return nil, nil } + stripNonCustomExtensions(parameter.Extensions) result := &openapi2.Parameter{ - Description: parameter.Description, - In: parameter.In, - Name: parameter.Name, - Required: parameter.Required, + Description: parameter.Description, + In: parameter.In, + Name: parameter.Name, + Required: parameter.Required, + ExtensionProps: parameter.ExtensionProps, } if schemaRef := parameter.Schema; schemaRef != nil { schemaRef = FromV3SchemaRef(schemaRef) @@ -621,8 +651,10 @@ func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) { if desc := response.Description; desc != nil { description = *desc } + stripNonCustomExtensions(response.Extensions) result := &openapi2.Response{ - Description: description, + Description: description, + ExtensionProps: response.ExtensionProps, } if content := response.Content; content != nil { if ct := content["application/json"]; ct != nil { @@ -637,9 +669,11 @@ func FromV3SecurityScheme(swagger *openapi3.Swagger, ref *openapi3.SecuritySchem if securityScheme == nil { return nil, nil } + stripNonCustomExtensions(securityScheme.Extensions) result := &openapi2.SecurityScheme{ - Ref: FromV3Ref(ref.Ref), - Description: securityScheme.Description, + Ref: FromV3Ref(ref.Ref), + Description: securityScheme.Description, + ExtensionProps: securityScheme.ExtensionProps, } switch securityScheme.Type { case "http": @@ -684,3 +718,25 @@ var attemptedBodyParameterNames = []string{ "body", "requestBody", } + +func stripNonCustomExtensions(extensions map[string]interface{}) { + for extName, _ := range extensions { + if !strings.HasPrefix(extName, "x-") { + delete(extensions, extName) + } + } +} + +func addPathExtensions(swagger *openapi2.Swagger, path string, extensionProps openapi3.ExtensionProps) { + paths := swagger.Paths + if paths == nil { + paths = make(map[string]*openapi2.PathItem, 8) + swagger.Paths = paths + } + pathItem := paths[path] + if pathItem == nil { + pathItem = &openapi2.PathItem{} + paths[path] = pathItem + } + pathItem.ExtensionProps = extensionProps +} diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index 2eeefecb3..d0e659bd8 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -35,7 +35,9 @@ func TestConvOpenAPIV2ToV3(t *testing.T) { const exampleV2 = ` { - "info": {"title":"MyAPI","version":"0.1"}, + "x-root": "root extension 1", + "x-root2": "root extension 2", + "info": {"title":"MyAPI","version":"0.1","x-info":"info extension"}, "schemes": ["https"], "host": "test.example.com", "basePath": "/v2", @@ -60,7 +62,10 @@ const exampleV2 = ` ] }, "/example": { + "x-path": "path extension 1", + "x-path2": "path extension 2", "delete": { + "x-operation": "operation extension 1", "description": "example delete", "responses": { "default": { @@ -83,6 +88,7 @@ const exampleV2 = ` ], "parameters": [ { + "x-parameter": "parameter extension 1", "in": "query", "name": "x" }, @@ -108,6 +114,7 @@ const exampleV2 = ` }, { "in": "body", + "x-requestBody": "requestbody extension 1", "name": "body", "schema": {} } @@ -123,6 +130,7 @@ const exampleV2 = ` } }, "default": { + "x-response": "response extension 1", "description": "default response" }, "404": { @@ -217,8 +225,10 @@ const exampleV2 = ` const exampleV3 = ` { + "x-root": "root extension 1", + "x-root2": "root extension 2", "openapi": "3.0.2", - "info": {"title":"MyAPI","version":"0.1"}, + "info": {"title":"MyAPI","version":"0.1","x-info":"info extension"}, "components": { "responses": { "ForbiddenError": { @@ -297,7 +307,10 @@ const exampleV3 = ` ] }, "/example": { + "x-path": "path extension 1", + "x-path2": "path extension 2", "delete": { + "x-operation": "operation extension 1", "description": "example delete", "responses": { "default": { @@ -320,6 +333,7 @@ const exampleV3 = ` ], "parameters": [ { + "x-parameter": "parameter extension 1", "in": "query", "name": "x" }, @@ -349,6 +363,7 @@ const exampleV3 = ` } ], "requestBody": { + "x-requestBody": "requestbody extension 1", "content": { "application/json": { "schema": {} @@ -370,6 +385,7 @@ const exampleV3 = ` } }, "default": { + "x-response": "response extension 1", "description": "default response" }, "404": { From f094a75ed4ddfc55772f40fdcac845930bf4abdc Mon Sep 17 00:00:00 2001 From: gitforbit <44337839+gitforbit@users.noreply.github.com> Date: Tue, 7 Jul 2020 10:51:41 +0200 Subject: [PATCH 009/260] openapi2_conv: include ExternalDocs prop when converting to v3 (#232) --- openapi2conv/openapi2_conv.go | 2 ++ openapi2conv/openapi2_conv_test.go | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 14a77a1c9..565436698 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -21,6 +21,7 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { Components: openapi3.Components{}, Tags: swagger.Tags, ExtensionProps: swagger.ExtensionProps, + ExternalDocs: swagger.ExternalDocs, } host := swagger.Host if len(host) > 0 { @@ -362,6 +363,7 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { Responses: resultResponses, Tags: swagger.Tags, ExtensionProps: swagger.ExtensionProps, + ExternalDocs: swagger.ExternalDocs, } isHTTPS := false diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index d0e659bd8..536a56927 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -41,6 +41,10 @@ const exampleV2 = ` "schemes": ["https"], "host": "test.example.com", "basePath": "/v2", + "externalDocs": { + "url": "https://example/doc/", + "description": "Example Documentation" + }, "tags": [ { "name": "Example", @@ -229,6 +233,10 @@ const exampleV3 = ` "x-root2": "root extension 2", "openapi": "3.0.2", "info": {"title":"MyAPI","version":"0.1","x-info":"info extension"}, + "externalDocs": { + "url": "https://example/doc/", + "description": "Example Documentation" + }, "components": { "responses": { "ForbiddenError": { From 45811417b788af82062146a866853aabc878c6e7 Mon Sep 17 00:00:00 2001 From: Dudko Alexey Date: Wed, 15 Jul 2020 09:48:06 +0200 Subject: [PATCH 010/260] added a support for absolute paths in $ref (#234) --- openapi3/swagger_loader.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 83767340b..cb0cda438 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -240,6 +240,10 @@ func join(basePath *url.URL, relativePath *url.URL) (*url.URL, error) { func resolvePath(basePath *url.URL, componentPath *url.URL) (*url.URL, error) { if componentPath.Scheme == "" && componentPath.Host == "" { + // support absolute paths + if componentPath.Path[0] == '/' { + return componentPath, nil + } return join(basePath, componentPath) } return componentPath, nil From 100b968d4813c01f576526d09ef34b0c828cc0cf Mon Sep 17 00:00:00 2001 From: gitforbit Date: Wed, 15 Jul 2020 18:42:14 +0200 Subject: [PATCH 011/260] conversion: conversion for file upload from v2 to v3 and vice versa (#233) This change enables converting spec for file upload with POST from v2 to v3 and vice versa according to https://swagger.io/docs/specification/2-0/file-upload/ specification. Added support for `multipart/form-data`. Added `in: formData` case for file upload. Updated unit tests related sections. --- openapi2conv/openapi2_conv.go | 118 +++++++++++++++++++++++++---- openapi2conv/openapi2_conv_test.go | 40 +++++++++- openapi3/content.go | 12 +++ openapi3/request_body.go | 10 +++ 4 files changed, 165 insertions(+), 15 deletions(-) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 565436698..dada63704 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -121,6 +121,51 @@ func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*open return result, nil } +func ToV3RequestBodyFormData(parameters []*openapi2.Parameter) *openapi3.RequestBodyRef { + if len(parameters) == 0 || parameters[0].In != "formData" { + return nil + } + schema := &openapi3.Schema{ + Type: "object", + Properties: make(map[string]*openapi3.SchemaRef, len(parameters)), + } + for _, parameter := range parameters { + if parameter.In != "formData" || parameter.Name == "" { + continue + } + format := parameter.Format + typ := parameter.Type + if parameter.Type == "file" { + format = "binary" + typ = "string" + } + pschema := &openapi3.Schema{ + Description: parameter.Description, + Type: typ, + ExtensionProps: parameter.ExtensionProps, + Format: format, + Enum: parameter.Enum, + Min: parameter.Minimum, + Max: parameter.Maximum, + ExclusiveMin: parameter.ExclusiveMin, + ExclusiveMax: parameter.ExclusiveMax, + MinLength: parameter.MinLength, + MaxLength: parameter.MaxLength, + Default: parameter.Default, + Items: parameter.Items, + MinItems: parameter.MinItems, + MaxItems: parameter.MaxItems, + } + schemaRef := openapi3.SchemaRef{ + Value: pschema, + } + schema.Properties[parameter.Name] = &schemaRef + } + return &openapi3.RequestBodyRef{ + Value: openapi3.NewRequestBody().WithFormDataSchema(schema), + } +} + func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, operation *openapi2.Operation) (*openapi3.Operation, error) { if operation == nil { return nil, nil @@ -137,15 +182,21 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera resultSecurity := ToV3SecurityRequirements(*v) result.Security = &resultSecurity } - for _, parameter := range operation.Parameters { - v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) - if err != nil { - return nil, err - } - if v3RequestBody != nil { - result.RequestBody = v3RequestBody - } else if v3Parameter != nil { - result.Parameters = append(result.Parameters, v3Parameter) + + requestBodyRef := ToV3RequestBodyFormData(operation.Parameters) + if requestBodyRef != nil { + result.RequestBody = requestBodyRef + } else { + for _, parameter := range operation.Parameters { + v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) + if err != nil { + return nil, err + } + if v3RequestBody != nil { + result.RequestBody = v3RequestBody + } else if v3Parameter != nil { + result.Parameters = append(result.Parameters, v3Parameter) + } } } if responses := operation.Responses; responses != nil { @@ -515,6 +566,42 @@ nameSearch: return "" } +func FromV3RequestBodyFormData(requestBodyRef *openapi3.RequestBodyRef) openapi2.Parameters { + mediaType := requestBodyRef.Value.GetMediaType("multipart/form-data") + if mediaType == nil { + return nil + } + parameters := openapi2.Parameters{} + for prop, schemaRef := range mediaType.Schema.Value.Properties { + val := schemaRef.Value + typ := val.Type + if val.Format == "binary" { + typ = "file" + } + parameter := &openapi2.Parameter{ + Name: prop, + Description: val.Description, + Type: typ, + In: "formData", + ExtensionProps: val.ExtensionProps, + Enum: val.Enum, + ExclusiveMin: val.ExclusiveMin, + ExclusiveMax: val.ExclusiveMax, + MinLength: val.MinLength, + MaxLength: val.MaxLength, + Default: val.Default, + Items: val.Items, + MinItems: val.MinItems, + MaxItems: val.MaxItems, + Maximum: val.Max, + Minimum: val.Min, + Pattern: val.Pattern, + } + parameters = append(parameters, parameter) + } + return parameters +} + func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (*openapi2.Operation, error) { if operation == nil { return nil, nil @@ -539,11 +626,16 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( result.Parameters = append(result.Parameters, r) } if v := operation.RequestBody; v != nil { - r, err := FromV3RequestBody(swagger, operation, v) - if err != nil { - return nil, err + parameters := FromV3RequestBodyFormData(operation.RequestBody) + if len(parameters) > 0 { + result.Parameters = append(result.Parameters, parameters...) + } else { + r, err := FromV3RequestBody(swagger, operation, v) + if err != nil { + return nil, err + } + result.Parameters = append(result.Parameters, r) } - result.Parameters = append(result.Parameters, r) } if responses := operation.Responses; responses != nil { resultResponses, err := FromV3Responses(responses) diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index 536a56927..58191ffbf 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -161,7 +161,22 @@ const exampleV2 = ` }, "post": { "description": "example post", - "responses": {} + "responses": {}, + "parameters": [ + { + "in": "formData", + "name": "fileUpload", + "type": "file", + "description": "param description", + "x-mimetype": "text/plain" + }, + { + "in": "formData", + "name":"note", + "type": "integer", + "description": "Description of file contents" + } + ] }, "put": { "description": "example put", @@ -424,7 +439,28 @@ const exampleV3 = ` }, "post": { "description": "example post", - "responses": {} + "responses": {}, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "fileUpload": { + "description": "param description", + "format": "binary", + "type": "string", + "x-mimetype": "text/plain" + }, + "note":{ + "type": "integer", + "description": "Description of file contents" + } + }, + "type": "object" + } + } + } + } }, "put": { "description": "example put", diff --git a/openapi3/content.go b/openapi3/content.go index 8d187fd91..f28912c66 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -23,6 +23,18 @@ func NewContentWithJSONSchemaRef(schema *SchemaRef) Content { } } +func NewContentWithFormDataSchema(schema *Schema) Content { + return Content{ + "multipart/form-data": NewMediaType().WithSchema(schema), + } +} + +func NewContentWithFormDataSchemaRef(schema *SchemaRef) Content { + return Content{ + "multipart/form-data": NewMediaType().WithSchemaRef(schema), + } +} + func (content Content) Get(mime string) *MediaType { // If the mime is empty then short-circuit to the wildcard. // We do this here so that we catch only the specific case of diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 6e8c8fc72..56a055ba2 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -43,6 +43,16 @@ func (requestBody *RequestBody) WithJSONSchema(value *Schema) *RequestBody { return requestBody } +func (requestBody *RequestBody) WithFormDataSchemaRef(value *SchemaRef) *RequestBody { + requestBody.Content = NewContentWithFormDataSchemaRef(value) + return requestBody +} + +func (requestBody *RequestBody) WithFormDataSchema(value *Schema) *RequestBody { + requestBody.Content = NewContentWithFormDataSchema(value) + return requestBody +} + func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType { m := requestBody.Content if m == nil { From 6a5d5097708d77412815bd22eb4fd0c8b35296f8 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 15 Jul 2020 18:50:59 +0200 Subject: [PATCH 012/260] add couple tests around multiple file-specs (one needs fixing) + some code reuse (#236) --- openapi3/swagger_loader.go | 28 +++++++++++------------ openapi3/swagger_loader_issue235_test.go | 25 ++++++++++++++++++++ openapi3/testdata/issue235.spec0-typo.yml | 24 +++++++++++++++++++ openapi3/testdata/issue235.spec0.yml | 24 +++++++++++++++++++ openapi3/testdata/issue235.spec1.yml | 12 ++++++++++ openapi3/testdata/issue235.spec2.yml | 7 ++++++ 6 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 openapi3/swagger_loader_issue235_test.go create mode 100644 openapi3/testdata/issue235.spec0-typo.yml create mode 100644 openapi3/testdata/issue235.spec0.yml create mode 100644 openapi3/testdata/issue235.spec1.yml create mode 100644 openapi3/testdata/issue235.spec2.yml diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index cb0cda438..71216a28a 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -166,9 +166,11 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []b } func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.URL) (err error) { - swaggerLoader.visited = make(map[interface{}]struct{}) + if swaggerLoader.visited == nil { + swaggerLoader.visited = make(map[interface{}]struct{}) + } if swaggerLoader.visitedFiles == nil { - swaggerLoader.visitedFiles = make(map[string]struct{}) + swaggerLoader.reset() } // Visit all components @@ -232,7 +234,7 @@ func join(basePath *url.URL, relativePath *url.URL) (*url.URL, error) { } newPath, err := copyURL(basePath) if err != nil { - return nil, fmt.Errorf("Can't copy path: '%s'", basePath.String()) + return nil, fmt.Errorf("cannot copy path: %q", basePath.String()) } newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path) return newPath, nil @@ -274,9 +276,7 @@ func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref strin cursor = swagger for _, pathPart := range strings.Split(fragment[1:], "/") { - - pathPart = strings.Replace(pathPart, "~1", "/", -1) - pathPart = strings.Replace(pathPart, "~0", "~", -1) + pathPart = unescapeRefString(pathPart) if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil { return nil, nil, fmt.Errorf("Failed to resolve '%s' in fragment in URI: '%s': %v", ref, pathPart, err.Error()) @@ -369,7 +369,7 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component if component == nil { return errors.New("invalid header: value MUST be a JSON object") } - if ref := component.Ref; len(ref) > 0 { + if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var header Header if err := swaggerLoader.loadSingleElementFromURI(ref, path, &header); err != nil { @@ -416,7 +416,7 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon return errors.New("invalid parameter: value MUST be a JSON object") } ref := component.Ref - if len(ref) > 0 { + if ref != "" { if isSingleRefElement(ref) { var param Parameter if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { @@ -477,7 +477,7 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp if component == nil { return errors.New("invalid requestBody: value MUST be a JSON object") } - if ref := component.Ref; len(ref) > 0 { + if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var requestBody RequestBody if err := swaggerLoader.loadSingleElementFromURI(ref, path, &requestBody); err != nil { @@ -532,7 +532,7 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone return errors.New("invalid response: value MUST be a JSON object") } ref := component.Ref - if len(ref) > 0 { + if ref != "" { if isSingleRefElement(ref) { var resp Response @@ -607,7 +607,7 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component return errors.New("invalid schema: value MUST be a JSON object") } ref := component.Ref - if len(ref) > 0 { + if ref != "" { if isSingleRefElement(ref) { var schema Schema if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { @@ -692,7 +692,7 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c if component == nil { return errors.New("invalid securityScheme: value MUST be a JSON object") } - if ref := component.Ref; len(ref) > 0 { + if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var scheme SecurityScheme if err := swaggerLoader.loadSingleElementFromURI(ref, path, &scheme); err != nil { @@ -729,7 +729,7 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen if component == nil { return errors.New("invalid example: value MUST be a JSON object") } - if ref := component.Ref; len(ref) > 0 { + if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var example Example if err := swaggerLoader.loadSingleElementFromURI(ref, path, &example); err != nil { @@ -766,7 +766,7 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * if component == nil { return errors.New("invalid link: value MUST be a JSON object") } - if ref := component.Ref; len(ref) > 0 { + if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var link Link if err := swaggerLoader.loadSingleElementFromURI(ref, path, &link); err != nil { diff --git a/openapi3/swagger_loader_issue235_test.go b/openapi3/swagger_loader_issue235_test.go new file mode 100644 index 000000000..79515c12d --- /dev/null +++ b/openapi3/swagger_loader_issue235_test.go @@ -0,0 +1,25 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue235OK(t *testing.T) { + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadSwaggerFromFile("testdata/issue235.spec0.yml") + require.NoError(t, err) + err = doc.Validate(loader.Context) + require.NoError(t, err) +} + +func TestIssue235CircularDep(t *testing.T) { + t.Skip("TODO: return an error on circular dependencies between external files of a spec") + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadSwaggerFromFile("testdata/issue235.spec0-typo.yml") + require.Nil(t, doc) + require.Error(t, err) +} diff --git a/openapi3/testdata/issue235.spec0-typo.yml b/openapi3/testdata/issue235.spec0-typo.yml new file mode 100644 index 000000000..543600620 --- /dev/null +++ b/openapi3/testdata/issue235.spec0-typo.yml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + title: 'OAI Specification in YAML' + version: 0.0.1 +paths: + /test: + get: + responses: + "200": + $ref: '#/components/responses/GetTestOK' +components: + responses: + GetTestOK: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ObjectA' + schemas: + ObjectA: + type: object + properties: + object_b: + $ref: 'issue235.spec0-typo.yml#/components/schemas/ObjectD' diff --git a/openapi3/testdata/issue235.spec0.yml b/openapi3/testdata/issue235.spec0.yml new file mode 100644 index 000000000..d9236aaec --- /dev/null +++ b/openapi3/testdata/issue235.spec0.yml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + title: 'OAI Specification in YAML' + version: 0.0.1 +paths: + /test: + get: + responses: + "200": + $ref: '#/components/responses/GetTestOK' +components: + responses: + GetTestOK: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ObjectA' + schemas: + ObjectA: + type: object + properties: + object_b: + $ref: 'issue235.spec1.yml#/components/schemas/ObjectD' diff --git a/openapi3/testdata/issue235.spec1.yml b/openapi3/testdata/issue235.spec1.yml new file mode 100644 index 000000000..a1bc67906 --- /dev/null +++ b/openapi3/testdata/issue235.spec1.yml @@ -0,0 +1,12 @@ +components: + schemas: + ObjectD: + type: object + properties: + result: + $ref: '#/components/schemas/ObjectE' + + ObjectE: + properties: + name: + $ref: issue235.spec2.yml#/components/schemas/ObjectX diff --git a/openapi3/testdata/issue235.spec2.yml b/openapi3/testdata/issue235.spec2.yml new file mode 100644 index 000000000..b0bcb0fa2 --- /dev/null +++ b/openapi3/testdata/issue235.spec2.yml @@ -0,0 +1,7 @@ +components: + schemas: + ObjectX: + type: object + properties: + name: + type: string From 62affaab231f4796e160bbe0f0fad3fdd39f3779 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 16 Jul 2020 14:37:35 +0200 Subject: [PATCH 013/260] Fix wrong `Found unresolved ref` error when converting from Swagger/OpenAPIv2 spec (#237) --- openapi2conv/issue187_test.go | 100 ++++++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 29 ++++++---- openapi3/components.go | 4 +- 3 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 openapi2conv/issue187_test.go diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go new file mode 100644 index 000000000..0fccf9da8 --- /dev/null +++ b/openapi2conv/issue187_test.go @@ -0,0 +1,100 @@ +package openapi2conv + +import ( + "context" + "encoding/json" + "testing" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func v2v3(spec2 []byte) (doc3 *openapi3.Swagger, err error) { + var doc2 openapi2.Swagger + if err = json.Unmarshal(spec2, &doc2); err != nil { + return + } + doc3, err = ToV3Swagger(&doc2) + return +} + +func TestIssue187(t *testing.T) { + spec := ` +{ + "swagger": "2.0", + "info": { + "description": "Test Golang Application", + "version": "1.0", + "title": "Test", + "contact": { + "name": "Test", + "email": "test@test.com" + } + }, + + "paths": { + "/me": { + "get": { + "description": "", + "operationId": "someTest", + "summary": "Some test", + "tags": ["probe"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "successful operation", + "schema": {"$ref": "#/definitions/model.ProductSearchAttributeRequest"} + } + } + } + } + }, + + "host": "", + "basePath": "/test", + "definitions": { + "model.ProductSearchAttributeRequest": { + "type": "object", + "properties": { + "filterField": { + "type": "string" + }, + "filterKey": { + "type": "string" + }, + "type": { + "type": "string" + }, + "values": { + "$ref": "#/definitions/model.ProductSearchAttributeValueRequest" + } + }, + "title": "model.ProductSearchAttributeRequest" + }, + "model.ProductSearchAttributeValueRequest": { + "type": "object", + "properties": { + "imageUrl": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "title": "model.ProductSearchAttributeValueRequest" + } + } +} +` + doc3, err := v2v3([]byte(spec)) + require.NoError(t, err) + + spec3, err := json.Marshal(doc3) + require.NoError(t, err) + const expected = `{"components":{"schemas":{"model.ProductSearchAttributeRequest":{"properties":{"filterField":{"type":"string"},"filterKey":{"type":"string"},"type":{"type":"string"},"values":{"$ref":"#/components/schemas/model.ProductSearchAttributeValueRequest"}},"title":"model.ProductSearchAttributeRequest","type":"object"},"model.ProductSearchAttributeValueRequest":{"properties":{"imageUrl":{"type":"string"},"text":{"type":"string"}},"title":"model.ProductSearchAttributeValueRequest","type":"object"}}},"info":{"contact":{"email":"test@test.com","name":"Test"},"description":"Test Golang Application","title":"Test","version":"1.0"},"openapi":"3.0.2","paths":{"/me":{"get":{"operationId":"someTest","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/model.ProductSearchAttributeRequest"}}},"description":"successful operation"}},"summary":"Some test","tags":["probe"]}}}}` + require.Equal(t, string(spec3), expected) + + err = doc3.Validate(context.Background()) + require.NoError(t, err) +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index dada63704..408e3f5b0 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -23,13 +23,11 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { ExtensionProps: swagger.ExtensionProps, ExternalDocs: swagger.ExternalDocs, } - host := swagger.Host - if len(host) > 0 { + + if host := swagger.Host; len(host) > 0 { schemes := swagger.Schemes if len(schemes) == 0 { - schemes = []string{ - "https://", - } + schemes = []string{"https://"} } basePath := swagger.BasePath for _, scheme := range schemes { @@ -38,11 +36,10 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { Host: host, Path: basePath, } - result.AddServer(&openapi3.Server{ - URL: u.String(), - }) + result.AddServer(&openapi3.Server{URL: u.String()}) } } + if paths := swagger.Paths; paths != nil { resultPaths := make(map[string]*openapi3.PathItem, len(paths)) for path, pathItem := range paths { @@ -54,6 +51,7 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { } result.Paths = resultPaths } + if parameters := swagger.Parameters; parameters != nil { result.Components.Parameters = make(map[string]*openapi3.ParameterRef) result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) @@ -70,6 +68,7 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { } } } + if responses := swagger.Responses; responses != nil { result.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses)) for k, response := range responses { @@ -80,7 +79,9 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { result.Components.Responses[k] = r } } + result.Components.Schemas = ToV3Schemas(swagger.Definitions) + if m := swagger.SecurityDefinitions; m != nil { resultSecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef) for k, v := range m { @@ -92,7 +93,15 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { } result.Components.SecuritySchemes = resultSecuritySchemes } + result.Security = ToV3SecurityRequirements(swagger.Security) + + { + sl := openapi3.NewSwaggerLoader() + if err := sl.ResolveRefsIn(result, nil); err != nil { + return nil, err + } + } return result, nil } @@ -508,7 +517,7 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { return schema } if schema.Value.Items != nil { - schema.Value.Items = FromV3SchemaRef((schema.Value.Items)) + schema.Value.Items = FromV3SchemaRef(schema.Value.Items) } for k, v := range schema.Value.Properties { schema.Value.Properties[k] = FromV3SchemaRef(v) @@ -814,7 +823,7 @@ var attemptedBodyParameterNames = []string{ } func stripNonCustomExtensions(extensions map[string]interface{}) { - for extName, _ := range extensions { + for extName := range extensions { if !strings.HasPrefix(extName, "x-") { delete(extensions, extName) } diff --git a/openapi3/components.go b/openapi3/components.go index 78b66aa31..a8ebd7926 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -92,7 +92,7 @@ func (components *Components) Validate(c context.Context) (err error) { return } -const identifierPattern = `^[a-zA-Z0-9.\-_]+$` +const identifierPattern = `^[a-zA-Z0-9._-]+$` var identifierRegExp = regexp.MustCompile(identifierPattern) @@ -100,5 +100,5 @@ func ValidateIdentifier(value string) error { if identifierRegExp.MatchString(value) { return nil } - return fmt.Errorf("Identifier '%s' is not supported by OpenAPI version 3 standard (regexp: '%s')", value, identifierPattern) + return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (regexp: %q)", value, identifierPattern) } From 8459893af4ad6c5efe9a42f13d685dfe52ceb91f Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 17 Jul 2020 22:11:37 +0200 Subject: [PATCH 014/260] various fixes mainly to openapi2<->openapi3 conversion (#239) --- openapi2/openapi2.go | 46 +-- openapi2/testdata/swagger.json | 2 +- openapi2conv/issue187_test.go | 75 +++- openapi2conv/openapi2_conv.go | 367 ++++++++++++-------- openapi2conv/openapi2_conv_test.go | 531 +++++++++++++++++------------ openapi3/media_type.go | 4 +- openapi3/schema.go | 11 +- 7 files changed, 635 insertions(+), 401 deletions(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index f6b113fa1..247775257 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -17,6 +17,7 @@ import ( type Swagger struct { openapi3.ExtensionProps + Swagger string `json:"swagger"` Info openapi3.Info `json:"info"` ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"` Schemes []string `json:"schemes,omitempty"` @@ -168,27 +169,30 @@ type Parameters []*Parameter type Parameter struct { openapi3.ExtensionProps - Ref string `json:"$ref,omitempty"` - In string `json:"in,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Required bool `json:"required,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - ExclusiveMin bool `json:"exclusiveMinimum,omitempty"` - ExclusiveMax bool `json:"exclusiveMaximum,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty"` - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Enum []interface{} `json:"enum,omitempty"` - Minimum *float64 `json:"minimum,omitempty"` - Maximum *float64 `json:"maximum,omitempty"` - MinLength uint64 `json:"minLength,omitempty"` - MaxLength *uint64 `json:"maxLength,omitempty"` - Pattern string `json:"pattern,omitempty"` - Items *openapi3.SchemaRef `json:"items,omitempty"` - MinItems uint64 `json:"minItems,omitempty"` - MaxItems *uint64 `json:"maxItems,omitempty"` - Default interface{} `json:"default,omitempty"` + Ref string `json:"$ref,omitempty"` + In string `json:"in,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + CollectionFormat string `json:"collectionFormat,omitempty"` + Type string `json:"type,omitempty"` + Format string `json:"format,omitempty"` + Pattern string `json:"pattern,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty"` + Required bool `json:"required,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty"` + ExclusiveMin bool `json:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty"` + Items *openapi3.SchemaRef `json:"items,omitempty"` + Enum []interface{} `json:"enum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty"` + Minimum *float64 `json:"minimum,omitempty"` + Maximum *float64 `json:"maximum,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty"` + MinLength uint64 `json:"minLength,omitempty"` + MinItems uint64 `json:"minItems,omitempty"` + Default interface{} `json:"default,omitempty"` } func (parameter *Parameter) MarshalJSON() ([]byte, error) { diff --git a/openapi2/testdata/swagger.json b/openapi2/testdata/swagger.json index 91484ff26..57f75d9a7 100644 --- a/openapi2/testdata/swagger.json +++ b/openapi2/testdata/swagger.json @@ -1 +1 @@ -{"info":{"title":"Swagger Petstore","description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.3"},"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"},"schemes":["https","http"],"host":"petstore.swagger.io","basePath":"/v2","paths":{"/pet":{"post":{"summary":"Add a new pet to the store","tags":["pet"],"operationId":"addPet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"put":{"summary":"Update an existing pet","tags":["pet"],"operationId":"updatePet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","tags":["pet"],"operationId":"findPetsByStatus","parameters":[{"in":"query","name":"status","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"default":"available","enum":["available","pending","sold"],"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid status value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","tags":["pet"],"operationId":"findPetsByTags","parameters":[{"in":"query","name":"tags","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid tag value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}":{"delete":{"summary":"Deletes a pet","tags":["pet"],"operationId":"deletePet","parameters":[{"in":"header","name":"api_key","type":"string"},{"in":"path","name":"petId","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"get":{"summary":"Find pet by ID","description":"Returns a single pet","tags":["pet"],"operationId":"getPetById","parameters":[{"in":"path","name":"petId","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"api_key":[]}]},"post":{"summary":"Updates a pet in the store with form data","tags":["pet"],"operationId":"updatePetWithForm","parameters":[{"in":"path","name":"petId","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"name","description":"Updated name of the pet","type":"string"},{"in":"formData","name":"status","description":"Updated status of the pet","type":"string"}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/x-www-form-urlencoded"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}/uploadImage":{"post":{"summary":"uploads an image","tags":["pet"],"operationId":"uploadFile","parameters":[{"in":"path","name":"petId","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"additionalMetadata","description":"Additional data to pass to server","type":"string"},{"in":"formData","name":"file","description":"file to upload","type":"file"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}}},"consumes":["multipart/form-data"],"produces":["application/json"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/inventory":{"get":{"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","tags":["store"],"operationId":"getInventory","responses":{"200":{"description":"successful operation","schema":{"additionalProperties":{"format":"int32","type":"integer"},"type":"object"}}},"produces":["application/json"],"security":[{"api_key":[]}]}},"/store/order":{"post":{"summary":"Place an order for a pet","tags":["store"],"operationId":"placeOrder","parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid Order"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/store/order/{orderId}":{"delete":{"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors","tags":["store"],"operationId":"deleteOrder","parameters":[{"in":"path","name":"orderId","description":"ID of the order that needs to be deleted","required":true,"type":"integer","format":"int64","minimum":1}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value \u003e= 1 and \u003c= 10. Other values will generated exceptions","tags":["store"],"operationId":"getOrderById","parameters":[{"in":"path","name":"orderId","description":"ID of pet that needs to be fetched","required":true,"type":"integer","format":"int64","minimum":1,"maximum":10}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]}},"/user":{"post":{"summary":"Create user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"createUser","parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithArray":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithArrayInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithList":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithListInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/login":{"get":{"summary":"Logs user into the system","tags":["user"],"operationId":"loginUser","parameters":[{"in":"query","name":"username","description":"The user name for login","required":true,"type":"string"},{"in":"query","name":"password","description":"The password for login in clear text","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"string"},"headers":{"X-Expires-After":{"description":"date in UTC when token expires","type":"string"},"X-Rate-Limit":{"description":"calls per hour allowed by the user","type":"integer"}}},"400":{"description":"Invalid username/password supplied"}},"produces":["application/json","application/xml"]}},"/user/logout":{"get":{"summary":"Logs out current logged in user session","tags":["user"],"operationId":"logoutUser","responses":{"default":{"description":"successful operation"}},"produces":["application/json","application/xml"]}},"/user/{username}":{"delete":{"summary":"Delete user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"deleteUser","parameters":[{"in":"path","name":"username","description":"The name that needs to be deleted","required":true,"type":"string"}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Get user by user name","tags":["user"],"operationId":"getUserByName","parameters":[{"in":"path","name":"username","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"put":{"summary":"Updated user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"updateUser","parameters":[{"in":"path","name":"username","description":"name that need to be updated","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"400":{"description":"Invalid user supplied"},"404":{"description":"User not found"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}}},"definitions":{"ApiResponse":{"properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"},"type":{"type":"string"}},"type":"object"},"Category":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Category"}},"Order":{"properties":{"complete":{"type":"boolean"},"id":{"format":"int64","type":"integer"},"petId":{"format":"int64","type":"integer"},"quantity":{"format":"int32","type":"integer"},"shipDate":{"format":"date-time","type":"string"},"status":{"description":"Order Status","enum":["placed","approved","delivered"],"type":"string"}},"type":"object","xml":{"name":"Order"}},"Pet":{"properties":{"category":{"$ref":"#/definitions/Category"},"id":{"format":"int64","type":"integer"},"name":{"example":"doggie","type":"string"},"photoUrls":{"items":{"type":"string","xml":{"name":"photoUrl"}},"type":"array","xml":{"wrapped":true}},"status":{"description":"pet status in the store","enum":["available","pending","sold"],"type":"string"},"tags":{"items":{"$ref":"#/definitions/Tag"},"type":"array","xml":{"wrapped":true}}},"required":["name","photoUrls"],"type":"object","xml":{"name":"Pet"}},"Tag":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Tag"}},"User":{"properties":{"email":{"type":"string"},"firstName":{"type":"string"},"id":{"format":"int64","type":"integer"},"lastName":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"description":"User Status","format":"int32","type":"integer"},"username":{"type":"string"}},"type":"object","xml":{"name":"User"}}},"securityDefinitions":{"api_key":{"type":"apiKey","in":"header","name":"api_key"},"petstore_auth":{"type":"oauth2","flow":"implicit","authorizationUrl":"https://petstore.swagger.io/oauth/authorize","scopes":{"read:pets":"read your pets","write:pets":"modify pets in your account"}}},"tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Access to Petstore orders"},{"name":"user","description":"Operations about user","externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}]} \ No newline at end of file +{"swagger":"2.0","info":{"title":"Swagger Petstore","description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.3"},"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"},"schemes":["https","http"],"host":"petstore.swagger.io","basePath":"/v2","paths":{"/pet":{"post":{"summary":"Add a new pet to the store","tags":["pet"],"operationId":"addPet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"put":{"summary":"Update an existing pet","tags":["pet"],"operationId":"updatePet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","tags":["pet"],"operationId":"findPetsByStatus","parameters":[{"in":"query","name":"status","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"default":"available","enum":["available","pending","sold"],"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid status value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","tags":["pet"],"operationId":"findPetsByTags","parameters":[{"in":"query","name":"tags","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid tag value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}":{"delete":{"summary":"Deletes a pet","tags":["pet"],"operationId":"deletePet","parameters":[{"in":"header","name":"api_key","type":"string"},{"in":"path","name":"petId","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"get":{"summary":"Find pet by ID","description":"Returns a single pet","tags":["pet"],"operationId":"getPetById","parameters":[{"in":"path","name":"petId","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"api_key":[]}]},"post":{"summary":"Updates a pet in the store with form data","tags":["pet"],"operationId":"updatePetWithForm","parameters":[{"in":"path","name":"petId","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"name","description":"Updated name of the pet","type":"string"},{"in":"formData","name":"status","description":"Updated status of the pet","type":"string"}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/x-www-form-urlencoded"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}/uploadImage":{"post":{"summary":"uploads an image","tags":["pet"],"operationId":"uploadFile","parameters":[{"in":"path","name":"petId","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"additionalMetadata","description":"Additional data to pass to server","type":"string"},{"in":"formData","name":"file","description":"file to upload","type":"file"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}}},"consumes":["multipart/form-data"],"produces":["application/json"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/inventory":{"get":{"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","tags":["store"],"operationId":"getInventory","responses":{"200":{"description":"successful operation","schema":{"additionalProperties":{"format":"int32","type":"integer"},"type":"object"}}},"produces":["application/json"],"security":[{"api_key":[]}]}},"/store/order":{"post":{"summary":"Place an order for a pet","tags":["store"],"operationId":"placeOrder","parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid Order"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/store/order/{orderId}":{"delete":{"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors","tags":["store"],"operationId":"deleteOrder","parameters":[{"in":"path","name":"orderId","description":"ID of the order that needs to be deleted","required":true,"type":"integer","format":"int64","minimum":1}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value \u003e= 1 and \u003c= 10. Other values will generated exceptions","tags":["store"],"operationId":"getOrderById","parameters":[{"in":"path","name":"orderId","description":"ID of pet that needs to be fetched","required":true,"type":"integer","format":"int64","minimum":1,"maximum":10}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]}},"/user":{"post":{"summary":"Create user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"createUser","parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithArray":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithArrayInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithList":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithListInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/login":{"get":{"summary":"Logs user into the system","tags":["user"],"operationId":"loginUser","parameters":[{"in":"query","name":"username","description":"The user name for login","required":true,"type":"string"},{"in":"query","name":"password","description":"The password for login in clear text","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"string"},"headers":{"X-Expires-After":{"description":"date in UTC when token expires","type":"string"},"X-Rate-Limit":{"description":"calls per hour allowed by the user","type":"integer"}}},"400":{"description":"Invalid username/password supplied"}},"produces":["application/json","application/xml"]}},"/user/logout":{"get":{"summary":"Logs out current logged in user session","tags":["user"],"operationId":"logoutUser","responses":{"default":{"description":"successful operation"}},"produces":["application/json","application/xml"]}},"/user/{username}":{"delete":{"summary":"Delete user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"deleteUser","parameters":[{"in":"path","name":"username","description":"The name that needs to be deleted","required":true,"type":"string"}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Get user by user name","tags":["user"],"operationId":"getUserByName","parameters":[{"in":"path","name":"username","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"put":{"summary":"Updated user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"updateUser","parameters":[{"in":"path","name":"username","description":"name that need to be updated","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"400":{"description":"Invalid user supplied"},"404":{"description":"User not found"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}}},"definitions":{"ApiResponse":{"properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"},"type":{"type":"string"}},"type":"object"},"Category":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Category"}},"Order":{"properties":{"complete":{"type":"boolean"},"id":{"format":"int64","type":"integer"},"petId":{"format":"int64","type":"integer"},"quantity":{"format":"int32","type":"integer"},"shipDate":{"format":"date-time","type":"string"},"status":{"description":"Order Status","enum":["placed","approved","delivered"],"type":"string"}},"type":"object","xml":{"name":"Order"}},"Pet":{"properties":{"category":{"$ref":"#/definitions/Category"},"id":{"format":"int64","type":"integer"},"name":{"example":"doggie","type":"string"},"photoUrls":{"items":{"type":"string","xml":{"name":"photoUrl"}},"type":"array","xml":{"wrapped":true}},"status":{"description":"pet status in the store","enum":["available","pending","sold"],"type":"string"},"tags":{"items":{"$ref":"#/definitions/Tag"},"type":"array","xml":{"wrapped":true}}},"required":["name","photoUrls"],"type":"object","xml":{"name":"Pet"}},"Tag":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Tag"}},"User":{"properties":{"email":{"type":"string"},"firstName":{"type":"string"},"id":{"format":"int64","type":"integer"},"lastName":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"description":"User Status","format":"int32","type":"integer"},"username":{"type":"string"}},"type":"object","xml":{"name":"User"}}},"securityDefinitions":{"api_key":{"type":"apiKey","in":"header","name":"api_key"},"petstore_auth":{"type":"oauth2","flow":"implicit","authorizationUrl":"https://petstore.swagger.io/oauth/authorize","scopes":{"read:pets":"read your pets","write:pets":"modify pets in your account"}}},"tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Access to Petstore orders"},{"name":"user","description":"Operations about user","externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}]} \ No newline at end of file diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index 0fccf9da8..979866c34 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -7,10 +7,11 @@ import ( "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" + "github.com/ghodss/yaml" "github.com/stretchr/testify/require" ) -func v2v3(spec2 []byte) (doc3 *openapi3.Swagger, err error) { +func v2v3JSON(spec2 []byte) (doc3 *openapi3.Swagger, err error) { var doc2 openapi2.Swagger if err = json.Unmarshal(spec2, &doc2); err != nil { return @@ -19,6 +20,15 @@ func v2v3(spec2 []byte) (doc3 *openapi3.Swagger, err error) { return } +func v2v3YAML(spec2 []byte) (doc3 *openapi3.Swagger, err error) { + var doc2 openapi2.Swagger + if err = yaml.Unmarshal(spec2, &doc2); err != nil { + return + } + doc3, err = ToV3Swagger(&doc2) + return +} + func TestIssue187(t *testing.T) { spec := ` { @@ -87,12 +97,71 @@ func TestIssue187(t *testing.T) { } } ` - doc3, err := v2v3([]byte(spec)) + doc3, err := v2v3JSON([]byte(spec)) require.NoError(t, err) spec3, err := json.Marshal(doc3) require.NoError(t, err) - const expected = `{"components":{"schemas":{"model.ProductSearchAttributeRequest":{"properties":{"filterField":{"type":"string"},"filterKey":{"type":"string"},"type":{"type":"string"},"values":{"$ref":"#/components/schemas/model.ProductSearchAttributeValueRequest"}},"title":"model.ProductSearchAttributeRequest","type":"object"},"model.ProductSearchAttributeValueRequest":{"properties":{"imageUrl":{"type":"string"},"text":{"type":"string"}},"title":"model.ProductSearchAttributeValueRequest","type":"object"}}},"info":{"contact":{"email":"test@test.com","name":"Test"},"description":"Test Golang Application","title":"Test","version":"1.0"},"openapi":"3.0.2","paths":{"/me":{"get":{"operationId":"someTest","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/model.ProductSearchAttributeRequest"}}},"description":"successful operation"}},"summary":"Some test","tags":["probe"]}}}}` + const expected = `{"components":{"schemas":{"model.ProductSearchAttributeRequest":{"properties":{"filterField":{"type":"string"},"filterKey":{"type":"string"},"type":{"type":"string"},"values":{"$ref":"#/components/schemas/model.ProductSearchAttributeValueRequest"}},"title":"model.ProductSearchAttributeRequest","type":"object"},"model.ProductSearchAttributeValueRequest":{"properties":{"imageUrl":{"type":"string"},"text":{"type":"string"}},"title":"model.ProductSearchAttributeValueRequest","type":"object"}}},"info":{"contact":{"email":"test@test.com","name":"Test"},"description":"Test Golang Application","title":"Test","version":"1.0"},"openapi":"3.0.3","paths":{"/me":{"get":{"operationId":"someTest","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/model.ProductSearchAttributeRequest"}}},"description":"successful operation"}},"summary":"Some test","tags":["probe"]}}}}` + require.Equal(t, string(spec3), expected) + + err = doc3.Validate(context.Background()) + require.NoError(t, err) +} + +func TestIssue237(t *testing.T) { + spec := ` +swagger: '2.0' +info: + version: 1.0.0 + title: title +paths: + /test: + get: + parameters: + - in: body + schema: + $ref: '#/definitions/TestRef' + responses: + '200': + description: description +definitions: + TestRef: + type: object + allOf: + - $ref: '#/definitions/TestRef2' + TestRef2: + type: object +` + doc3, err := v2v3YAML([]byte(spec)) + require.NoError(t, err) + + spec3, err := yaml.Marshal(doc3) + require.NoError(t, err) + const expected = `components: + schemas: + TestRef: + allOf: + - $ref: '#/components/schemas/TestRef2' + type: object + TestRef2: + type: object +info: + title: title + version: 1.0.0 +openapi: 3.0.3 +paths: + /test: + get: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRef' + responses: + "200": + description: description +` require.Equal(t, string(spec3), expected) err = doc3.Validate(context.Background()) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 408e3f5b0..97b00ebda 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -16,7 +16,7 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { stripNonCustomExtensions(swagger.Extensions) result := &openapi3.Swagger{ - OpenAPI: "3.0.2", + OpenAPI: "3.0.3", Info: &swagger.Info, Components: openapi3.Components{}, Tags: swagger.Tags, @@ -24,7 +24,7 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { ExternalDocs: swagger.ExternalDocs, } - if host := swagger.Host; len(host) > 0 { + if host := swagger.Host; host != "" { schemes := swagger.Schemes if len(schemes) == 0 { schemes = []string{"https://"} @@ -53,18 +53,17 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { } if parameters := swagger.Parameters; parameters != nil { - result.Components.Parameters = make(map[string]*openapi3.ParameterRef) - result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) + result.Components.Parameters = make(map[string]*openapi3.ParameterRef, len(parameters)) + result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef, len(parameters)) for k, parameter := range parameters { - resultParameter, resultRequestBody, err := ToV3Parameter(parameter) - if err != nil { + v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) + switch { + case err != nil: return nil, err - } - if resultParameter != nil { - result.Components.Parameters[k] = resultParameter - } - if resultRequestBody != nil { - result.Components.RequestBodies[k] = resultRequestBody + case v3RequestBody != nil: + result.Components.RequestBodies[k] = v3RequestBody + default: + result.Components.Parameters[k] = v3Parameter } } } @@ -119,62 +118,18 @@ func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*open } for _, parameter := range pathItem.Parameters { v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) - if err != nil { + switch { + case err != nil: return nil, err + case v3RequestBody != nil: + return nil, errors.New("pathItem must not have a body parameter") + default: + result.Parameters = append(result.Parameters, v3Parameter) } - if v3RequestBody != nil { - return nil, errors.New("PathItem shouldn't have a body parameter") - } - result.Parameters = append(result.Parameters, v3Parameter) } return result, nil } -func ToV3RequestBodyFormData(parameters []*openapi2.Parameter) *openapi3.RequestBodyRef { - if len(parameters) == 0 || parameters[0].In != "formData" { - return nil - } - schema := &openapi3.Schema{ - Type: "object", - Properties: make(map[string]*openapi3.SchemaRef, len(parameters)), - } - for _, parameter := range parameters { - if parameter.In != "formData" || parameter.Name == "" { - continue - } - format := parameter.Format - typ := parameter.Type - if parameter.Type == "file" { - format = "binary" - typ = "string" - } - pschema := &openapi3.Schema{ - Description: parameter.Description, - Type: typ, - ExtensionProps: parameter.ExtensionProps, - Format: format, - Enum: parameter.Enum, - Min: parameter.Minimum, - Max: parameter.Maximum, - ExclusiveMin: parameter.ExclusiveMin, - ExclusiveMax: parameter.ExclusiveMax, - MinLength: parameter.MinLength, - MaxLength: parameter.MaxLength, - Default: parameter.Default, - Items: parameter.Items, - MinItems: parameter.MinItems, - MaxItems: parameter.MaxItems, - } - schemaRef := openapi3.SchemaRef{ - Value: pschema, - } - schema.Properties[parameter.Name] = &schemaRef - } - return &openapi3.RequestBodyRef{ - Value: openapi3.NewRequestBody().WithFormDataSchema(schema), - } -} - func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, operation *openapi2.Operation) (*openapi3.Operation, error) { if operation == nil { return nil, nil @@ -192,22 +147,23 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera result.Security = &resultSecurity } - requestBodyRef := ToV3RequestBodyFormData(operation.Parameters) - if requestBodyRef != nil { - result.RequestBody = requestBodyRef - } else { - for _, parameter := range operation.Parameters { - v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) - if err != nil { - return nil, err - } - if v3RequestBody != nil { - result.RequestBody = v3RequestBody - } else if v3Parameter != nil { - result.Parameters = append(result.Parameters, v3Parameter) - } + var reqBodies []*openapi3.RequestBodyRef + for _, parameter := range operation.Parameters { + v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) + switch { + case err != nil: + return nil, err + case v3RequestBody != nil: + reqBodies = append(reqBodies, v3RequestBody) + default: + result.Parameters = append(result.Parameters, v3Parameter) } } + var err error + if result.RequestBody, err = onlyOneReqBodyParam(reqBodies); err != nil { + return nil, err + } + if responses := operation.Responses; responses != nil { resultResponses := make(openapi3.Responses, len(responses)) for k, response := range responses { @@ -223,68 +179,159 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera } func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, error) { - if parameter == nil { - return nil, nil, nil - } - if ref := parameter.Ref; len(ref) > 0 { - return &openapi3.ParameterRef{ - Ref: ToV3Ref(ref), - }, nil, nil + if ref := parameter.Ref; ref != "" { + return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil } stripNonCustomExtensions(parameter.Extensions) - in := parameter.In - if in == "body" { + + switch parameter.In { + case "body": result := &openapi3.RequestBody{ Description: parameter.Description, Required: parameter.Required, ExtensionProps: parameter.ExtensionProps, } if schemaRef := parameter.Schema; schemaRef != nil { - // Assume it's JSON + // Assuming JSON result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef)) } - return nil, &openapi3.RequestBodyRef{ - Value: result, - }, nil + return nil, &openapi3.RequestBodyRef{Value: result}, nil + + case "formData": + format, typ := parameter.Format, parameter.Type + if typ == "file" { + format, typ = "binary", "string" + } + reqBodyRef := formDataBody( + map[string]*openapi3.SchemaRef{ + parameter.Name: { + Value: &openapi3.Schema{ + Description: parameter.Description, + Type: typ, + ExtensionProps: parameter.ExtensionProps, + Format: format, + Enum: parameter.Enum, + Min: parameter.Minimum, + Max: parameter.Maximum, + ExclusiveMin: parameter.ExclusiveMin, + ExclusiveMax: parameter.ExclusiveMax, + MinLength: parameter.MinLength, + MaxLength: parameter.MaxLength, + Default: parameter.Default, + Items: parameter.Items, + MinItems: parameter.MinItems, + MaxItems: parameter.MaxItems, + Pattern: parameter.Pattern, + AllowEmptyValue: parameter.AllowEmptyValue, + UniqueItems: parameter.UniqueItems, + MultipleOf: parameter.MultipleOf, + }, + }, + }, + map[string]bool{parameter.Name: parameter.Required}, + ) + return nil, reqBodyRef, nil + + default: + required := parameter.Required + if parameter.In == openapi3.ParameterInPath { + required = true + } + result := &openapi3.Parameter{ + In: parameter.In, + Name: parameter.Name, + Description: parameter.Description, + Required: required, + ExtensionProps: parameter.ExtensionProps, + Schema: ToV3SchemaRef(&openapi3.SchemaRef{Value: &openapi3.Schema{ + Type: parameter.Type, + Format: parameter.Format, + Enum: parameter.Enum, + Min: parameter.Minimum, + Max: parameter.Maximum, + ExclusiveMin: parameter.ExclusiveMin, + ExclusiveMax: parameter.ExclusiveMax, + MinLength: parameter.MinLength, + MaxLength: parameter.MaxLength, + Default: parameter.Default, + Items: parameter.Items, + MinItems: parameter.MinItems, + MaxItems: parameter.MaxItems, + Pattern: parameter.Pattern, + AllowEmptyValue: parameter.AllowEmptyValue, + UniqueItems: parameter.UniqueItems, + MultipleOf: parameter.MultipleOf, + }}), + } + return &openapi3.ParameterRef{Value: result}, nil, nil } - result := &openapi3.Parameter{ - In: in, - Name: parameter.Name, - Description: parameter.Description, - Required: parameter.Required, - ExtensionProps: parameter.ExtensionProps, +} + +func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool) *openapi3.RequestBodyRef { + if len(bodies) != len(reqs) { + panic(`request bodies and them being required must match`) + } + requireds := make([]string, 0, len(reqs)) + for propName, req := range reqs { + if _, ok := bodies[propName]; !ok { + panic(`request bodies and them being required must match`) + } + if req { + requireds = append(requireds, propName) + } + } + schema := &openapi3.Schema{ + Type: "object", + Properties: ToV3Schemas(bodies), + Required: requireds, } + return &openapi3.RequestBodyRef{ + Value: openapi3.NewRequestBody().WithFormDataSchema(schema), + } +} - if parameter.Type != "" { - schema := &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: parameter.Type, - Format: parameter.Format, - Enum: parameter.Enum, - Min: parameter.Minimum, - Max: parameter.Maximum, - ExclusiveMin: parameter.ExclusiveMin, - ExclusiveMax: parameter.ExclusiveMax, - MinLength: parameter.MinLength, - MaxLength: parameter.MaxLength, - Default: parameter.Default, - Items: parameter.Items, - MinItems: parameter.MinItems, - MaxItems: parameter.MaxItems, - }, +func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef) (*openapi3.RequestBodyRef, error) { + var ( + body *openapi3.RequestBodyRef + formDataParams map[string]*openapi3.SchemaRef + formDataReqs map[string]bool + ) + for i, requestBodyRef := range bodies { + mediaType := requestBodyRef.Value.GetMediaType("multipart/form-data") + if mediaType != nil { + for name, schemaRef := range mediaType.Schema.Value.Properties { + if formDataParams == nil { + formDataParams = make(map[string]*openapi3.SchemaRef, len(bodies)-i) + } + if formDataReqs == nil { + formDataReqs = make(map[string]bool, len(bodies)-i) + } + formDataParams[name] = schemaRef + formDataReqs[name] = false + for _, req := range mediaType.Schema.Value.Required { + if name == req { + formDataReqs[name] = true + } + } + break + } + } else { + body = requestBodyRef } - result.Schema = ToV3SchemaRef(schema) } - return &openapi3.ParameterRef{ - Value: result, - }, nil, nil + switch { + case len(formDataParams) != 0 && body != nil: + return nil, errors.New("body and form parameters cannot exist together for the same operation") + case len(formDataParams) != 0: + return formDataBody(formDataParams, formDataReqs), nil + default: + return body, nil + } } func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) { - if ref := response.Ref; len(ref) > 0 { - return &openapi3.ResponseRef{ - Ref: ToV3Ref(ref), - }, nil + if ref := response.Ref; ref != "" { + return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil } stripNonCustomExtensions(response.Extensions) result := &openapi3.Response{ @@ -308,10 +355,8 @@ func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.Schem } func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { - if ref := schema.Ref; len(ref) > 0 { - return &openapi3.SchemaRef{ - Ref: ToV3Ref(ref), - } + if ref := schema.Ref; ref != "" { + return &openapi3.SchemaRef{Ref: ToV3Ref(ref)} } if schema.Value == nil { return schema @@ -322,8 +367,11 @@ func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { for k, v := range schema.Value.Properties { schema.Value.Properties[k] = ToV3SchemaRef(v) } - if schema.Value.AdditionalProperties != nil { - schema.Value.AdditionalProperties = ToV3SchemaRef(schema.Value.AdditionalProperties) + if v := schema.Value.AdditionalProperties; v != nil { + schema.Value.AdditionalProperties = ToV3SchemaRef(v) + } + for i, v := range schema.Value.AllOf { + schema.Value.AllOf[i] = ToV3SchemaRef(v) } return schema } @@ -410,6 +458,7 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu }, nil } +// FromV3Swagger converts an OpenAPIv3 spec to an OpenAPIv2 spec func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { resultResponses, err := FromV3Responses(swagger.Components.Responses) if err != nil { @@ -418,6 +467,7 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { stripNonCustomExtensions(swagger.Extensions) result := &openapi2.Swagger{ + Swagger: "2.0", Info: *swagger.Info, Definitions: FromV3Schemas(swagger.Components.Schemas), Responses: resultResponses, @@ -508,22 +558,23 @@ func FromV3Schemas(schemas map[string]*openapi3.SchemaRef) map[string]*openapi3. } func FromV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { - if ref := schema.Ref; len(ref) > 0 { - return &openapi3.SchemaRef{ - Ref: FromV3Ref(ref), - } + if ref := schema.Ref; ref != "" { + return &openapi3.SchemaRef{Ref: FromV3Ref(ref)} } if schema.Value == nil { return schema } - if schema.Value.Items != nil { - schema.Value.Items = FromV3SchemaRef(schema.Value.Items) + if v := schema.Value.Items; v != nil { + schema.Value.Items = FromV3SchemaRef(v) } for k, v := range schema.Value.Properties { schema.Value.Properties[k] = FromV3SchemaRef(v) } - if schema.Value.AdditionalProperties != nil { - schema.Value.AdditionalProperties = FromV3SchemaRef(schema.Value.AdditionalProperties) + if v := schema.Value.AdditionalProperties; v != nil { + schema.Value.AdditionalProperties = FromV3SchemaRef(v) + } + for i, v := range schema.Value.AllOf { + schema.Value.AllOf[i] = FromV3SchemaRef(v) } return schema } @@ -581,14 +632,21 @@ func FromV3RequestBodyFormData(requestBodyRef *openapi3.RequestBodyRef) openapi2 return nil } parameters := openapi2.Parameters{} - for prop, schemaRef := range mediaType.Schema.Value.Properties { + for propName, schemaRef := range mediaType.Schema.Value.Properties { val := schemaRef.Value typ := val.Type if val.Format == "binary" { typ = "file" } + required := false + for _, name := range val.Required { + if name == propName { + required = true + break + } + } parameter := &openapi2.Parameter{ - Name: prop, + Name: propName, Description: val.Description, Type: typ, In: "formData", @@ -605,6 +663,12 @@ func FromV3RequestBodyFormData(requestBodyRef *openapi3.RequestBodyRef) openapi2 Maximum: val.Max, Minimum: val.Min, Pattern: val.Pattern, + // CollectionFormat: val.CollectionFormat, + // Format: val.Format, + AllowEmptyValue: val.AllowEmptyValue, + Required: required, + UniqueItems: val.UniqueItems, + MultipleOf: val.MultipleOf, } parameters = append(parameters, parameter) } @@ -646,6 +710,11 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( result.Parameters = append(result.Parameters, r) } } + for _, param := range result.Parameters { + if param.Type == "file" { + result.Consumes = append(result.Consumes, "multipart/form-data") + } + } if responses := operation.Responses; responses != nil { resultResponses, err := FromV3Responses(responses) if err != nil { @@ -657,10 +726,8 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( } func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation, requestBodyRef *openapi3.RequestBodyRef) (*openapi2.Parameter, error) { - if ref := requestBodyRef.Ref; len(ref) > 0 { - return &openapi2.Parameter{ - Ref: FromV3Ref(ref), - }, nil + if ref := requestBodyRef.Ref; ref != "" { + return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil } requestBody := requestBodyRef.Value @@ -680,19 +747,17 @@ func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation, ExtensionProps: requestBody.ExtensionProps, } - // Add JSON schema + // Assuming JSON mediaType := requestBody.GetMediaType("application/json") if mediaType != nil { - result.Schema = mediaType.Schema + result.Schema = FromV3SchemaRef(mediaType.Schema) } return result, nil } func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) { - if v := ref.Ref; len(v) > 0 { - return &openapi2.Parameter{ - Ref: FromV3Ref(v), - }, nil + if ref := ref.Ref; ref != "" { + return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil } parameter := ref.Value if parameter == nil { @@ -723,6 +788,10 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) { result.Items = schema.Items result.MinItems = schema.MinItems result.MaxItems = schema.MaxItems + result.AllowEmptyValue = schema.AllowEmptyValue + // result.CollectionFormat = schema.CollectionFormat + result.UniqueItems = schema.UniqueItems + result.MultipleOf = schema.MultipleOf } return result, nil } @@ -740,10 +809,8 @@ func FromV3Responses(responses map[string]*openapi3.ResponseRef) (map[string]*op } func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) { - if v := ref.Ref; len(v) > 0 { - return &openapi2.Response{ - Ref: FromV3Ref(v), - }, nil + if ref := ref.Ref; ref != "" { + return &openapi2.Response{Ref: FromV3Ref(ref)}, nil } response := ref.Value diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index 58191ffbf..8ccc32ce4 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -1,6 +1,7 @@ package openapi2conv import ( + "context" "encoding/json" "testing" @@ -10,135 +11,168 @@ import ( ) func TestConvOpenAPIV3ToV2(t *testing.T) { - var swagger3 openapi3.Swagger - err := json.Unmarshal([]byte(exampleV3), &swagger3) + var doc3 openapi3.Swagger + err := json.Unmarshal([]byte(exampleV3), &doc3) require.NoError(t, err) + { + // Refs need resolving before we can Validate + sl := openapi3.NewSwaggerLoader() + err = sl.ResolveRefsIn(&doc3, nil) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + } - actualV2, err := FromV3Swagger(&swagger3) + spec2, err := FromV3Swagger(&doc3) require.NoError(t, err) - data, err := json.Marshal(actualV2) + data, err := json.Marshal(spec2) require.NoError(t, err) require.JSONEq(t, exampleV2, string(data)) } func TestConvOpenAPIV2ToV3(t *testing.T) { - var swagger2 openapi2.Swagger - err := json.Unmarshal([]byte(exampleV2), &swagger2) + var doc2 openapi2.Swagger + err := json.Unmarshal([]byte(exampleV2), &doc2) require.NoError(t, err) - actualV3, err := ToV3Swagger(&swagger2) + spec3, err := ToV3Swagger(&doc2) require.NoError(t, err) - data, err := json.Marshal(actualV3) + err = spec3.Validate(context.Background()) + require.NoError(t, err) + data, err := json.Marshal(spec3) require.NoError(t, err) require.JSONEq(t, exampleV3, string(data)) } const exampleV2 = ` { - "x-root": "root extension 1", - "x-root2": "root extension 2", - "info": {"title":"MyAPI","version":"0.1","x-info":"info extension"}, - "schemes": ["https"], - "host": "test.example.com", + "swagger": "2.0", "basePath": "/v2", + "definitions": { + "Error": { + "description": "Error response.", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "Item": { + "additionalProperties": true, + "properties": { + "foo": { + "type": "string" + }, + "quux": { + "$ref": "#/definitions/ItemExtension" + } + }, + "type": "object" + }, + "ItemExtension": { + "description": "It could be anything.", + "type": "boolean" + } + }, "externalDocs": { - "url": "https://example/doc/", - "description": "Example Documentation" + "description": "Example Documentation", + "url": "https://example/doc/" }, - "tags": [ - { - "name": "Example", - "description": "An example tag." + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1", + "x-info": "info extension" + }, + "parameters": { + "banana": { + "in": "path", + "name": "banana", + "required": true, + "type": "string" } - ], + }, "paths": { "/another/{banana}/{id}": { - "parameters": [ - { - "$ref": "#/parameters/banana" - }, - { - "in": "path", - "name": "id", - "type": "integer", - "required": true - } - ] + "parameters": [ + { + "$ref": "#/parameters/banana" + }, + { + "in": "path", + "name": "id", + "required": true, + "type": "integer" + } + ] }, "/example": { - "x-path": "path extension 1", - "x-path2": "path extension 2", - "delete": { - "x-operation": "operation extension 1", - "description": "example delete", + "get": { + "description": "example get", "responses": { - "default": { - "description": "default response" - }, "403": { "$ref": "#/responses/ForbiddenError" }, "404": { "description": "404 response" + }, + "default": { + "description": "default response" } - } + }, + "x-operation": "operation extension 1" }, - "get": { - "operationId": "example-get", - "summary": "example get", - "description": "example get", - "tags": [ - "Example" - ], + "delete": { + "description": "example delete", + "operationId": "example-delete", "parameters": [ { - "x-parameter": "parameter extension 1", "in": "query", - "name": "x" + "name": "x", + "type": "string", + "x-parameter": "parameter extension 1" }, { - "in": "query", - "name": "y", + "default": 250, "description": "The y parameter", - "type": "integer", - "minimum": 1, + "in": "query", "maximum": 10000, - "default": 250 + "minimum": 1, + "name": "y", + "type": "integer" }, { - "in": "query", - "name": "bbox", "description": "Only return results that intersect the provided bounding box.", - "maxItems": 4, - "minItems": 4, - "type": "array", + "in": "query", "items": { "type": "number" - } - }, - { - "in": "body", - "x-requestBody": "requestbody extension 1", - "name": "body", - "schema": {} + }, + "maxItems": 4, + "minItems": 4, + "name": "bbox", + "type": "array" } ], "responses": { "200": { "description": "ok", "schema": { - "type": "array", "items": { "$ref": "#/definitions/Item" - } + }, + "type": "array" } }, - "default": { - "x-response": "response extension 1", - "description": "default response" - }, "404": { "description": "404 response" + }, + "default": { + "description": "default response", + "x-response": "response extension 1" } }, "security": [ @@ -149,43 +183,86 @@ const exampleV2 = ` ], "get_security_1": [] } + ], + "summary": "example get", + "tags": [ + "Example" ] }, "head": { "description": "example head", - "responses": {} + "responses": { + "default": { + "description": "default response" + } + } + }, + "options": { + "description": "example options", + "responses": { + "default": { + "description": "default response" + } + } }, "patch": { "description": "example patch", - "responses": {} + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "allOf": [{"$ref": "#/definitions/Item"}] + }, + "x-requestBody": "requestbody extension 1" + } + ], + "responses": { + "default": { + "description": "default response" + } + } }, "post": { + "consumes": ["multipart/form-data"], "description": "example post", - "responses": {}, "parameters": [ { + "description": "File Id", + "in": "query", + "name": "id", + "type": "integer" + }, + { + "description": "param description", "in": "formData", "name": "fileUpload", "type": "file", - "description": "param description", "x-mimetype": "text/plain" }, { + "description": "Description of file contents", "in": "formData", - "name":"note", - "type": "integer", - "description": "Description of file contents" + "name": "note", + "type": "integer" } - ] + ], + "responses": { + "default": { + "description": "default response" + } + } }, "put": { "description": "example put", - "responses": {} + "responses": { + "default": { + "description": "default response" + } + } }, - "options": { - "description": "example options", - "responses": {} - } + "x-path": "path extension 1", + "x-path2": "path extension 2" } }, "responses": { @@ -196,40 +273,9 @@ const exampleV2 = ` } } }, - "definitions": { - "Item": { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - }, - "additionalProperties": { - "$ref": "#/definitions/ItemExtension" - } - }, - "ItemExtension": { - "description": "It could be anything." - }, - "Error": { - "description": "Error response.", - "type": "object", - "required": [ - "message" - ], - "properties": { - "message": { - "type": "string" - } - } - } - }, - "parameters": { - "banana": { - "in": "path", - "type": "string" - } - }, + "schemes": [ + "https" + ], "security": [ { "default_security_0": [ @@ -238,21 +284,31 @@ const exampleV2 = ` ], "default_security_1": [] } - ] + ], + "tags": [ + { + "description": "An example tag.", + "name": "Example" + } + ], + "x-root": "root extension 1", + "x-root2": "root extension 2" } ` const exampleV3 = ` { - "x-root": "root extension 1", - "x-root2": "root extension 2", - "openapi": "3.0.2", - "info": {"title":"MyAPI","version":"0.1","x-info":"info extension"}, - "externalDocs": { - "url": "https://example/doc/", - "description": "Example Documentation" - }, "components": { + "parameters": { + "banana": { + "in": "path", + "name": "banana", + "required": true, + "schema": { + "type": "string" + } + } + }, "responses": { "ForbiddenError": { "content": { @@ -265,29 +321,7 @@ const exampleV3 = ` "description": "Insufficient permission to perform the requested action." } }, - "parameters": { - "banana": { - "in": "path", - "schema": { - "type": "string" - } - } - }, "schemas": { - "Item": { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/ItemExtension" - } - }, - "ItemExtension": { - "description": "It could be anything." - }, "Error": { "description": "Error response.", "properties": { @@ -299,66 +333,76 @@ const exampleV3 = ` "message" ], "type": "object" + }, + "Item": { + "additionalProperties": true, + "properties": { + "foo": { + "type": "string" + }, + "quux": { + "$ref": "#/components/schemas/ItemExtension" + } + }, + "type": "object" + }, + "ItemExtension": { + "type": "boolean", + "description": "It could be anything." } } }, - "tags": [ - { - "name": "Example", - "description": "An example tag." - } - ], - "servers": [ - { - "url": "https://test.example.com/v2" - } - ], + "externalDocs": { + "description": "Example Documentation", + "url": "https://example/doc/" + }, + "info": { + "title": "MyAPI", + "version": "0.1", + "x-info": "info extension" + }, + "openapi": "3.0.3", "paths": { "/another/{banana}/{id}": { - "parameters": [ - { - "$ref": "#/components/parameters/banana" - }, - { - "in": "path", - "name": "id", - "schema": { - "type": "integer" - }, - "required": true + "parameters": [ + { + "$ref": "#/components/parameters/banana" + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" } - ] + } + ] }, "/example": { - "x-path": "path extension 1", - "x-path2": "path extension 2", - "delete": { - "x-operation": "operation extension 1", - "description": "example delete", + "get": { + "description": "example get", "responses": { - "default": { - "description": "default response" - }, "403": { "$ref": "#/components/responses/ForbiddenError" }, "404": { "description": "404 response" + }, + "default": { + "description": "default response" } - } + }, + "x-operation": "operation extension 1" }, - "get": { - "operationId": "example-get", - "summary": "example get", - "description": "example get", - "tags": [ - "Example" - ], + "delete": { + "description": "example delete", + "operationId": "example-delete", "parameters": [ { - "x-parameter": "parameter extension 1", "in": "query", - "name": "x" + "name": "x", + "schema": {"type": "string"}, + "x-parameter": "parameter extension 1" }, { "description": "The y parameter", @@ -376,26 +420,17 @@ const exampleV3 = ` "in": "query", "name": "bbox", "schema": { - "type": "array", "items": { "type": "number" }, + "maxItems": 4, "minItems": 4, - "maxItems": 4 + "type": "array" } } ], - "requestBody": { - "x-requestBody": "requestbody extension 1", - "content": { - "application/json": { - "schema": {} - } - } - }, "responses": { "200": { - "description": "ok", "content": { "application/json": { "schema": { @@ -405,14 +440,15 @@ const exampleV3 = ` "type": "array" } } - } - }, - "default": { - "x-response": "response extension 1", - "description": "default response" + }, + "description": "ok" }, "404": { "description": "404 response" + }, + "default": { + "description": "default response", + "x-response": "response extension 1" } }, "security": [ @@ -423,23 +459,58 @@ const exampleV3 = ` ], "get_security_1": [] } + ], + "summary": "example get", + "tags": [ + "Example" ] }, "head": { "description": "example head", - "responses": {} + "responses": { + "default": { + "description": "default response" + } + } }, "options": { "description": "example options", - "responses": {} + "responses": { + "default": { + "description": "default response" + } + } }, "patch": { "description": "example patch", - "responses": {} + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}] + } + } + }, + "x-requestBody": "requestbody extension 1" + }, + "responses": { + "default": { + "description": "default response" + } + } }, "post": { "description": "example post", - "responses": {}, + "parameters": [ + { + "description": "File Id", + "in": "query", + "name": "id", + "schema": { + "type": "integer" + } + } + ], "requestBody": { "content": { "multipart/form-data": { @@ -447,25 +518,36 @@ const exampleV3 = ` "properties": { "fileUpload": { "description": "param description", - "format": "binary", "type": "string", + "format": "binary", "x-mimetype": "text/plain" }, - "note":{ - "type": "integer", - "description": "Description of file contents" + "note": { + "description": "Description of file contents", + "type": "integer" } }, "type": "object" } } } + }, + "responses": { + "default": { + "description": "default response" + } } }, "put": { "description": "example put", - "responses": {} - } + "responses": { + "default": { + "description": "default response" + } + } + }, + "x-path": "path extension 1", + "x-path2": "path extension 2" } }, "security": [ @@ -476,6 +558,19 @@ const exampleV3 = ` ], "default_security_1": [] } - ] + ], + "servers": [ + { + "url": "https://test.example.com/v2" + } + ], + "tags": [ + { + "description": "An example tag.", + "name": "Example" + } + ], + "x-root": "root extension 1", + "x-root2": "root extension 2" } ` diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 3b8be81c7..942ecd9e0 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -24,9 +24,7 @@ func (mediaType *MediaType) WithSchema(schema *Schema) *MediaType { if schema == nil { mediaType.Schema = nil } else { - mediaType.Schema = &SchemaRef{ - Value: schema, - } + mediaType.Schema = &SchemaRef{Value: schema} } return mediaType } diff --git a/openapi3/schema.go b/openapi3/schema.go index 427c376a4..fba9e342a 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -73,10 +73,11 @@ type Schema struct { ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` // Properties - Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` - ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` - WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` - XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"` + Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` + ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"` // Number Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` @@ -395,7 +396,7 @@ func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema { func (schema *Schema) IsEmpty() bool { if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 || schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax || - !schema.Nullable || + schema.Nullable || schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil || schema.MinLength != 0 || schema.MaxLength != nil || schema.Pattern != "" || schema.MinItems != 0 || schema.MaxItems != nil || From 2392e46f8ad6050d797390cb1859623f0994f710 Mon Sep 17 00:00:00 2001 From: Tevic Date: Fri, 7 Aug 2020 03:22:24 +0800 Subject: [PATCH 015/260] Add deprecated field in Schema (#242) Change-Id: If750ff340ae29cf24a6ad870071502c9327485ca --- openapi3/schema.go | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi3/schema.go b/openapi3/schema.go index fba9e342a..edb35c40c 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -78,6 +78,7 @@ type Schema struct { WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` // Number Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` From ceae068a70e7ef3d55351ab14a261e8b9802ce46 Mon Sep 17 00:00:00 2001 From: Kaushal Madappa Date: Fri, 4 Sep 2020 14:48:00 +0530 Subject: [PATCH 016/260] Fix openapi3.referencedDocumentPath (#248) --- openapi3/swagger_loader.go | 23 +++---- ...er_loader_referenced_document_path_test.go | 60 +++++++++++++++++++ 2 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 openapi3/swagger_loader_referenced_document_path_test.go diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 71216a28a..aae028065 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -876,16 +876,19 @@ func unescapeRefString(ref string) string { } func referencedDocumentPath(documentPath *url.URL, ref string) (*url.URL, error) { - newDocumentPath := documentPath - if documentPath != nil { - refDirectory, err := url.Parse(path.Dir(ref)) - if err != nil { - return nil, err - } - joinedDirectory := path.Join(path.Dir(documentPath.String()), refDirectory.String()) - if newDocumentPath, err = url.Parse(joinedDirectory + "/"); err != nil { - return nil, err - } + if documentPath == nil { + return nil, nil + } + + newDocumentPath, err := copyURL(documentPath) + if err != nil { + return nil, err } + refPath, err := url.Parse(ref) + if err != nil { + return nil, err + } + newDocumentPath.Path = path.Join(path.Dir(newDocumentPath.Path), path.Dir(refPath.Path)) + "/" + return newDocumentPath, nil } diff --git a/openapi3/swagger_loader_referenced_document_path_test.go b/openapi3/swagger_loader_referenced_document_path_test.go new file mode 100644 index 000000000..de219c369 --- /dev/null +++ b/openapi3/swagger_loader_referenced_document_path_test.go @@ -0,0 +1,60 @@ +package openapi3 + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestReferencedDocumentPath(t *testing.T) { + httpURL, err := url.Parse("http://example.com/path/to/schemas/test1.yaml") + require.NoError(t, err) + + fileURL, err := url.Parse("path/to/schemas/test1.yaml") + require.NoError(t, err) + + refEmpty := "" + refNoComponent := "moreschemas/test2.yaml" + refWithComponent := "moreschemas/test2.yaml#/components/schemas/someobject" + + for _, test := range []struct { + path *url.URL + ref, expected string + }{ + { + path: httpURL, + ref: refEmpty, + expected: "http://example.com/path/to/schemas/", + }, + { + path: httpURL, + ref: refNoComponent, + expected: "http://example.com/path/to/schemas/moreschemas/", + }, + { + path: httpURL, + ref: refWithComponent, + expected: "http://example.com/path/to/schemas/moreschemas/", + }, + { + path: fileURL, + ref: refEmpty, + expected: "path/to/schemas/", + }, + { + path: fileURL, + ref: refNoComponent, + expected: "path/to/schemas/moreschemas/", + }, + { + path: fileURL, + ref: refWithComponent, + expected: "path/to/schemas/moreschemas/", + }, + } { + result, err := referencedDocumentPath(test.path, test.ref) + require.NoError(t, err) + require.Equal(t, test.expected, result.String()) + } +} From 5f67f433f1c41577a27e893ba66c7a6d1a709207 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sun, 6 Sep 2020 16:08:24 +0200 Subject: [PATCH 017/260] follow lint rules (#250) --- README.md | 6 +- jsoninfo/unmarshal_test.go | 33 ++- openapi3/discriminator_test.go | 5 +- openapi3/encoding_test.go | 37 ++- openapi3/example_test.go | 9 +- openapi3/extension_test.go | 14 +- openapi3/media_type_test.go | 21 +- openapi3/schema.go | 3 + openapi3/schema_test.go | 181 ++++++------- openapi3/security_scheme_test.go | 7 +- openapi3/server.go | 3 +- openapi3/server_test.go | 21 +- openapi3/swagger_loader.go | 44 ++-- ..._loader_empty_response_description_test.go | 13 +- openapi3/swagger_loader_paths_test.go | 5 +- openapi3/swagger_loader_relative_refs_test.go | 78 +++--- openapi3/swagger_loader_test.go | 47 ++-- openapi3/swagger_test.go | 244 ++++++------------ openapi3/unique_items_checker_test.go | 36 +++ openapi3filter/validation_test.go | 81 +++--- openapi3gen/openapi3gen_test.go | 9 +- 21 files changed, 398 insertions(+), 499 deletions(-) create mode 100644 openapi3/unique_items_checker_test.go diff --git a/README.md b/README.md index fbd0f0873..d6fe16a4d 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ import ( func main() { router := openapi3filter.NewRouter().WithSwaggerFromFile("swagger.json") - ctx := context.TODO() + ctx := context.Background() httpReq, _ := http.NewRequest(http.MethodGet, "/items", nil) // Find route @@ -105,9 +105,7 @@ func main() { RequestValidationInput: requestValidationInput, Status: respStatus, Header: http.Header{ - "Content-Type": []string{ - respContentType, - }, + "Content-Type": []string{respContentType}, }, } if respBody != nil { diff --git a/jsoninfo/unmarshal_test.go b/jsoninfo/unmarshal_test.go index ce448a5fb..77ab42bb3 100644 --- a/jsoninfo/unmarshal_test.go +++ b/jsoninfo/unmarshal_test.go @@ -1,10 +1,9 @@ -package jsoninfo_test +package jsoninfo import ( "errors" "testing" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/stretchr/testify/assert" ) @@ -16,7 +15,7 @@ func TestNewObjectDecoder(t *testing.T) { } `) t.Run("test new object decoder", func(t *testing.T) { - decoder, err := jsoninfo.NewObjectDecoder(data) + decoder, err := NewObjectDecoder(data) assert.Nil(t, err) assert.NotNil(t, decoder) assert.Equal(t, data, decoder.Data) @@ -25,15 +24,15 @@ func TestNewObjectDecoder(t *testing.T) { } type mockStrictStruct struct { - EncodeWithFn func(encoder *jsoninfo.ObjectEncoder, value interface{}) error - DecodeWithFn func(decoder *jsoninfo.ObjectDecoder, value interface{}) error + EncodeWithFn func(encoder *ObjectEncoder, value interface{}) error + DecodeWithFn func(decoder *ObjectDecoder, value interface{}) error } -func (m *mockStrictStruct) EncodeWith(encoder *jsoninfo.ObjectEncoder, value interface{}) error { +func (m *mockStrictStruct) EncodeWith(encoder *ObjectEncoder, value interface{}) error { return m.EncodeWithFn(encoder, value) } -func (m *mockStrictStruct) DecodeWith(decoder *jsoninfo.ObjectDecoder, value interface{}) error { +func (m *mockStrictStruct) DecodeWith(decoder *ObjectDecoder, value interface{}) error { return m.DecodeWithFn(decoder, value) } @@ -48,15 +47,15 @@ func TestUnmarshalStrictStruct(t *testing.T) { t.Run("test unmarshal with StrictStruct without err", func(t *testing.T) { decodeWithFnCalled := 0 mockStruct := &mockStrictStruct{ - EncodeWithFn: func(encoder *jsoninfo.ObjectEncoder, value interface{}) error { + EncodeWithFn: func(encoder *ObjectEncoder, value interface{}) error { return nil }, - DecodeWithFn: func(decoder *jsoninfo.ObjectDecoder, value interface{}) error { + DecodeWithFn: func(decoder *ObjectDecoder, value interface{}) error { decodeWithFnCalled++ return nil }, } - err := jsoninfo.UnmarshalStrictStruct(data, mockStruct) + err := UnmarshalStrictStruct(data, mockStruct) assert.Nil(t, err) assert.Equal(t, 1, decodeWithFnCalled) }) @@ -64,15 +63,15 @@ func TestUnmarshalStrictStruct(t *testing.T) { t.Run("test unmarshal with StrictStruct with err", func(t *testing.T) { decodeWithFnCalled := 0 mockStruct := &mockStrictStruct{ - EncodeWithFn: func(encoder *jsoninfo.ObjectEncoder, value interface{}) error { + EncodeWithFn: func(encoder *ObjectEncoder, value interface{}) error { return nil }, - DecodeWithFn: func(decoder *jsoninfo.ObjectDecoder, value interface{}) error { + DecodeWithFn: func(decoder *ObjectDecoder, value interface{}) error { decodeWithFnCalled++ return errors.New("unable to decode the value") }, } - err := jsoninfo.UnmarshalStrictStruct(data, mockStruct) + err := UnmarshalStrictStruct(data, mockStruct) assert.NotNil(t, err) assert.Equal(t, 1, decodeWithFnCalled) }) @@ -85,7 +84,7 @@ func TestDecodeStructFieldsAndExtensions(t *testing.T) { "field2": "field2" } `) - decoder, err := jsoninfo.NewObjectDecoder(data) + decoder, err := NewObjectDecoder(data) assert.Nil(t, err) assert.NotNil(t, decoder) @@ -111,7 +110,7 @@ func TestDecodeStructFieldsAndExtensions(t *testing.T) { }) t.Run("successfully decoded with all fields", func(t *testing.T) { - d, err := jsoninfo.NewObjectDecoder(data) + d, err := NewObjectDecoder(data) assert.Nil(t, err) assert.NotNil(t, d) @@ -127,7 +126,7 @@ func TestDecodeStructFieldsAndExtensions(t *testing.T) { }) t.Run("successfully decoded with renaming field", func(t *testing.T) { - d, err := jsoninfo.NewObjectDecoder(data) + d, err := NewObjectDecoder(data) assert.Nil(t, err) assert.NotNil(t, d) @@ -141,7 +140,7 @@ func TestDecodeStructFieldsAndExtensions(t *testing.T) { }) t.Run("un-successfully decoded due to data mismatch", func(t *testing.T) { - d, err := jsoninfo.NewObjectDecoder(data) + d, err := NewObjectDecoder(data) assert.Nil(t, err) assert.NotNil(t, d) diff --git a/openapi3/discriminator_test.go b/openapi3/discriminator_test.go index c12227141..602b4fd68 100644 --- a/openapi3/discriminator_test.go +++ b/openapi3/discriminator_test.go @@ -1,9 +1,8 @@ -package openapi3_test +package openapi3 import ( "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -37,7 +36,7 @@ var jsonSpecWithDiscriminator = []byte(` `) func TestParsingDiscriminator(t *testing.T) { - loader, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(jsonSpecWithDiscriminator) + loader, err := NewSwaggerLoader().LoadSwaggerFromData(jsonSpecWithDiscriminator) require.NoError(t, err) require.Equal(t, 2, len(loader.Components.Schemas["MyResponseType"].Value.OneOf)) } diff --git a/openapi3/encoding_test.go b/openapi3/encoding_test.go index 67e7b6b65..5c354540d 100644 --- a/openapi3/encoding_test.go +++ b/openapi3/encoding_test.go @@ -1,4 +1,4 @@ -package openapi3_test +package openapi3 import ( "context" @@ -6,7 +6,6 @@ import ( "reflect" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -17,13 +16,13 @@ func TestEncodingJSON(t *testing.T) { require.NotEmpty(t, data) t.Log("Unmarshal *openapi3.Encoding from JSON") - docA := &openapi3.Encoding{} + docA := &Encoding{} err = json.Unmarshal(encodingJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Validate *openapi3.Encoding") - err = docA.Validate(context.TODO()) + err = docA.Validate(context.Background()) require.NoError(t, err) t.Log("Ensure representations match") @@ -45,13 +44,13 @@ var encodingJSON = []byte(` } `) -func encoding() *openapi3.Encoding { +func encoding() *Encoding { explode := true - return &openapi3.Encoding{ + return &Encoding{ ContentType: "application/json", - Headers: map[string]*openapi3.HeaderRef{ + Headers: map[string]*HeaderRef{ "someHeader": { - Value: &openapi3.Header{}, + Value: &Header{}, }, }, Style: "form", @@ -64,32 +63,32 @@ func TestEncodingSerializationMethod(t *testing.T) { boolPtr := func(b bool) *bool { return &b } testCases := []struct { name string - enc *openapi3.Encoding - want *openapi3.SerializationMethod + enc *Encoding + want *SerializationMethod }{ { name: "default", - want: &openapi3.SerializationMethod{Style: openapi3.SerializationForm, Explode: true}, + want: &SerializationMethod{Style: SerializationForm, Explode: true}, }, { name: "encoding with style", - enc: &openapi3.Encoding{Style: openapi3.SerializationSpaceDelimited}, - want: &openapi3.SerializationMethod{Style: openapi3.SerializationSpaceDelimited, Explode: true}, + enc: &Encoding{Style: SerializationSpaceDelimited}, + want: &SerializationMethod{Style: SerializationSpaceDelimited, Explode: true}, }, { name: "encoding with explode", - enc: &openapi3.Encoding{Explode: boolPtr(true)}, - want: &openapi3.SerializationMethod{Style: openapi3.SerializationForm, Explode: true}, + enc: &Encoding{Explode: boolPtr(true)}, + want: &SerializationMethod{Style: SerializationForm, Explode: true}, }, { name: "encoding with no explode", - enc: &openapi3.Encoding{Explode: boolPtr(false)}, - want: &openapi3.SerializationMethod{Style: openapi3.SerializationForm, Explode: false}, + enc: &Encoding{Explode: boolPtr(false)}, + want: &SerializationMethod{Style: SerializationForm, Explode: false}, }, { name: "encoding with style and explode ", - enc: &openapi3.Encoding{Style: openapi3.SerializationSpaceDelimited, Explode: boolPtr(false)}, - want: &openapi3.SerializationMethod{Style: openapi3.SerializationSpaceDelimited, Explode: false}, + enc: &Encoding{Style: SerializationSpaceDelimited, Explode: boolPtr(false)}, + want: &SerializationMethod{Style: SerializationSpaceDelimited, Explode: false}, }, } for _, tc := range testCases { diff --git a/openapi3/example_test.go b/openapi3/example_test.go index a5dfb3008..4e9296ac0 100644 --- a/openapi3/example_test.go +++ b/openapi3/example_test.go @@ -1,10 +1,9 @@ -package openapi3_test +package openapi3 import ( "encoding/json" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -15,7 +14,7 @@ func TestExampleJSON(t *testing.T) { require.NotEmpty(t, data) t.Log("Unmarshal *openapi3.Example from JSON") - docA := &openapi3.Example{} + docA := &Example{} err = json.Unmarshal(exampleJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) @@ -40,7 +39,7 @@ var exampleJSON = []byte(` } `) -func example() *openapi3.Example { +func example() *Example { value := map[string]string{ "name": "Fluffy", "petType": "Cat", @@ -48,7 +47,7 @@ func example() *openapi3.Example { "gender": "male", "breed": "Persian", } - return &openapi3.Example{ + return &Example{ Summary: "An example of a cat", Value: value, } diff --git a/openapi3/extension_test.go b/openapi3/extension_test.go index 775d8b6bc..3d0b233da 100644 --- a/openapi3/extension_test.go +++ b/openapi3/extension_test.go @@ -1,16 +1,16 @@ -package openapi3_test +package openapi3 import ( + "testing" + "github.com/getkin/kin-openapi/jsoninfo" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/assert" - "testing" ) func TestExtensionProps_EncodeWith(t *testing.T) { t.Run("successfully encoded", func(t *testing.T) { encoder := jsoninfo.NewObjectEncoder() - var extensionProps = openapi3.ExtensionProps{ + var extensionProps = ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", }, @@ -36,7 +36,7 @@ func TestExtensionProps_DecodeWith(t *testing.T) { t.Run("successfully decode all the fields", func(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) assert.Nil(t, err) - var extensionProps = &openapi3.ExtensionProps{ + var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", "field2": "value1", @@ -58,7 +58,7 @@ func TestExtensionProps_DecodeWith(t *testing.T) { t.Run("successfully decode some of the fields", func(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) assert.Nil(t, err) - var extensionProps = &openapi3.ExtensionProps{ + var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", "field2": "value2", @@ -79,7 +79,7 @@ func TestExtensionProps_DecodeWith(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) assert.Nil(t, err) - var extensionProps = &openapi3.ExtensionProps{ + var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", "field2": "value2", diff --git a/openapi3/media_type_test.go b/openapi3/media_type_test.go index 9d5092802..099c4b667 100644 --- a/openapi3/media_type_test.go +++ b/openapi3/media_type_test.go @@ -1,11 +1,10 @@ -package openapi3_test +package openapi3 import ( "context" "encoding/json" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -16,13 +15,13 @@ func TestMediaTypeJSON(t *testing.T) { require.NotEmpty(t, data) t.Log("Unmarshal *openapi3.MediaType from JSON") - docA := &openapi3.MediaType{} + docA := &MediaType{} err = json.Unmarshal(mediaTypeJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Validate *openapi3.MediaType") - err = docA.Validate(context.TODO()) + err = docA.Validate(context.Background()) require.NoError(t, err) t.Log("Ensure representations match") @@ -52,22 +51,22 @@ var mediaTypeJSON = []byte(` } `) -func mediaType() *openapi3.MediaType { +func mediaType() *MediaType { example := map[string]string{"name": "Some example"} - return &openapi3.MediaType{ - Schema: &openapi3.SchemaRef{ - Value: &openapi3.Schema{ + return &MediaType{ + Schema: &SchemaRef{ + Value: &Schema{ Description: "Some schema", }, }, - Encoding: map[string]*openapi3.Encoding{ + Encoding: map[string]*Encoding{ "someEncoding": { ContentType: "application/xml; charset=utf-8", }, }, - Examples: map[string]*openapi3.ExampleRef{ + Examples: map[string]*ExampleRef{ "someExample": { - Value: openapi3.NewExample(example), + Value: NewExample(example), }, }, } diff --git a/openapi3/schema.go b/openapi3/schema.go index edb35c40c..8a34282ff 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -995,6 +995,9 @@ func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error) } // "uniqueItems" + if sliceUniqueItemsChecker == nil { + sliceUniqueItemsChecker = isSliceOfUniqueItems + } if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) { if fast { return errSchema diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index e82aba26b..10e1d0589 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1,4 +1,4 @@ -package openapi3_test +package openapi3 import ( "context" @@ -8,20 +8,19 @@ import ( "strings" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) type schemaExample struct { Title string - Schema *openapi3.Schema + Schema *Schema Serialization interface{} AllValid []interface{} AllInvalid []interface{} } func TestSchemas(t *testing.T) { - openapi3.DefineStringFormat("uuid", openapi3.FormatOfStringForUUIDOfRFC4122) + DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) for _, example := range schemaExamples { t.Run(example.Title, testSchema(t, example)) } @@ -36,10 +35,10 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) { jsonSchema, err := json.Marshal(schema) require.NoError(t, err) require.JSONEq(t, string(jsonSerialized), string(jsonSchema)) - var dataUnserialized openapi3.Schema + var dataUnserialized Schema err = json.Unmarshal(jsonSerialized, &dataUnserialized) require.NoError(t, err) - var dataSchema openapi3.Schema + var dataSchema Schema err = json.Unmarshal(jsonSchema, &dataSchema) require.NoError(t, err) require.Equal(t, dataUnserialized, dataSchema) @@ -60,7 +59,7 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) { } } -func validateSchema(t *testing.T, schema *openapi3.Schema, value interface{}) error { +func validateSchema(t *testing.T, schema *Schema, value interface{}) error { data, err := json.Marshal(value) require.NoError(t, err) var val interface{} @@ -72,7 +71,7 @@ func validateSchema(t *testing.T, schema *openapi3.Schema, value interface{}) er var schemaExamples = []schemaExample{ { Title: "EMPTY SCHEMA", - Schema: &openapi3.Schema{}, + Schema: &Schema{}, Serialization: map[string]interface{}{ // This OA3 schema is exactly this draft-04 schema: // {"not": {"type": "null"}} @@ -92,7 +91,7 @@ var schemaExamples = []schemaExample{ { Title: "JUST NULLABLE", - Schema: openapi3.NewSchema().WithNullable(), + Schema: NewSchema().WithNullable(), Serialization: map[string]interface{}{ // This OA3 schema is exactly both this draft-04 schema: {} and: // {anyOf: [type:string, type:number, type:integer, type:boolean @@ -114,7 +113,7 @@ var schemaExamples = []schemaExample{ { Title: "NULLABLE BOOLEAN", - Schema: openapi3.NewBoolSchema().WithNullable(), + Schema: NewBoolSchema().WithNullable(), Serialization: map[string]interface{}{ "nullable": true, "type": "boolean", @@ -136,9 +135,9 @@ var schemaExamples = []schemaExample{ { Title: "NULLABLE ANYOF", - Schema: openapi3.NewAnyOfSchema( - openapi3.NewIntegerSchema(), - openapi3.NewFloat64Schema(), + Schema: NewAnyOfSchema( + NewIntegerSchema(), + NewFloat64Schema(), ).WithNullable(), Serialization: map[string]interface{}{ "nullable": true, @@ -162,7 +161,7 @@ var schemaExamples = []schemaExample{ { Title: "BOOLEAN", - Schema: openapi3.NewBoolSchema(), + Schema: NewBoolSchema(), Serialization: map[string]interface{}{ "type": "boolean", }, @@ -181,7 +180,7 @@ var schemaExamples = []schemaExample{ { Title: "NUMBER", - Schema: openapi3.NewFloat64Schema(). + Schema: NewFloat64Schema(). WithMin(2.5). WithMax(3.5), Serialization: map[string]interface{}{ @@ -208,7 +207,7 @@ var schemaExamples = []schemaExample{ { Title: "INTEGER", - Schema: openapi3.NewInt64Schema(). + Schema: NewInt64Schema(). WithMin(2). WithMax(5), Serialization: map[string]interface{}{ @@ -236,7 +235,7 @@ var schemaExamples = []schemaExample{ { Title: "STRING", - Schema: openapi3.NewStringSchema(). + Schema: NewStringSchema(). WithMinLength(2). WithMaxLength(3). WithPattern("^[abc]+$"), @@ -265,7 +264,7 @@ var schemaExamples = []schemaExample{ { Title: "STRING: optional format 'uuid'", - Schema: openapi3.NewUUIDSchema(), + Schema: NewUUIDSchema(), Serialization: map[string]interface{}{ "type": "string", "format": "uuid", @@ -286,7 +285,7 @@ var schemaExamples = []schemaExample{ { Title: "STRING: format 'date-time'", - Schema: openapi3.NewDateTimeSchema(), + Schema: NewDateTimeSchema(), Serialization: map[string]interface{}{ "type": "string", "format": "date-time", @@ -311,7 +310,7 @@ var schemaExamples = []schemaExample{ { Title: "STRING: format 'date-time'", - Schema: openapi3.NewBytesSchema(), + Schema: NewBytesSchema(), Serialization: map[string]interface{}{ "type": "string", "format": "byte", @@ -343,12 +342,12 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY", - Schema: &openapi3.Schema{ + Schema: &Schema{ Type: "array", MinItems: 2, - MaxItems: openapi3.Uint64Ptr(3), + MaxItems: Uint64Ptr(3), UniqueItems: true, - Items: openapi3.NewFloat64Schema().NewRef(), + Items: NewFloat64Schema().NewRef(), }, Serialization: map[string]interface{}{ "type": "array", @@ -383,13 +382,13 @@ var schemaExamples = []schemaExample{ }, { Title: "ARRAY : items format 'object'", - Schema: &openapi3.Schema{ + Schema: &Schema{ Type: "array", UniqueItems: true, - Items: (&openapi3.Schema{ + Items: (&Schema{ Type: "object", - Properties: map[string]*openapi3.SchemaRef{ - "key1": openapi3.NewFloat64Schema().NewRef(), + Properties: map[string]*SchemaRef{ + "key1": NewFloat64Schema().NewRef(), }, }).NewRef(), }, @@ -440,16 +439,16 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY : items format 'object' and object with a property of array type ", - Schema: &openapi3.Schema{ + Schema: &Schema{ Type: "array", UniqueItems: true, - Items: (&openapi3.Schema{ + Items: (&Schema{ Type: "object", - Properties: map[string]*openapi3.SchemaRef{ - "key1": (&openapi3.Schema{ + Properties: map[string]*SchemaRef{ + "key1": (&Schema{ Type: "array", UniqueItems: true, - Items: openapi3.NewFloat64Schema().NewRef(), + Items: NewFloat64Schema().NewRef(), }).NewRef(), }, }).NewRef(), @@ -526,13 +525,13 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY : items format 'array'", - Schema: &openapi3.Schema{ + Schema: &Schema{ Type: "array", UniqueItems: true, - Items: (&openapi3.Schema{ + Items: (&Schema{ Type: "array", UniqueItems: true, - Items: openapi3.NewFloat64Schema().NewRef(), + Items: NewFloat64Schema().NewRef(), }).NewRef(), }, Serialization: map[string]interface{}{ @@ -570,16 +569,16 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY : items format 'array' and array with object type items", - Schema: &openapi3.Schema{ + Schema: &Schema{ Type: "array", UniqueItems: true, - Items: (&openapi3.Schema{ + Items: (&Schema{ Type: "array", UniqueItems: true, - Items: (&openapi3.Schema{ + Items: (&Schema{ Type: "object", - Properties: map[string]*openapi3.SchemaRef{ - "key1": openapi3.NewFloat64Schema().NewRef(), + Properties: map[string]*SchemaRef{ + "key1": NewFloat64Schema().NewRef(), }, }).NewRef(), }).NewRef(), @@ -674,11 +673,11 @@ var schemaExamples = []schemaExample{ { Title: "OBJECT", - Schema: &openapi3.Schema{ + Schema: &Schema{ Type: "object", - MaxProps: openapi3.Uint64Ptr(2), - Properties: map[string]*openapi3.SchemaRef{ - "numberProperty": openapi3.NewFloat64Schema().NewRef(), + MaxProps: Uint64Ptr(2), + Properties: map[string]*SchemaRef{ + "numberProperty": NewFloat64Schema().NewRef(), }, }, Serialization: map[string]interface{}{ @@ -718,10 +717,10 @@ var schemaExamples = []schemaExample{ }, }, { - Schema: &openapi3.Schema{ + Schema: &Schema{ Type: "object", - AdditionalProperties: &openapi3.SchemaRef{ - Value: &openapi3.Schema{ + AdditionalProperties: &SchemaRef{ + Value: &Schema{ Type: "number", }, }, @@ -746,9 +745,9 @@ var schemaExamples = []schemaExample{ }, }, { - Schema: &openapi3.Schema{ + Schema: &Schema{ Type: "object", - AdditionalPropertiesAllowed: openapi3.BoolPtr(true), + AdditionalPropertiesAllowed: BoolPtr(true), }, Serialization: map[string]interface{}{ "type": "object", @@ -765,9 +764,9 @@ var schemaExamples = []schemaExample{ { Title: "NOT", - Schema: &openapi3.Schema{ - Not: &openapi3.SchemaRef{ - Value: &openapi3.Schema{ + Schema: &Schema{ + Not: &SchemaRef{ + Value: &Schema{ Enum: []interface{}{ nil, true, @@ -802,15 +801,15 @@ var schemaExamples = []schemaExample{ { Title: "ANY OF", - Schema: &openapi3.Schema{ - AnyOf: []*openapi3.SchemaRef{ + Schema: &Schema{ + AnyOf: []*SchemaRef{ { - Value: openapi3.NewFloat64Schema(). + Value: NewFloat64Schema(). WithMin(1). WithMax(2), }, { - Value: openapi3.NewFloat64Schema(). + Value: NewFloat64Schema(). WithMin(2). WithMax(3), }, @@ -843,15 +842,15 @@ var schemaExamples = []schemaExample{ { Title: "ALL OF", - Schema: &openapi3.Schema{ - AllOf: []*openapi3.SchemaRef{ + Schema: &Schema{ + AllOf: []*SchemaRef{ { - Value: openapi3.NewFloat64Schema(). + Value: NewFloat64Schema(). WithMin(1). WithMax(2), }, { - Value: openapi3.NewFloat64Schema(). + Value: NewFloat64Schema(). WithMin(2). WithMax(3), }, @@ -884,15 +883,15 @@ var schemaExamples = []schemaExample{ { Title: "ONE OF", - Schema: &openapi3.Schema{ - OneOf: []*openapi3.SchemaRef{ + Schema: &Schema{ + OneOf: []*SchemaRef{ { - Value: openapi3.NewFloat64Schema(). + Value: NewFloat64Schema(). WithMin(1). WithMax(2), }, { - Value: openapi3.NewFloat64Schema(). + Value: NewFloat64Schema(). WithMin(2). WithMax(3), }, @@ -926,7 +925,7 @@ var schemaExamples = []schemaExample{ type schemaTypeExample struct { Title string - Schema *openapi3.Schema + Schema *Schema AllValid []string AllInvalid []string } @@ -942,12 +941,12 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { baseSchema := example.Schema for _, typ := range example.AllValid { schema := baseSchema.WithFormat(typ) - err := schema.Validate(context.TODO()) + err := schema.Validate(context.Background()) require.NoError(t, err) } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) - err := schema.Validate(context.TODO()) + err := schema.Validate(context.Background()) require.Error(t, err) } } @@ -956,7 +955,7 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { var typeExamples = []schemaTypeExample{ { Title: "STRING", - Schema: openapi3.NewStringSchema(), + Schema: NewStringSchema(), AllValid: []string{ "", "byte", @@ -974,7 +973,7 @@ var typeExamples = []schemaTypeExample{ { Title: "NUMBER", - Schema: openapi3.NewFloat64Schema(), + Schema: NewFloat64Schema(), AllValid: []string{ "", "float", @@ -987,7 +986,7 @@ var typeExamples = []schemaTypeExample{ { Title: "INTEGER", - Schema: openapi3.NewIntegerSchema(), + Schema: NewIntegerSchema(), AllValid: []string{ "", "int32", @@ -1014,60 +1013,32 @@ func testSchemaError(t *testing.T, example schemaErrorExample) func(*testing.T) type schemaErrorExample struct { Title string - Error *openapi3.SchemaError + Error *SchemaError Want string } var schemaErrorExamples = []schemaErrorExample{ { Title: "SIMPLE", - Error: &openapi3.SchemaError{ + Error: &SchemaError{ Value: 1, - Schema: &openapi3.Schema{}, + Schema: &Schema{}, Reason: "SIMPLE", }, Want: "SIMPLE", }, { Title: "NEST", - Error: &openapi3.SchemaError{ + Error: &SchemaError{ Value: 1, - Schema: &openapi3.Schema{}, + Schema: &Schema{}, Reason: "PARENT", - Origin: &openapi3.SchemaError{ + Origin: &SchemaError{ Value: 1, - Schema: &openapi3.Schema{}, + Schema: &Schema{}, Reason: "NEST", }, }, Want: "NEST", }, } - -func TestRegisterArrayUniqueItemsChecker(t *testing.T) { - var ( - checker = func(items []interface{}) bool { - return false - } - scheme = openapi3.Schema{ - Type: "array", - UniqueItems: true, - Items: openapi3.NewStringSchema().NewRef(), - } - val = []interface{}{"1", "2", "3"} - err error - ) - - // Fist checked by predefined function - err = scheme.VisitJSON(val) - require.NoError(t, err) - - // Register a function will always return false when check if a - // slice has unique items, then use a slice indeed has unique - // items to verify that check unique items will failed. - openapi3.RegisterArrayUniqueItemsChecker(checker) - - err = scheme.VisitJSON(val) - require.Error(t, err) - require.True(t, strings.HasPrefix(err.Error(), "Duplicate items found")) -} diff --git a/openapi3/security_scheme_test.go b/openapi3/security_scheme_test.go index 7f013be4c..2a6420877 100644 --- a/openapi3/security_scheme_test.go +++ b/openapi3/security_scheme_test.go @@ -1,10 +1,9 @@ -package openapi3_test +package openapi3 import ( "context" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -23,10 +22,10 @@ func TestSecuritySchemaExample(t *testing.T) { func testSecuritySchemaExample(t *testing.T, e securitySchemeExample) func(*testing.T) { return func(t *testing.T) { var err error - ss := &openapi3.SecurityScheme{} + ss := &SecurityScheme{} err = ss.UnmarshalJSON(e.raw) require.NoError(t, err) - err = ss.Validate(context.TODO()) + err = ss.Validate(context.Background()) if e.valid { require.NoError(t, err) } else { diff --git a/openapi3/server.go b/openapi3/server.go index 2594d2b30..c6cd44353 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -11,6 +11,7 @@ import ( // Servers is specified by OpenAPI/Swagger standard version 3.0. type Servers []*Server +// Validate ensures servers are per the OpenAPIv3 specification. func (servers Servers) Validate(c context.Context) error { for _, v := range servers { if err := v.Validate(c); err != nil { @@ -141,7 +142,7 @@ func (serverVariable *ServerVariable) Validate(c context.Context) error { switch item.(type) { case float64, string: default: - return errors.New("Every variable 'enum' item must be number of string") + return errors.New("All 'enum' items must be either a number or a string") } } return nil diff --git a/openapi3/server_test.go b/openapi3/server_test.go index beafcaa63..550eacbd9 100644 --- a/openapi3/server_test.go +++ b/openapi3/server_test.go @@ -1,16 +1,15 @@ -package openapi3_test +package openapi3 import ( "context" "errors" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) func TestServerParamNames(t *testing.T) { - server := &openapi3.Server{ + server := &Server{ URL: "http://{x}.{y}.example.com", } values, err := server.ParameterNames() @@ -19,7 +18,7 @@ func TestServerParamNames(t *testing.T) { } func TestServerParamValuesWithPath(t *testing.T) { - server := &openapi3.Server{ + server := &Server{ URL: "http://{arg0}.{arg1}.example.com/a/{arg3}-version/{arg4}c{arg5}", } for input, expected := range map[string]*serverMatch{ @@ -41,7 +40,7 @@ func TestServerParamValuesWithPath(t *testing.T) { } func TestServerParamValuesNoPath(t *testing.T) { - server := &openapi3.Server{ + server := &Server{ URL: "https://{arg0}.{arg1}.example.com/", } for input, expected := range map[string]*serverMatch{ @@ -51,20 +50,20 @@ func TestServerParamValuesNoPath(t *testing.T) { } } -func validServer() *openapi3.Server { - return &openapi3.Server{ +func validServer() *Server { + return &Server{ URL: "http://my.cool.website", } } -func invalidServer() *openapi3.Server { - return &openapi3.Server{} +func invalidServer() *Server { + return &Server{} } func TestServerValidation(t *testing.T) { tests := []struct { name string - input *openapi3.Server + input *Server expectedError error }{ { @@ -89,7 +88,7 @@ func TestServerValidation(t *testing.T) { } } -func testServerParamValues(t *testing.T, server *openapi3.Server, input string, expected *serverMatch) func(*testing.T) { +func testServerParamValues(t *testing.T, server *Server, input string, expected *serverMatch) func(*testing.T) { return func(t *testing.T) { args, remaining, ok := server.MatchRawURL(input) if expected == nil { diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index aae028065..44d98ab7a 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -17,15 +17,15 @@ import ( ) func foundUnresolvedRef(ref string) error { - return fmt.Errorf("Found unresolved ref: '%s'", ref) + return fmt.Errorf("found unresolved ref: %q", ref) } func failedToResolveRefFragment(value string) error { - return fmt.Errorf("Failed to resolve fragment in URI: '%s'", value) + return fmt.Errorf("failed to resolve fragment in URI: %q", value) } func failedToResolveRefFragmentPart(value string, what string) error { - return fmt.Errorf("Failed to resolve '%s' in fragment in URI: '%s'", what, value) + return fmt.Errorf("failed to resolve %q in fragment in URI: %q", what, value) } type SwaggerLoader struct { @@ -65,7 +65,7 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL // passed element. func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) error { if !swaggerLoader.IsExternalRefsAllowed { - return fmt.Errorf("encountered non-allowed external reference: '%s'", ref) + return fmt.Errorf("encountered non-allowed external reference: %q", ref) } parsedURL, err := url.Parse(ref) @@ -107,7 +107,7 @@ func readURL(location *url.URL) ([]byte, error) { return data, nil } if location.Scheme != "" || location.Host != "" || location.RawQuery != "" { - return nil, fmt.Errorf("Unsupported URI: '%s'", location.String()) + return nil, fmt.Errorf("unsupported URI: %q", location.String()) } data, err := ioutil.ReadFile(location.Path) if err != nil { @@ -123,18 +123,16 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*Swagger, error) { f := swaggerLoader.LoadSwaggerFromURIFunc + pathAsURL := &url.URL{Path: path} if f != nil { - return f(swaggerLoader, &url.URL{ - Path: path, - }) + x, err := f(swaggerLoader, pathAsURL) + return x, err } data, err := ioutil.ReadFile(path) if err != nil { return nil, err } - return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, &url.URL{ - Path: path, - }) + return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, pathAsURL) } func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, error) { @@ -266,11 +264,11 @@ func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref strin parsedURL, err := url.Parse(ref) if err != nil { - return nil, nil, fmt.Errorf("Can't parse reference: '%s': %v", ref, parsedURL) + return nil, nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) } fragment := parsedURL.Fragment if !strings.HasPrefix(fragment, "/") { - err := fmt.Errorf("expected fragment prefix '#/' in URI '%s'", ref) + err := fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) return nil, nil, err } @@ -279,7 +277,7 @@ func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref strin pathPart = unescapeRefString(pathPart) if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil { - return nil, nil, fmt.Errorf("Failed to resolve '%s' in fragment in URI: '%s': %v", ref, pathPart, err.Error()) + return nil, nil, fmt.Errorf("failed to resolve %q in fragment in URI: %q: %v", ref, pathPart, err.Error()) } if cursor == nil { return nil, nil, failedToResolveRefFragmentPart(ref, pathPart) @@ -294,7 +292,7 @@ func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, e case reflect.Map: elementValue := val.MapIndex(reflect.ValueOf(fieldName)) if !elementValue.IsValid() { - return nil, fmt.Errorf("Map key not found: %v", fieldName) + return nil, fmt.Errorf("map key %q not found", fieldName) } return elementValue.Interface(), nil @@ -324,7 +322,7 @@ func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, e return drillIntoSwaggerField(val.FieldByName("Value").Interface(), fieldName) // recurse into .Value } // give up - return nil, fmt.Errorf("Struct field not found: %v", fieldName) + return nil, fmt.Errorf("struct field %q not found", fieldName) default: return nil, errors.New("not a map, slice nor struct") @@ -335,24 +333,24 @@ func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref stri componentPath := path if !strings.HasPrefix(ref, "#") { if !swaggerLoader.IsExternalRefsAllowed { - return nil, "", nil, fmt.Errorf("Encountered non-allowed external reference: '%s'", ref) + return nil, "", nil, fmt.Errorf("encountered non-allowed external reference: %q", ref) } parsedURL, err := url.Parse(ref) if err != nil { - return nil, "", nil, fmt.Errorf("Can't parse reference: '%s': %v", ref, parsedURL) + return nil, "", nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) } fragment := parsedURL.Fragment parsedURL.Fragment = "" resolvedPath, err := resolvePath(path, parsedURL) if err != nil { - return nil, "", nil, fmt.Errorf("Error while resolving path: %v", err) + return nil, "", nil, fmt.Errorf("error resolving path: %v", err) } if swagger, err = swaggerLoader.loadSwaggerFromURIInternal(resolvedPath); err != nil { - return nil, "", nil, fmt.Errorf("Error while resolving reference '%s': %v", ref, err) + return nil, "", nil, fmt.Errorf("error resolving reference %q: %v", ref, err) } - ref = fmt.Sprintf("#%s", fragment) + ref = "#" + fragment componentPath = resolvedPath } return swagger, ref, componentPath, nil @@ -449,7 +447,7 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon } if value.Content != nil && value.Schema != nil { - return errors.New("Cannot contain both schema and content in a parameter") + return errors.New("cannot contain both schema and content in a parameter") } for _, contentType := range value.Content { if schema := contentType.Schema; schema != nil { @@ -822,7 +820,7 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo } if !strings.HasPrefix(ref, prefix) { - err = fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref) + err = fmt.Errorf("expected prefix %q in URI %q", prefix, ref) return } id := unescapeRefString(ref[len(prefix):]) diff --git a/openapi3/swagger_loader_empty_response_description_test.go b/openapi3/swagger_loader_empty_response_description_test.go index 5199ac169..c75ad8aae 100644 --- a/openapi3/swagger_loader_empty_response_description_test.go +++ b/openapi3/swagger_loader_empty_response_description_test.go @@ -1,11 +1,10 @@ -package openapi3_test +package openapi3 import ( "encoding/json" "strings" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -34,7 +33,7 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { { spec := []byte(spec) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(spec) require.NoError(t, err) got := doc.Paths["/path1"].Get.Responses["200"].Value.Description @@ -47,7 +46,7 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { { spec := []byte(strings.Replace(spec, `"description": ""`, `"description": "My response"`, 1)) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(spec) require.NoError(t, err) got := doc.Paths["/path1"].Get.Responses["200"].Value.Description @@ -58,8 +57,8 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { require.NoError(t, err) } - noDescriptionIsInvalid := func(data []byte) *openapi3.Swagger { - loader := openapi3.NewSwaggerLoader() + noDescriptionIsInvalid := func(data []byte) *Swagger { + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(data) require.NoError(t, err) got := doc.Paths["/path1"].Get.Responses["200"].Value.Description @@ -70,7 +69,7 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { return doc } - var docWithNoResponseDescription *openapi3.Swagger + var docWithNoResponseDescription *Swagger { spec := []byte(strings.Replace(spec, `"description": ""`, ``, 1)) docWithNoResponseDescription = noDescriptionIsInvalid(spec) diff --git a/openapi3/swagger_loader_paths_test.go b/openapi3/swagger_loader_paths_test.go index 9605b07a4..babd52c25 100644 --- a/openapi3/swagger_loader_paths_test.go +++ b/openapi3/swagger_loader_paths_test.go @@ -1,10 +1,9 @@ -package openapi3_test +package openapi3 import ( "strings" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -27,7 +26,7 @@ paths: "foo/bar": "invalid paths: path \"foo/bar\" does not start with a forward slash (/)", "/foo/bar": "", } { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData([]byte(strings.Replace(spec, "PATH", path, 1))) require.NoError(t, err) err = doc.Validate(loader.Context) diff --git a/openapi3/swagger_loader_relative_refs_test.go b/openapi3/swagger_loader_relative_refs_test.go index 5ad7585ae..071938908 100644 --- a/openapi3/swagger_loader_relative_refs_test.go +++ b/openapi3/swagger_loader_relative_refs_test.go @@ -1,33 +1,31 @@ -package openapi3_test +package openapi3 import ( "fmt" - "net/url" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) type refTestDataEntry struct { name string contentTemplate string - testFunc func(t *testing.T, swagger *openapi3.Swagger) + testFunc func(t *testing.T, swagger *Swagger) } type refTestDataEntryWithErrorMessage struct { name string contentTemplate string errorMessage *string - testFunc func(t *testing.T, swagger *openapi3.Swagger) + testFunc func(t *testing.T, swagger *Swagger) } var refTestDataEntries = []refTestDataEntry{ { name: "SchemaRef", contentTemplate: externalSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.Schemas["TestSchema"].Value.Type) require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type) }, @@ -35,7 +33,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "ResponseRef", contentTemplate: externalResponseRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { desc := "description" require.Equal(t, &desc, swagger.Components.Responses["TestResponse"].Value.Description) }, @@ -43,7 +41,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "ParameterRef", contentTemplate: externalParameterRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.Parameters["TestParameter"].Value.Name) require.Equal(t, "id", swagger.Components.Parameters["TestParameter"].Value.Name) }, @@ -51,7 +49,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "ExampleRef", contentTemplate: externalExampleRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.Examples["TestExample"].Value.Description) require.Equal(t, "description", swagger.Components.Examples["TestExample"].Value.Description) }, @@ -59,14 +57,14 @@ var refTestDataEntries = []refTestDataEntry{ { name: "RequestBodyRef", contentTemplate: externalRequestBodyRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.RequestBodies["TestRequestBody"].Value.Content) }, }, { name: "SecuritySchemeRef", contentTemplate: externalSecuritySchemeRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Description) require.Equal(t, "description", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Description) }, @@ -74,7 +72,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "ExternalHeaderRef", contentTemplate: externalHeaderRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.Headers["TestHeader"].Value.Description) require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description) }, @@ -82,7 +80,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathParameterRef", contentTemplate: externalPathParameterRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test/{id}"].Parameters[0].Value.Name) require.Equal(t, "id", swagger.Paths["/test/{id}"].Parameters[0].Value.Name) }, @@ -90,7 +88,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationParameterRef", contentTemplate: externalPathOperationParameterRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test/{id}"].Get.Parameters[0].Value) require.Equal(t, "id", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Name) }, @@ -98,7 +96,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationRequestBodyRef", contentTemplate: externalPathOperationRequestBodyRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value) require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content) }, @@ -106,7 +104,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationResponseRef", contentTemplate: externalPathOperationResponseRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value) desc := "description" require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description) @@ -115,7 +113,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationParameterSchemaRef", contentTemplate: externalPathOperationParameterSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value) require.Equal(t, "string", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value.Type) require.Equal(t, "id", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Name) @@ -125,7 +123,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationParameterRefWithContentInQuery", contentTemplate: externalPathOperationParameterWithContentInQueryTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { schemaRef := swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Content["application/json"].Schema require.NotNil(t, schemaRef.Value) require.Equal(t, "string", schemaRef.Value.Type) @@ -135,7 +133,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationRequestBodyExampleRef", contentTemplate: externalPathOperationRequestBodyExampleRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value) require.Equal(t, "description", swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description) }, @@ -143,7 +141,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationReqestBodyContentSchemaRef", contentTemplate: externalPathOperationReqestBodyContentSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value) require.Equal(t, "string", swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) }, @@ -151,7 +149,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationResponseExampleRef", contentTemplate: externalPathOperationResponseExampleRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value) desc := "testdescription" require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description) @@ -161,7 +159,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationResponseSchemaRef", contentTemplate: externalPathOperationResponseSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value) desc := "testdescription" require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description) @@ -171,7 +169,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "ComponentHeaderSchemaRef", contentTemplate: externalComponentHeaderSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.Headers["TestHeader"].Value) require.Equal(t, "string", swagger.Components.Headers["TestHeader"].Value.Schema.Value.Type) }, @@ -179,7 +177,7 @@ var refTestDataEntries = []refTestDataEntry{ { name: "RequestResponseHeaderRef", contentTemplate: externalRequestResponseHeaderRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) }, @@ -190,8 +188,8 @@ var refTestDataEntriesResponseError = []refTestDataEntryWithErrorMessage{ { name: "CannotContainBothSchemaAndContentInAParameter", contentTemplate: externalCannotContainBothSchemaAndContentInAParameter, - errorMessage: &(&struct{ x string }{"Cannot contain both schema and content in a parameter"}).x, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + errorMessage: &(&struct{ x string }{"cannot contain both schema and content in a parameter"}).x, + testFunc: func(t *testing.T, swagger *Swagger) { }, }, } @@ -201,7 +199,7 @@ func TestLoadFromDataWithExternalRef(t *testing.T) { t.Logf("testcase '%s'", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json")) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) @@ -214,7 +212,7 @@ func TestLoadFromDataWithExternalRefResponseError(t *testing.T) { t.Logf("testcase '%s'", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json")) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.EqualError(t, err, *td.errorMessage) @@ -227,7 +225,7 @@ func TestLoadFromDataWithExternalNestedRef(t *testing.T) { t.Logf("testcase '%s'", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "nesteddir/nestedcomponents.openapi.json")) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) @@ -725,7 +723,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "SchemaRef", contentTemplate: relativeSchemaDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.Schemas["TestSchema"].Value.Type) require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type) }, @@ -733,7 +731,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "ResponseRef", contentTemplate: relativeResponseDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { desc := "description" require.Equal(t, &desc, swagger.Components.Responses["TestResponse"].Value.Description) }, @@ -741,7 +739,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "ParameterRef", contentTemplate: relativeParameterDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.Parameters["TestParameter"].Value.Name) require.Equal(t, "param", swagger.Components.Parameters["TestParameter"].Value.Name) require.Equal(t, true, swagger.Components.Parameters["TestParameter"].Value.Required) @@ -750,7 +748,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "ExampleRef", contentTemplate: relativeExampleDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, "param", swagger.Components.Examples["TestExample"].Value.Summary) require.NotNil(t, "param", swagger.Components.Examples["TestExample"].Value.Value) require.Equal(t, "An example", swagger.Components.Examples["TestExample"].Value.Summary) @@ -759,7 +757,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "RequestRef", contentTemplate: relativeRequestDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, "param", swagger.Components.RequestBodies["TestRequestBody"].Value.Description) require.Equal(t, "example request", swagger.Components.RequestBodies["TestRequestBody"].Value.Description) }, @@ -767,7 +765,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "HeaderRef", contentTemplate: relativeHeaderDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, "param", swagger.Components.Headers["TestHeader"].Value.Description) require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description) }, @@ -775,7 +773,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "HeaderRef", contentTemplate: relativeHeaderDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, "param", swagger.Components.Headers["TestHeader"].Value.Description) require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description) }, @@ -783,7 +781,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "SecuritySchemeRef", contentTemplate: relativeSecuritySchemeDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Type) require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme) require.Equal(t, "http", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Type) @@ -793,7 +791,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "PathRef", contentTemplate: relativePathDocsRefTemplate, - testFunc: func(t *testing.T, swagger *openapi3.Swagger) { + testFunc: func(t *testing.T, swagger *Swagger) { require.NotNil(t, swagger.Paths["/pets"]) require.NotNil(t, swagger.Paths["/pets"].Get.Responses["200"]) require.NotNil(t, swagger.Paths["/pets"].Get.Responses["200"].Value.Content["application/json"]) @@ -806,7 +804,7 @@ func TestLoadSpecWithRelativeDocumentRefs(t *testing.T) { t.Logf("testcase '%s'", td.name) spec := []byte(td.contentTemplate) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/"}) require.NoError(t, err) @@ -908,7 +906,7 @@ paths: ` func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromFile("testdata/relativeDocsUseDocumentPath/openapi/openapi.yml") diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go index 960383260..692ebd2b8 100644 --- a/openapi3/swagger_loader_test.go +++ b/openapi3/swagger_loader_test.go @@ -1,4 +1,4 @@ -package openapi3_test +package openapi3 import ( "fmt" @@ -8,7 +8,6 @@ import ( "net/url" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -54,7 +53,7 @@ paths: $ref: '#/components/schemas/ErrorModel' `) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(spec) require.NoError(t, err) require.Equal(t, "An API", doc.Info.Title) @@ -69,7 +68,7 @@ paths: func ExampleSwaggerLoader() { source := `{"info":{"description":"An API"}}` - swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData([]byte(source)) + swagger, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(source)) if err != nil { panic(err) } @@ -80,7 +79,7 @@ func ExampleSwaggerLoader() { func TestResolveSchemaRef(t *testing.T) { source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1",description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) @@ -93,11 +92,11 @@ func TestResolveSchemaRef(t *testing.T) { func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) { source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{"/foo":{"post":{"requestBody":{"content":{"application/json":{"schema":null}}}}}}}`) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) - require.EqualError(t, err, "invalid paths: Found unresolved ref: ''") + require.EqualError(t, err, `invalid paths: found unresolved ref: ""`) } func TestResolveResponseExampleRef(t *testing.T) { @@ -122,7 +121,7 @@ paths: examples: test: $ref: '#/components/examples/test'`) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(source) require.NoError(t, err) @@ -144,9 +143,9 @@ type multipleSourceSwaggerLoaderExample struct { } func (l *multipleSourceSwaggerLoaderExample) LoadSwaggerFromURI( - loader *openapi3.SwaggerLoader, + loader *SwaggerLoader, location *url.URL, -) (*openapi3.Swagger, error) { +) (*Swagger, error) { source := l.resolveSourceFromURI(location) if source == nil { return nil, fmt.Errorf("Unsupported URI: '%s'", location.String()) @@ -184,7 +183,7 @@ func TestResolveSchemaExternalRef(t *testing.T) { }, }, } - loader := &openapi3.SwaggerLoader{ + loader := &SwaggerLoader{ IsExternalRefsAllowed: true, LoadSwaggerFromURIFunc: multipleSourceLoader.LoadSwaggerFromURI, } @@ -223,7 +222,7 @@ paths: $ref: '#/components/schemas/Thing' `) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() _, err := loader.LoadSwaggerFromData(spec) require.Error(t, err) } @@ -251,7 +250,7 @@ paths: description: Test call. `) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() swagger, err := loader.LoadSwaggerFromData(spec) require.NoError(t, err) @@ -283,7 +282,7 @@ paths: description: Test call. `) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() swagger, err := loader.LoadSwaggerFromData(spec) require.NoError(t, err) @@ -305,7 +304,7 @@ func TestLoadFromRemoteURL(t *testing.T) { ts.Start() defer ts.Close() - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true url, err := url.Parse("http://" + addr + "/test.openapi.json") require.NoError(t, err) @@ -317,7 +316,7 @@ func TestLoadFromRemoteURL(t *testing.T) { } func TestLoadFileWithExternalSchemaRef(t *testing.T) { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.json") require.NoError(t, err) @@ -326,7 +325,7 @@ func TestLoadFileWithExternalSchemaRef(t *testing.T) { } func TestLoadFileWithExternalSchemaRefSingleComponent(t *testing.T) { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromFile("testdata/testrefsinglecomponent.openapi.json") require.NoError(t, err) @@ -369,7 +368,7 @@ func TestLoadRequestResponseHeaderRef(t *testing.T) { } }`) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() swagger, err := loader.LoadSwaggerFromData(spec) require.NoError(t, err) @@ -408,7 +407,7 @@ func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) { ts.Start() defer ts.Close() - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) @@ -418,7 +417,7 @@ func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) { } func TestLoadYamlFile(t *testing.T) { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromFile("testdata/test.openapi.yml") require.NoError(t, err) @@ -427,7 +426,7 @@ func TestLoadYamlFile(t *testing.T) { } func TestLoadYamlFileWithExternalSchemaRef(t *testing.T) { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.yml") require.NoError(t, err) @@ -436,7 +435,7 @@ func TestLoadYamlFileWithExternalSchemaRef(t *testing.T) { } func TestLoadYamlFileWithExternalPathRef(t *testing.T) { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromFile("testdata/pathref.openapi.yml") require.NoError(t, err) @@ -476,7 +475,7 @@ paths: father: $ref: '#/components/links/Father' `) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(source) require.NoError(t, err) @@ -545,7 +544,7 @@ paths: $ref: '#/components/schemas/ErrorModel' `) - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() doc, err := loader.LoadSwaggerFromData(spec) require.NoError(t, err) err = doc.Validate(loader.Context) diff --git a/openapi3/swagger_test.go b/openapi3/swagger_test.go index 06c478c76..0117cea6e 100644 --- a/openapi3/swagger_test.go +++ b/openapi3/swagger_test.go @@ -1,39 +1,38 @@ -package openapi3_test +package openapi3 import ( "context" "encoding/json" - "errors" + "strings" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/ghodss/yaml" "github.com/stretchr/testify/require" ) func TestRefsJSON(t *testing.T) { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() - t.Log("Marshal *openapi3.Swagger to JSON") + t.Log("Marshal *Swagger to JSON") data, err := json.Marshal(spec()) require.NoError(t, err) require.NotEmpty(t, data) - t.Log("Unmarshal *openapi3.Swagger from JSON") - docA := &openapi3.Swagger{} - err = json.Unmarshal([]byte(specJSON), &docA) + t.Log("Unmarshal *Swagger from JSON") + docA := &Swagger{} + err = json.Unmarshal(specJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) - t.Log("Resolve refs in unmarshalled *openapi3.Swagger") + t.Log("Resolve refs in unmarshalled *Swagger") err = loader.ResolveRefsIn(docA, nil) require.NoError(t, err) - t.Log("Resolve refs in marshalled *openapi3.Swagger") + t.Log("Resolve refs in marshalled *Swagger") docB, err := loader.LoadSwaggerFromData(data) require.NoError(t, err) require.NotEmpty(t, docB) - t.Log("Validate *openapi3.Swagger") + t.Log("Validate *Swagger") err = docA.Validate(loader.Context) require.NoError(t, err) err = docB.Validate(loader.Context) @@ -44,34 +43,34 @@ func TestRefsJSON(t *testing.T) { require.NoError(t, err) dataB, err := json.Marshal(docB) require.NoError(t, err) - require.JSONEq(t, string(data), specJSON) + require.JSONEq(t, string(data), string(specJSON)) require.JSONEq(t, string(data), string(dataA)) require.JSONEq(t, string(data), string(dataB)) } func TestRefsYAML(t *testing.T) { - loader := openapi3.NewSwaggerLoader() + loader := NewSwaggerLoader() - t.Log("Marshal *openapi3.Swagger to YAML") + t.Log("Marshal *Swagger to YAML") data, err := yaml.Marshal(spec()) require.NoError(t, err) require.NotEmpty(t, data) - t.Log("Unmarshal *openapi3.Swagger from YAML") - docA := &openapi3.Swagger{} - err = yaml.Unmarshal([]byte(specYAML), &docA) + t.Log("Unmarshal *Swagger from YAML") + docA := &Swagger{} + err = yaml.Unmarshal(specYAML, &docA) require.NoError(t, err) require.NotEmpty(t, data) - t.Log("Resolve refs in unmarshalled *openapi3.Swagger") + t.Log("Resolve refs in unmarshalled *Swagger") err = loader.ResolveRefsIn(docA, nil) require.NoError(t, err) - t.Log("Resolve refs in marshalled *openapi3.Swagger") + t.Log("Resolve refs in marshalled *Swagger") docB, err := loader.LoadSwaggerFromData(data) require.NoError(t, err) require.NotEmpty(t, docB) - t.Log("Validate *openapi3.Swagger") + t.Log("Validate *Swagger") err = docA.Validate(loader.Context) require.NoError(t, err) err = docB.Validate(loader.Context) @@ -82,7 +81,7 @@ func TestRefsYAML(t *testing.T) { require.NoError(t, err) dataB, err := yaml.Marshal(docB) require.NoError(t, err) - eqYAML(t, data, []byte(specYAML)) + eqYAML(t, data, specYAML) eqYAML(t, data, dataA) eqYAML(t, data, dataB) } @@ -96,7 +95,7 @@ func eqYAML(t *testing.T, expected, actual []byte) { require.Equal(t, e, a) } -var specYAML = ` +var specYAML = []byte(` openapi: '3.0' info: title: MyAPI @@ -148,9 +147,9 @@ components: name: token someSecurityScheme: "$ref": "#/components/securitySchemes/otherSecurityScheme" -` +`) -var specJSON = ` +var specJSON = []byte(` { "openapi": "3.0", "info": { @@ -236,55 +235,55 @@ var specJSON = ` } } } -` +`) -func spec() *openapi3.Swagger { - parameter := &openapi3.Parameter{ +func spec() *Swagger { + parameter := &Parameter{ Description: "Some parameter", Name: "example", In: "query", - Schema: &openapi3.SchemaRef{ + Schema: &SchemaRef{ Ref: "#/components/schemas/someSchema", }, } - requestBody := &openapi3.RequestBody{ + requestBody := &RequestBody{ Description: "Some request body", } responseDescription := "Some response" - response := &openapi3.Response{ + response := &Response{ Description: &responseDescription, } - schema := &openapi3.Schema{ + schema := &Schema{ Description: "Some schema", } example := map[string]string{"name": "Some example"} - return &openapi3.Swagger{ + return &Swagger{ OpenAPI: "3.0", - Info: &openapi3.Info{ + Info: &Info{ Title: "MyAPI", Version: "0.1", }, - Paths: openapi3.Paths{ - "/hello": &openapi3.PathItem{ - Post: &openapi3.Operation{ - Parameters: openapi3.Parameters{ + Paths: Paths{ + "/hello": &PathItem{ + Post: &Operation{ + Parameters: Parameters{ { Ref: "#/components/parameters/someParameter", Value: parameter, }, }, - RequestBody: &openapi3.RequestBodyRef{ + RequestBody: &RequestBodyRef{ Ref: "#/components/requestBodies/someRequestBody", Value: requestBody, }, - Responses: openapi3.Responses{ - "200": &openapi3.ResponseRef{ + Responses: Responses{ + "200": &ResponseRef{ Ref: "#/components/responses/someResponse", Value: response, }, }, }, - Parameters: openapi3.Parameters{ + Parameters: Parameters{ { Ref: "#/components/parameters/someParameter", Value: parameter, @@ -292,49 +291,49 @@ func spec() *openapi3.Swagger { }, }, }, - Components: openapi3.Components{ - Parameters: map[string]*openapi3.ParameterRef{ + Components: Components{ + Parameters: map[string]*ParameterRef{ "someParameter": { Value: parameter, }, }, - RequestBodies: map[string]*openapi3.RequestBodyRef{ + RequestBodies: map[string]*RequestBodyRef{ "someRequestBody": { Value: requestBody, }, }, - Responses: map[string]*openapi3.ResponseRef{ + Responses: map[string]*ResponseRef{ "someResponse": { Value: response, }, }, - Schemas: map[string]*openapi3.SchemaRef{ + Schemas: map[string]*SchemaRef{ "someSchema": { Value: schema, }, }, - Headers: map[string]*openapi3.HeaderRef{ + Headers: map[string]*HeaderRef{ "someHeader": { Ref: "#/components/headers/otherHeader", }, "otherHeader": { - Value: &openapi3.Header{}, + Value: &Header{}, }, }, - Examples: map[string]*openapi3.ExampleRef{ + Examples: map[string]*ExampleRef{ "someExample": { Ref: "#/components/examples/otherExample", }, "otherExample": { - Value: openapi3.NewExample(example), + Value: NewExample(example), }, }, - SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{ + SecuritySchemes: map[string]*SecuritySchemeRef{ "someSecurityScheme": { Ref: "#/components/securitySchemes/otherSecurityScheme", }, "otherSecurityScheme": { - Value: &openapi3.SecurityScheme{ + Value: &SecurityScheme{ Description: "Some security scheme", Type: "apiKey", In: "query", @@ -346,20 +345,13 @@ func spec() *openapi3.Swagger { } } -// TestValidation tests validation of properties in the root of the OpenAPI -// file. func TestValidation(t *testing.T) { - tests := []struct { - name string - input string - expectedError error - }{ - { - "when no OpenAPI property is supplied", - ` + info := ` info: title: "Hello World REST APIs" version: "1.0" +` + paths := ` paths: "/api/v2/greetings.json": get: @@ -380,103 +372,10 @@ paths: responses: 200: description: "Get a single greeting object" -`, - errors.New("value of openapi must be a non-empty JSON string"), - }, - { - "when an empty OpenAPI property is supplied", - ` -openapi: '' -info: - title: "Hello World REST APIs" - version: "1.0" -paths: - "/api/v2/greetings.json": - get: - operationId: listGreetings - responses: - 200: - description: "List different greetings" - "/api/v2/greetings/{id}.json": - parameters: - - name: id - in: path - required: true - schema: - type: string - example: "greeting" - get: - operationId: showGreeting - responses: - 200: - description: "Get a single greeting object" -`, - errors.New("value of openapi must be a non-empty JSON string"), - }, - { - "when the Info property is not supplied", - ` -openapi: '1.0' -paths: - "/api/v2/greetings.json": - get: - operationId: listGreetings - responses: - 200: - description: "List different greetings" - "/api/v2/greetings/{id}.json": - parameters: - - name: id - in: path - required: true - schema: - type: string - example: "greeting" - get: - operationId: showGreeting - responses: - 200: - description: "Get a single greeting object" -`, - errors.New("invalid info: must be a JSON object"), - }, - { - "when the Paths property is not supplied", - ` -openapi: '1.0' -info: - title: "Hello World REST APIs" - version: "1.0" -`, - errors.New("invalid paths: must be a JSON object"), - }, - { - "when a valid spec is supplied", - ` +` + spec := ` openapi: 3.0.2 -info: - title: "Hello World REST APIs" - version: "1.0" -paths: - "/api/v2/greetings.json": - get: - operationId: listGreetings - responses: - 200: - description: "List different greetings" - "/api/v2/greetings/{id}.json": - parameters: - - name: id - in: path - required: true - schema: - type: string - example: "greeting" - get: - operationId: showGreeting - responses: - 200: - description: "Get a single greeting object" +` + info + paths + ` components: schemas: GreetingObject: @@ -490,21 +389,28 @@ components: properties: description: type: string -`, - nil, - }, +` + + tests := map[string]string{ + spec: "", + strings.Replace(spec, `openapi: 3.0.2`, ``, 1): "value of openapi must be a non-empty JSON string", + strings.Replace(spec, `openapi: 3.0.2`, `openapi: ''`, 1): "value of openapi must be a non-empty JSON string", + strings.Replace(spec, info, ``, 1): "invalid info: must be a JSON object", + strings.Replace(spec, paths, ``, 1): "invalid paths: must be a JSON object", } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - doc := &openapi3.Swagger{} - err := yaml.Unmarshal([]byte(test.input), &doc) + for spec, expectedErr := range tests { + t.Run(expectedErr, func(t *testing.T) { + doc := &Swagger{} + err := yaml.Unmarshal([]byte(spec), &doc) require.NoError(t, err) - c := context.Background() - validationErr := doc.Validate(c) - - require.Equal(t, test.expectedError, validationErr, "expected errors (or lack of) to match") + err = doc.Validate(context.Background()) + if expectedErr != "" { + require.EqualError(t, err, expectedErr) + } else { + require.NoError(t, err) + } }) } } diff --git a/openapi3/unique_items_checker_test.go b/openapi3/unique_items_checker_test.go new file mode 100644 index 000000000..c2dc6f381 --- /dev/null +++ b/openapi3/unique_items_checker_test.go @@ -0,0 +1,36 @@ +package openapi3_test + +import ( + "strings" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func TestRegisterArrayUniqueItemsChecker(t *testing.T) { + var ( + schema = openapi3.Schema{ + Type: "array", + UniqueItems: true, + Items: openapi3.NewStringSchema().NewRef(), + } + val = []interface{}{"1", "2", "3"} + ) + + // Fist checked by predefined function + err := schema.VisitJSON(val) + require.NoError(t, err) + + // Register a function will always return false when check if a + // slice has unique items, then use a slice indeed has unique + // items to verify that check unique items will failed. + openapi3.RegisterArrayUniqueItemsChecker(func(items []interface{}) bool { + return false + }) + defer openapi3.RegisterArrayUniqueItemsChecker(nil) // Reset for other tests + + err = schema.VisitJSON(val) + require.Error(t, err) + require.True(t, strings.HasPrefix(err.Error(), "Duplicate items found")) +} diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index c4e6e0f17..4de536ef1 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -1,4 +1,4 @@ -package openapi3filter_test +package openapi3filter import ( "bytes" @@ -15,7 +15,6 @@ import ( "testing" "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" "github.com/stretchr/testify/require" ) @@ -155,8 +154,8 @@ func TestFilter(t *testing.T) { }, } - router := openapi3filter.NewRouter().WithSwagger(swagger) - expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder openapi3filter.ContentParameterDecoder) error { + router := NewRouter().WithSwagger(swagger) + expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder ContentParameterDecoder) error { t.Logf("Request: %s %s", req.Method, req.URL) httpReq, _ := http.NewRequest(req.Method, req.URL, marshalReader(req.Body)) httpReq.Header.Set("Content-Type", req.ContentType) @@ -166,17 +165,17 @@ func TestFilter(t *testing.T) { require.NoError(t, err) // Validate request - requestValidationInput := &openapi3filter.RequestValidationInput{ + requestValidationInput := &RequestValidationInput{ Request: httpReq, PathParams: pathParams, Route: route, ParamDecoder: decoder, } - if err := openapi3filter.ValidateRequest(context.TODO(), requestValidationInput); err != nil { + if err := ValidateRequest(context.Background(), requestValidationInput); err != nil { return err } t.Logf("Response: %d", resp.Status) - responseValidationInput := &openapi3filter.ResponseValidationInput{ + responseValidationInput := &ResponseValidationInput{ RequestValidationInput: requestValidationInput, Status: resp.Status, Header: http.Header{ @@ -190,7 +189,7 @@ func TestFilter(t *testing.T) { require.NoError(t, err) responseValidationInput.SetBodyBytes(data) } - err = openapi3filter.ValidateResponse(context.TODO(), responseValidationInput) + err = ValidateResponse(context.Background(), responseValidationInput) require.NoError(t, err) return err } @@ -220,7 +219,7 @@ func TestFilter(t *testing.T) { URL: "http://example.com/api/prefix/EXCEEDS_MAX_LENGTH/suffix", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) // Test query parameter openapi3filter req = ExampleRequest{ @@ -235,14 +234,14 @@ func TestFilter(t *testing.T) { URL: "http://example.com/api/prefix/v/suffix?queryArg=EXCEEDS_MAX_LENGTH", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "GET", URL: "http://example.com/api/issue151?par2=par1_is_missing", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) // Test query parameter openapi3filter req = ExampleRequest{ @@ -264,28 +263,28 @@ func TestFilter(t *testing.T) { URL: "http://example.com/api/prefix/v/suffix?queryArgAnyOf=123", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgOneOf=567", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgOneOf=2017-12-31T11:59:59", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgAllOf=abdfg", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) // TODO(decode not): handle decoding "not" JSON Schema // req = ExampleRequest{ @@ -293,7 +292,7 @@ func TestFilter(t *testing.T) { // URL: "http://example.com/api/prefix/v/suffix?queryArgNot=abdfg", // } // err = expect(req, resp) - // require.IsType(t, &openapi3filter.RequestError{}, err) + // require.IsType(t, &RequestError{}, err) // TODO(decode not): handle decoding "not" JSON Schema // req = ExampleRequest{ @@ -301,14 +300,14 @@ func TestFilter(t *testing.T) { // URL: "http://example.com/api/prefix/v/suffix?queryArgNot=123", // } // err = expect(req, resp) - // require.IsType(t, &openapi3filter.RequestError{}, err) + // require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArg=EXCEEDS_MAX_LENGTH", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", @@ -318,7 +317,7 @@ func TestFilter(t *testing.T) { Status: 200, } err = expect(req, resp) - // require.IsType(t, &openapi3filter.ResponseError{}, err) + // require.IsType(t, &ResponseError{}, err) require.NoError(t, err) // Check that content validation works. This should pass, as ID is short @@ -336,7 +335,7 @@ func TestFilter(t *testing.T) { URL: "http://example.com/api/prefix/v/suffix?contentArg={\"name\":\"bob\", \"id\":\"EXCEEDS_MAX_LENGTH\"}", } err = expect(req, resp) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) // Now, repeat the above two test cases using a custom parameter decoder. customDecoder := func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) { @@ -359,7 +358,7 @@ func TestFilter(t *testing.T) { URL: "http://example.com/api/prefix/v/suffix?contentArg2={\"name\":\"bob\", \"id\":\"EXCEEDS_MAX_LENGTH\"}", } err = expectWithDecoder(req, resp, customDecoder) - require.IsType(t, &openapi3filter.RequestError{}, err) + require.IsType(t, &RequestError{}, err) } func marshalReader(value interface{}) io.ReadCloser { @@ -403,7 +402,7 @@ func TestValidateRequestBody(t *testing.T) { { name: "required empty", body: requiredReqBody, - wantErr: &openapi3filter.RequestError{RequestBody: requiredReqBody, Err: openapi3filter.ErrInvalidRequired}, + wantErr: &RequestError{RequestBody: requiredReqBody, Err: ErrInvalidRequired}, }, { name: "required not empty", @@ -436,8 +435,8 @@ func TestValidateRequestBody(t *testing.T) { if tc.mime != "" { req.Header.Set(http.CanonicalHeaderKey("Content-Type"), tc.mime) } - inp := &openapi3filter.RequestValidationInput{Request: req} - err := openapi3filter.ValidateRequestBody(context.Background(), inp, tc.body) + inp := &RequestValidationInput{Request: req} + err := ValidateRequestBody(context.Background(), inp, tc.body) if tc.wantErr == nil { require.NoError(t, err) @@ -453,11 +452,11 @@ func matchReqBodyError(want, got error) bool { if want == got { return true } - wErr, ok := want.(*openapi3filter.RequestError) + wErr, ok := want.(*RequestError) if !ok { return false } - gErr, ok := got.(*openapi3filter.RequestError) + gErr, ok := got.(*RequestError) if !ok { return false } @@ -572,7 +571,7 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { } // Declare the router - router := openapi3filter.NewRouter().WithSwagger(swagger) + router := NewRouter().WithSwagger(swagger) // Test each case for _, path := range tc { @@ -592,11 +591,11 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { require.NoError(t, err) route, _, err := router.FindRoute(http.MethodGet, pathURL) require.NoError(t, err) - req := openapi3filter.RequestValidationInput{ + req := RequestValidationInput{ Request: httptest.NewRequest(http.MethodGet, path.name, emptyBody), Route: route, - Options: &openapi3filter.Options{ - AuthenticationFunc: func(c context.Context, input *openapi3filter.AuthenticationInput) error { + Options: &Options{ + AuthenticationFunc: func(c context.Context, input *AuthenticationInput) error { if schemesValidated != nil { if validated, ok := (*schemesValidated)[input.SecurityScheme]; ok { if validated { @@ -617,7 +616,7 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { } // Validate the request - err = openapi3filter.ValidateRequest(context.TODO(), &req) + err = ValidateRequest(context.Background(), &req) require.NoError(t, err) for securityRequirement, validated := range *schemesValidated { @@ -703,7 +702,7 @@ func TestAnySecurityRequirementMet(t *testing.T) { } // Create the router - router := openapi3filter.NewRouter().WithSwagger(&swagger) + router := NewRouter().WithSwagger(&swagger) // Create the authentication function authFunc := makeAuthFunc(schemes) @@ -714,15 +713,15 @@ func TestAnySecurityRequirementMet(t *testing.T) { require.NoError(t, err) route, _, err := router.FindRoute(http.MethodGet, tcURL) require.NoError(t, err) - req := openapi3filter.RequestValidationInput{ + req := RequestValidationInput{ Route: route, - Options: &openapi3filter.Options{ + Options: &Options{ AuthenticationFunc: authFunc, }, } // Validate the security requirements - err = openapi3filter.ValidateSecurityRequirements(context.TODO(), &req, *route.Operation.Security) + err = ValidateSecurityRequirements(context.Background(), &req, *route.Operation.Security) // If there should have been an error if tc.error { @@ -803,7 +802,7 @@ func TestAllSchemesMet(t *testing.T) { } // Create the router from the swagger - router := openapi3filter.NewRouter().WithSwagger(&swagger) + router := NewRouter().WithSwagger(&swagger) // Create the authentication function authFunc := makeAuthFunc(schemes) @@ -814,15 +813,15 @@ func TestAllSchemesMet(t *testing.T) { require.NoError(t, err) route, _, err := router.FindRoute(http.MethodGet, tcURL) require.NoError(t, err) - req := openapi3filter.RequestValidationInput{ + req := RequestValidationInput{ Route: route, - Options: &openapi3filter.Options{ + Options: &Options{ AuthenticationFunc: authFunc, }, } // Validate the security requirements - err = openapi3filter.ValidateSecurityRequirements(context.TODO(), &req, *route.Operation.Security) + err = ValidateSecurityRequirements(context.Background(), &req, *route.Operation.Security) // If there should have been an error if tc.error { @@ -836,8 +835,8 @@ func TestAllSchemesMet(t *testing.T) { // makeAuthFunc creates an authentication function that accepts the given valid schemes. // If an invalid or unknown scheme is encountered, an error is returned by the returned function. // Otherwise the return value of the returned function is nil. -func makeAuthFunc(schemes map[string]bool) func(c context.Context, input *openapi3filter.AuthenticationInput) error { - return func(c context.Context, input *openapi3filter.AuthenticationInput) error { +func makeAuthFunc(schemes map[string]bool) func(c context.Context, input *AuthenticationInput) error { + return func(c context.Context, input *AuthenticationInput) error { // If the scheme is valid and present in the schemes valid, present := schemes[input.SecuritySchemeName] if valid && present { diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index d94cfce9f..2a58433cb 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -1,11 +1,10 @@ -package openapi3gen_test +package openapi3gen import ( "encoding/json" "testing" "time" - "github.com/getkin/kin-openapi/openapi3gen" "github.com/stretchr/testify/require" ) @@ -17,8 +16,8 @@ type CyclicType1 struct { } func TestCyclic(t *testing.T) { - schema, refsMap, err := openapi3gen.NewSchemaRefForValue(&CyclicType0{}) - require.IsType(t, &openapi3gen.CycleError{}, err) + schema, refsMap, err := NewSchemaRefForValue(&CyclicType0{}) + require.IsType(t, &CycleError{}, err) require.Nil(t, schema) require.Empty(t, refsMap) } @@ -45,7 +44,7 @@ func TestSimple(t *testing.T) { Ptr *ExampleChild `json:"ptr"` } - schema, refsMap, err := openapi3gen.NewSchemaRefForValue(&Example{}) + schema, refsMap, err := NewSchemaRefForValue(&Example{}) require.NoError(t, err) require.Len(t, refsMap, 14) data, err := json.Marshal(schema) From e5faea2a83ab6b4b14ab5090bf411da4ba4a1823 Mon Sep 17 00:00:00 2001 From: Kevin Disneur Date: Mon, 28 Sep 2020 12:24:37 +0200 Subject: [PATCH 018/260] Fix broken link to alternative projects (#255) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6fe16a4d..606839170 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Here's some projects that depend on _kin-openapi_: ## Alternative projects * [go-openapi](https://github.com/go-openapi) * Supports OpenAPI version 2. - * See [this list](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/IMPLEMENTATIONS.md). + * See [this list](https://github.com/OAI/OpenAPI-Specification/blob/master/IMPLEMENTATIONS.md). # Structure * _openapi2_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi2)) From 07f8b1819b00a31fc5f9b517a6f0a218e55a17a2 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 30 Sep 2020 12:14:40 +0200 Subject: [PATCH 019/260] openapi2 security scheme requires accessCode not accesscode (#256) Signed-off-by: Pierre Fenoll --- openapi2conv/openapi2_conv.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 97b00ebda..d06d83874 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -444,7 +444,7 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu switch securityScheme.Flow { case "implicit": flows.Implicit = flow - case "accesscode": + case "accessCode": flows.AuthorizationCode = flow case "password": flows.Password = flow @@ -868,7 +868,7 @@ func FromV3SecurityScheme(swagger *openapi3.Swagger, ref *openapi3.SecuritySchem if flow = flows.Implicit; flow != nil { result.Flow = "implicit" } else if flow = flows.AuthorizationCode; flow != nil { - result.Flow = "accesscode" + result.Flow = "accessCode" } else if flow = flows.Password; flow != nil { result.Flow = "password" } else { From 5c863afc9e9f66c2d7974413c1a15cd08cbd5739 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 21 Oct 2020 09:57:16 +0200 Subject: [PATCH 020/260] Validator: check readOnly/writeOnly properties (#246) --- openapi3/schema.go | 162 ++++++++++++++--------- openapi3/schema_validation_settings.go | 29 ++++ openapi3filter/validate_readonly_test.go | 88 ++++++++++++ openapi3filter/validate_request.go | 2 +- openapi3filter/validate_response.go | 2 +- 5 files changed, 217 insertions(+), 66 deletions(-) create mode 100644 openapi3/schema_validation_settings.go create mode 100644 openapi3filter/validate_readonly_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 8a34282ff..a4e3b8d00 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -397,7 +397,7 @@ func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema { func (schema *Schema) IsEmpty() bool { if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 || schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax || - schema.Nullable || + schema.Nullable || schema.ReadOnly || schema.WriteOnly || schema.AllowEmptyValue || schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil || schema.MinLength != 0 || schema.MaxLength != nil || schema.Pattern != "" || schema.MinItems != 0 || schema.MaxItems != nil || @@ -452,6 +452,10 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { } stack = append(stack, schema) + if schema.ReadOnly && schema.WriteOnly { + return errors.New("A property MUST NOT be marked as both readOnly and writeOnly being true") + } + for _, item := range schema.OneOf { v := item.Value if v == nil { @@ -577,37 +581,44 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { } func (schema *Schema) IsMatching(value interface{}) bool { - return schema.visitJSON(value, true) == nil + settings := newSchemaValidationSettings(FailFast()) + return schema.visitJSON(settings, value) == nil } func (schema *Schema) IsMatchingJSONBoolean(value bool) bool { - return schema.visitJSON(value, true) == nil + settings := newSchemaValidationSettings(FailFast()) + return schema.visitJSON(settings, value) == nil } func (schema *Schema) IsMatchingJSONNumber(value float64) bool { - return schema.visitJSON(value, true) == nil + settings := newSchemaValidationSettings(FailFast()) + return schema.visitJSON(settings, value) == nil } func (schema *Schema) IsMatchingJSONString(value string) bool { - return schema.visitJSON(value, true) == nil + settings := newSchemaValidationSettings(FailFast()) + return schema.visitJSON(settings, value) == nil } func (schema *Schema) IsMatchingJSONArray(value []interface{}) bool { - return schema.visitJSON(value, true) == nil + settings := newSchemaValidationSettings(FailFast()) + return schema.visitJSON(settings, value) == nil } func (schema *Schema) IsMatchingJSONObject(value map[string]interface{}) bool { - return schema.visitJSON(value, true) == nil + settings := newSchemaValidationSettings(FailFast()) + return schema.visitJSON(settings, value) == nil } -func (schema *Schema) VisitJSON(value interface{}) error { - return schema.visitJSON(value, false) +func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOption) error { + settings := newSchemaValidationSettings(opts...) + return schema.visitJSON(settings, value) } -func (schema *Schema) visitJSON(value interface{}, fast bool) (err error) { +func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interface{}) (err error) { switch value := value.(type) { case nil: - return schema.visitJSONNull(fast) + return schema.visitJSONNull(settings) case float64: if math.IsNaN(value) { return ErrSchemaInputNaN @@ -620,23 +631,23 @@ func (schema *Schema) visitJSON(value interface{}, fast bool) (err error) { if schema.IsEmpty() { return } - if err = schema.visitSetOperations(value, fast); err != nil { + if err = schema.visitSetOperations(settings, value); err != nil { return } switch value := value.(type) { case nil: - return schema.visitJSONNull(fast) + return schema.visitJSONNull(settings) case bool: - return schema.visitJSONBoolean(value, fast) + return schema.visitJSONBoolean(settings, value) case float64: - return schema.visitJSONNumber(value, fast) + return schema.visitJSONNumber(settings, value) case string: - return schema.visitJSONString(value, fast) + return schema.visitJSONString(settings, value) case []interface{}: - return schema.visitJSONArray(value, fast) + return schema.visitJSONArray(settings, value) case map[string]interface{}: - return schema.visitJSONObject(value, fast) + return schema.visitJSONObject(settings, value) default: return &SchemaError{ Value: value, @@ -647,14 +658,16 @@ func (schema *Schema) visitJSON(value interface{}, fast bool) (err error) { } } -func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err error) { +func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) { + var oldfailfast bool + if enum := schema.Enum; len(enum) != 0 { for _, v := range enum { if value == v { return } } - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -670,8 +683,9 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro if v == nil { return foundUnresolvedRef(ref.Ref) } - if err := v.visitJSON(value, true); err == nil { - if fast { + oldfailfast, settings.failfast = settings.failfast, true + if err := v.visitJSON(settings, value); err == nil { + if oldfailfast { return errSchema } return &SchemaError{ @@ -680,6 +694,7 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro SchemaField: "not", } } + settings.failfast = oldfailfast } if v := schema.OneOf; len(v) > 0 { @@ -689,12 +704,14 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro if v == nil { return foundUnresolvedRef(item.Ref) } - if err := v.visitJSON(value, true); err == nil { + oldfailfast, settings.failfast = settings.failfast, true + if err := v.visitJSON(settings, value); err == nil { ok++ } + settings.failfast = oldfailfast } if ok != 1 { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -712,13 +729,15 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro if v == nil { return foundUnresolvedRef(item.Ref) } - if err := v.visitJSON(value, true); err == nil { + oldfailfast, settings.failfast = settings.failfast, true + if err := v.visitJSON(settings, value); err == nil { ok = true break } + settings.failfast = oldfailfast } if !ok { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -734,8 +753,9 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro if v == nil { return foundUnresolvedRef(item.Ref) } - if err := v.visitJSON(value, false); err != nil { - if fast { + oldfailfast, settings.failfast = settings.failfast, false + if err := v.visitJSON(settings, value); err != nil { + if oldfailfast { return errSchema } return &SchemaError{ @@ -745,15 +765,16 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro Origin: err, } } + settings.failfast = oldfailfast } return } -func (schema *Schema) visitJSONNull(fast bool) (err error) { +func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) { if schema.Nullable { return } - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -765,25 +786,27 @@ func (schema *Schema) visitJSONNull(fast bool) (err error) { } func (schema *Schema) VisitJSONBoolean(value bool) error { - return schema.visitJSONBoolean(value, false) + settings := newSchemaValidationSettings() + return schema.visitJSONBoolean(settings, value) } -func (schema *Schema) visitJSONBoolean(value bool, fast bool) (err error) { +func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) { if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" { - return schema.expectedType("boolean", fast) + return schema.expectedType(settings, "boolean") } return } func (schema *Schema) VisitJSONNumber(value float64) error { - return schema.visitJSONNumber(value, false) + settings := newSchemaValidationSettings() + return schema.visitJSONNumber(settings, value) } -func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) { +func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) (err error) { schemaType := schema.Type if schemaType == "integer" { if bigFloat := big.NewFloat(value); !bigFloat.IsInt() { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -794,12 +817,12 @@ func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) { } } } else if schemaType != "" && schemaType != "number" { - return schema.expectedType("number, integer", fast) + return schema.expectedType(settings, "number, integer") } // "exclusiveMinimum" if v := schema.ExclusiveMin; v && !(*schema.Min < value) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -812,7 +835,7 @@ func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) { // "exclusiveMaximum" if v := schema.ExclusiveMax; v && !(*schema.Max > value) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -825,7 +848,7 @@ func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) { // "minimum" if v := schema.Min; v != nil && !(*v <= value) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -838,7 +861,7 @@ func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) { // "maximum" if v := schema.Max; v != nil && !(*v >= value) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -854,7 +877,7 @@ func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) { // "A numeric instance is valid only if division by this keyword's // value results in an integer." if bigFloat := big.NewFloat(value / *v); !bigFloat.IsInt() { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -868,12 +891,13 @@ func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) { } func (schema *Schema) VisitJSONString(value string) error { - return schema.visitJSONString(value, false) + settings := newSchemaValidationSettings() + return schema.visitJSONString(settings, value) } -func (schema *Schema) visitJSONString(value string, fast bool) (err error) { +func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) (err error) { if schemaType := schema.Type; schemaType != "" && schemaType != "string" { - return schema.expectedType("string", fast) + return schema.expectedType(settings, "string") } // "minLength" and "maxLength" @@ -890,7 +914,7 @@ func (schema *Schema) visitJSONString(value string, fast bool) (err error) { } } if minLength != 0 && length < int64(minLength) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -901,7 +925,7 @@ func (schema *Schema) visitJSONString(value string, fast bool) (err error) { } } if maxLength != nil && length > int64(*maxLength) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -958,19 +982,20 @@ func (schema *Schema) visitJSONString(value string, fast bool) (err error) { } func (schema *Schema) VisitJSONArray(value []interface{}) error { - return schema.visitJSONArray(value, false) + settings := newSchemaValidationSettings() + return schema.visitJSONArray(settings, value) } -func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error) { +func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) (err error) { if schemaType := schema.Type; schemaType != "" && schemaType != "array" { - return schema.expectedType("array", fast) + return schema.expectedType(settings, "array") } lenValue := int64(len(value)) // "minItems" if v := schema.MinItems; v != 0 && lenValue < int64(v) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -983,7 +1008,7 @@ func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error) // "maxItems" if v := schema.MaxItems; v != nil && lenValue > int64(*v) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -999,7 +1024,7 @@ func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error) sliceUniqueItemsChecker = isSliceOfUniqueItems } if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -1026,12 +1051,13 @@ func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error) } func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { - return schema.visitJSONObject(value, false) + settings := newSchemaValidationSettings() + return schema.visitJSONObject(settings, value) } -func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) (err error) { +func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) (err error) { if schemaType := schema.Type; schemaType != "" && schemaType != "object" { - return schema.expectedType("object", fast) + return schema.expectedType(settings, "object") } // "properties" @@ -1040,7 +1066,7 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) ( // "minProperties" if v := schema.MinProps; v != 0 && lenValue < int64(v) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -1053,7 +1079,7 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) ( // "maxProperties" if v := schema.MaxProps; v != nil && lenValue > int64(*v) { - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -1078,7 +1104,7 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) ( return foundUnresolvedRef(propertyRef.Ref) } if err := p.VisitJSON(v); err != nil { - if fast { + if settings.failfast { return errSchema } return markSchemaErrorKey(err, k) @@ -1090,7 +1116,7 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) ( if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) { if additionalProperties != nil { if err := additionalProperties.VisitJSON(v); err != nil { - if fast { + if settings.failfast { return errSchema } return markSchemaErrorKey(err, k) @@ -1098,7 +1124,7 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) ( } continue } - if fast { + if settings.failfast { return errSchema } return &SchemaError{ @@ -1108,9 +1134,17 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) ( Reason: fmt.Sprintf("Property '%s' is unsupported", k), } } + + // "required" for _, k := range schema.Required { if _, ok := value[k]; !ok { - if fast { + if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asreq { + continue + } + if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asrep { + continue + } + if settings.failfast { return errSchema } return markSchemaErrorKey(&SchemaError{ @@ -1124,8 +1158,8 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) ( return } -func (schema *Schema) expectedType(typ string, fast bool) error { - if fast { +func (schema *Schema) expectedType(settings *schemaValidationSettings, typ string) error { + if settings.failfast { return errSchema } return &SchemaError{ diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go new file mode 100644 index 000000000..6c073cd43 --- /dev/null +++ b/openapi3/schema_validation_settings.go @@ -0,0 +1,29 @@ +package openapi3 + +// SchemaValidationOption describes options a user has when validating request / response bodies. +type SchemaValidationOption func(*schemaValidationSettings) + +type schemaValidationSettings struct { + failfast bool + asreq, asrep bool // exclusive (XOR) fields +} + +// FailFast returns schema validation errors quicker. +func FailFast() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.failfast = true } +} + +func VisitAsRequest() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false } +} +func VisitAsResponse() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true } +} + +func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings { + settings := &schemaValidationSettings{} + for _, opt := range opts { + opt(settings) + } + return settings +} diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go new file mode 100644 index 000000000..fac9bf524 --- /dev/null +++ b/openapi3filter/validate_readonly_test.go @@ -0,0 +1,88 @@ +package openapi3filter + +import ( + "bytes" + "encoding/json" + "net/http" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { + const spec = `{ + "openapi": "3.0.3", + "info": { + "version": "1.0.0", + "title": "title", + "description": "desc", + "contact": { + "email": "email" + } + }, + "paths": { + "/accounts": { + "post": { + "description": "Create a new account", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "description": "Unique identifier for this object.", + "pattern": "[0-9a-v]+$", + "minLength": 20, + "maxLength": 20, + "readOnly": true + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Successfully created a new account" + }, + "400": { + "description": "The server could not understand the request due to invalid syntax", + } + } + } + } + } +} +` + + type Request struct { + ID string `json:"_id"` + } + + sl := openapi3.NewSwaggerLoader() + l, err := sl.LoadSwaggerFromData([]byte(spec)) + require.NoError(t, err) + router := NewRouter().WithSwagger(l) + + b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) + require.NoError(t, err) + + httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b)) + require.NoError(t, err) + httpReq.Header.Add("Content-Type", "application/json") + + route, pathParams, err := router.FindRoute(httpReq.Method, httpReq.URL) + require.NoError(t, err) + + err = ValidateRequest(sl.Context, &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + }) + require.NoError(t, err) +} diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index f22e71efb..69fc58bd1 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -192,7 +192,7 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque } // Validate JSON with the schema - if err := contentType.Schema.Value.VisitJSON(value); err != nil { + if err := contentType.Schema.Value.VisitJSON(value, openapi3.VisitAsRequest()); err != nil { return &RequestError{ Input: input, RequestBody: requestBody, diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index dad0864d2..9a458aa1b 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -129,7 +129,7 @@ func ValidateResponse(c context.Context, input *ResponseValidationInput) error { } // Validate data with the schema. - if err := contentType.Schema.Value.VisitJSON(value); err != nil { + if err := contentType.Schema.Value.VisitJSON(value, openapi3.VisitAsResponse()); err != nil { return &ResponseError{ Input: input, Reason: "response body doesn't match the schema", From a7795f5709c62b0c84306db83ddf3481b0ee31ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A3=AE=20=E5=84=AA=E5=A4=AA?= <59682979+uta-mori@users.noreply.github.com> Date: Sun, 25 Oct 2020 17:06:19 +0900 Subject: [PATCH 021/260] feat: add Goa to README (#261) Goa v3 depend on kin-openapi https://github.com/goadesign/goa/blob/v3/go.mod --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 606839170..bc4a733d7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Here's some projects that depend on _kin-openapi_: * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPI 3 spec * [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs" * [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "...a CLI for interacting with REST-ish HTTP APIs with some nice features built-in" + * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Goa is a framework for building micro-services and APIs in Go using a unique design-first approach." * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request) ## Alternative projects From d9b54aff9ba984568f50998940cd22ff7e0992c2 Mon Sep 17 00:00:00 2001 From: FrancisLennon17 Date: Mon, 26 Oct 2020 15:04:41 +0000 Subject: [PATCH 022/260] swagger2 formData & request body refs (#260) Co-authored-by: Francis Lennon --- openapi2conv/openapi2_conv.go | 400 ++++++++++++++++++++--------- openapi2conv/openapi2_conv_test.go | 64 ++++- 2 files changed, 337 insertions(+), 127 deletions(-) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index d06d83874..fbf2b099c 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -2,6 +2,7 @@ package openapi2conv import ( + "encoding/json" "errors" "fmt" "net/url" @@ -40,35 +41,40 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { } } - if paths := swagger.Paths; paths != nil { - resultPaths := make(map[string]*openapi3.PathItem, len(paths)) - for path, pathItem := range paths { - r, err := ToV3PathItem(swagger, pathItem) - if err != nil { - return nil, err - } - resultPaths[path] = r - } - result.Paths = resultPaths - } - - if parameters := swagger.Parameters; parameters != nil { - result.Components.Parameters = make(map[string]*openapi3.ParameterRef, len(parameters)) - result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef, len(parameters)) + result.Components.Schemas = make(map[string]*openapi3.SchemaRef) + if parameters := swagger.Parameters; len(parameters) != 0 { + result.Components.Parameters = make(map[string]*openapi3.ParameterRef) + result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) for k, parameter := range parameters { - v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) + v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&result.Components, parameter) switch { case err != nil: return nil, err case v3RequestBody != nil: result.Components.RequestBodies[k] = v3RequestBody + case v3SchemaMap != nil: + for _, v3Schema := range v3SchemaMap { + result.Components.Schemas[k] = v3Schema + } default: result.Components.Parameters[k] = v3Parameter } } } - if responses := swagger.Responses; responses != nil { + if paths := swagger.Paths; len(paths) != 0 { + resultPaths := make(map[string]*openapi3.PathItem, len(paths)) + for path, pathItem := range paths { + r, err := ToV3PathItem(swagger, &result.Components, pathItem) + if err != nil { + return nil, err + } + resultPaths[path] = r + } + result.Paths = resultPaths + } + + if responses := swagger.Responses; len(responses) != 0 { result.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses)) for k, response := range responses { r, err := ToV3Response(response) @@ -79,9 +85,11 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { } } - result.Components.Schemas = ToV3Schemas(swagger.Definitions) + for key, schema := range ToV3Schemas(swagger.Definitions) { + result.Components.Schemas[key] = schema + } - if m := swagger.SecurityDefinitions; m != nil { + if m := swagger.SecurityDefinitions; len(m) != 0 { resultSecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef) for k, v := range m { r, err := ToV3SecurityScheme(v) @@ -94,7 +102,6 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { } result.Security = ToV3SecurityRequirements(swagger.Security) - { sl := openapi3.NewSwaggerLoader() if err := sl.ResolveRefsIn(result, nil); err != nil { @@ -104,25 +111,27 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { return result, nil } -func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*openapi3.PathItem, error) { +func ToV3PathItem(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem) (*openapi3.PathItem, error) { stripNonCustomExtensions(pathItem.Extensions) result := &openapi3.PathItem{ ExtensionProps: pathItem.ExtensionProps, } for method, operation := range pathItem.Operations() { - resultOperation, err := ToV3Operation(swagger, pathItem, operation) + resultOperation, err := ToV3Operation(swagger, components, pathItem, operation) if err != nil { return nil, err } result.SetOperation(method, resultOperation) } for _, parameter := range pathItem.Parameters { - v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) + v3Parameter, v3RequestBody, v3Schema, err := ToV3Parameter(components, parameter) switch { case err != nil: return nil, err case v3RequestBody != nil: return nil, errors.New("pathItem must not have a body parameter") + case v3Schema != nil: + return nil, errors.New("pathItem must not have a schema parameter") default: result.Parameters = append(result.Parameters, v3Parameter) } @@ -130,7 +139,7 @@ func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*open return result, nil } -func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, operation *openapi2.Operation) (*openapi3.Operation, error) { +func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation) (*openapi3.Operation, error) { if operation == nil { return nil, nil } @@ -148,19 +157,24 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera } var reqBodies []*openapi3.RequestBodyRef + formDataSchemas := make(map[string]*openapi3.SchemaRef) for _, parameter := range operation.Parameters { - v3Parameter, v3RequestBody, err := ToV3Parameter(parameter) + v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(components, parameter) switch { case err != nil: return nil, err case v3RequestBody != nil: reqBodies = append(reqBodies, v3RequestBody) + case v3SchemaMap != nil: + for key, v3Schema := range v3SchemaMap { + formDataSchemas[key] = v3Schema + } default: result.Parameters = append(result.Parameters, v3Parameter) } } var err error - if result.RequestBody, err = onlyOneReqBodyParam(reqBodies); err != nil { + if result.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components); err != nil { return nil, err } @@ -178,9 +192,31 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera return result, nil } -func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, error) { +func getParameterNameFromOldRef(ref string) string { + cleanPath := strings.TrimPrefix(ref, "#/parameters/") + pathSections := strings.SplitN(cleanPath, "/", 1) + + return pathSections[0] +} + +func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, map[string]*openapi3.SchemaRef, error) { if ref := parameter.Ref; ref != "" { - return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil + if strings.HasPrefix(ref, "#/parameters/") { + name := getParameterNameFromOldRef(ref) + if _, ok := components.RequestBodies[name]; ok { + v3Ref := strings.Replace(ref, "#/parameters/", "#/components/requestBodies/", 1) + return nil, &openapi3.RequestBodyRef{Ref: v3Ref}, nil, nil + } else if schema, ok := components.Schemas[name]; ok { + schemaRefMap := make(map[string]*openapi3.SchemaRef) + if val, ok := schema.Value.Extensions["x-formData-name"]; ok { + name = val.(string) + } + v3Ref := strings.Replace(ref, "#/parameters/", "#/components/schemas/", 1) + schemaRefMap[name] = &openapi3.SchemaRef{Ref: v3Ref} + return nil, nil, schemaRefMap, nil + } + } + return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil, nil } stripNonCustomExtensions(parameter.Extensions) @@ -191,52 +227,63 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open Required: parameter.Required, ExtensionProps: parameter.ExtensionProps, } + if parameter.Name != "" { + result.Extensions["x-originalParamName"] = parameter.Name + } + if schemaRef := parameter.Schema; schemaRef != nil { // Assuming JSON result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef)) } - return nil, &openapi3.RequestBodyRef{Value: result}, nil + return nil, &openapi3.RequestBodyRef{Value: result}, nil, nil case "formData": format, typ := parameter.Format, parameter.Type if typ == "file" { format, typ = "binary", "string" } - reqBodyRef := formDataBody( - map[string]*openapi3.SchemaRef{ - parameter.Name: { - Value: &openapi3.Schema{ - Description: parameter.Description, - Type: typ, - ExtensionProps: parameter.ExtensionProps, - Format: format, - Enum: parameter.Enum, - Min: parameter.Minimum, - Max: parameter.Maximum, - ExclusiveMin: parameter.ExclusiveMin, - ExclusiveMax: parameter.ExclusiveMax, - MinLength: parameter.MinLength, - MaxLength: parameter.MaxLength, - Default: parameter.Default, - Items: parameter.Items, - MinItems: parameter.MinItems, - MaxItems: parameter.MaxItems, - Pattern: parameter.Pattern, - AllowEmptyValue: parameter.AllowEmptyValue, - UniqueItems: parameter.UniqueItems, - MultipleOf: parameter.MultipleOf, - }, - }, + if parameter.ExtensionProps.Extensions == nil { + parameter.ExtensionProps.Extensions = make(map[string]interface{}) + } + parameter.ExtensionProps.Extensions["x-formData-name"] = parameter.Name + var required []string + if parameter.Required { + required = []string{parameter.Name} + } + schemaRef := &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Description: parameter.Description, + Type: typ, + ExtensionProps: parameter.ExtensionProps, + Format: format, + Enum: parameter.Enum, + Min: parameter.Minimum, + Max: parameter.Maximum, + ExclusiveMin: parameter.ExclusiveMin, + ExclusiveMax: parameter.ExclusiveMax, + MinLength: parameter.MinLength, + MaxLength: parameter.MaxLength, + Default: parameter.Default, + Items: parameter.Items, + MinItems: parameter.MinItems, + MaxItems: parameter.MaxItems, + Pattern: parameter.Pattern, + AllowEmptyValue: parameter.AllowEmptyValue, + UniqueItems: parameter.UniqueItems, + MultipleOf: parameter.MultipleOf, + Required: required, }, - map[string]bool{parameter.Name: parameter.Required}, - ) - return nil, reqBodyRef, nil + } + schemaRefMap := make(map[string]*openapi3.SchemaRef) + schemaRefMap[parameter.Name] = schemaRef + return nil, nil, schemaRefMap, nil default: required := parameter.Required if parameter.In == openapi3.ParameterInPath { required = true } + result := &openapi3.Parameter{ In: parameter.In, Name: parameter.Name, @@ -263,7 +310,7 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open MultipleOf: parameter.MultipleOf, }}), } - return &openapi3.ParameterRef{Value: result}, nil, nil + return &openapi3.ParameterRef{Value: result}, nil, nil, nil } } @@ -290,43 +337,59 @@ func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool) * } } -func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef) (*openapi3.RequestBodyRef, error) { - var ( - body *openapi3.RequestBodyRef - formDataParams map[string]*openapi3.SchemaRef - formDataReqs map[string]bool - ) - for i, requestBodyRef := range bodies { - mediaType := requestBodyRef.Value.GetMediaType("multipart/form-data") - if mediaType != nil { - for name, schemaRef := range mediaType.Schema.Value.Properties { - if formDataParams == nil { - formDataParams = make(map[string]*openapi3.SchemaRef, len(bodies)-i) - } - if formDataReqs == nil { - formDataReqs = make(map[string]bool, len(bodies)-i) +func getParameterNameFromNewRef(ref string) string { + cleanPath := strings.TrimPrefix(ref, "#/components/schemas/") + pathSections := strings.SplitN(cleanPath, "/", 1) + + return pathSections[0] +} + +func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (*openapi3.RequestBodyRef, error) { + if len(bodies) > 1 { + return nil, errors.New("multiple body parameters cannot exist for the same operation") + } + + if len(bodies) != 0 && len(formDataSchemas) != 0 { + return nil, errors.New("body and form parameters cannot exist together for the same operation") + } + + for _, requestBodyRef := range bodies { + return requestBodyRef, nil + } + + if len(formDataSchemas) > 0 { + formDataParams := make(map[string]*openapi3.SchemaRef, len(formDataSchemas)) + formDataReqs := make(map[string]bool, len(formDataSchemas)) + for formDataName, formDataSchema := range formDataSchemas { + if formDataSchema.Ref != "" { + name := getParameterNameFromNewRef(formDataSchema.Ref) + if schema := components.Schemas[name]; schema != nil && schema.Value != nil { + if tempName, ok := schema.Value.Extensions["x-formData-name"]; ok { + name = tempName.(string) + } + formDataParams[name] = formDataSchema + formDataReqs[name] = false + for _, req := range schema.Value.Required { + if name == req { + formDataReqs[name] = true + } + } } - formDataParams[name] = schemaRef - formDataReqs[name] = false - for _, req := range mediaType.Schema.Value.Required { - if name == req { - formDataReqs[name] = true + } else if formDataSchema.Value != nil { + formDataParams[formDataName] = formDataSchema + formDataReqs[formDataName] = false + for _, req := range formDataSchema.Value.Required { + if formDataName == req { + formDataReqs[formDataName] = true } } - break } - } else { - body = requestBodyRef } - } - switch { - case len(formDataParams) != 0 && body != nil: - return nil, errors.New("body and form parameters cannot exist together for the same operation") - case len(formDataParams) != 0: + return formDataBody(formDataParams, formDataReqs), nil - default: - return body, nil } + + return nil, nil } func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) { @@ -395,6 +458,8 @@ func FromV3Ref(ref string) string { for new, old := range ref2To3 { if strings.HasPrefix(ref, old) { ref = strings.Replace(ref, old, new, 1) + } else if strings.HasPrefix(ref, "#/components/requestBodies/") { + ref = strings.Replace(ref, "#/components/requestBodies/", "#/parameters/", 1) } } return ref @@ -460,16 +525,17 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu // FromV3Swagger converts an OpenAPIv3 spec to an OpenAPIv2 spec func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { - resultResponses, err := FromV3Responses(swagger.Components.Responses) + resultResponses, err := FromV3Responses(swagger.Components.Responses, &swagger.Components) if err != nil { return nil, err } stripNonCustomExtensions(swagger.Extensions) - + schemas, parameters := FromV3Schemas(swagger.Components.Schemas, &swagger.Components) result := &openapi2.Swagger{ Swagger: "2.0", Info: *swagger.Info, - Definitions: FromV3Schemas(swagger.Components.Schemas), + Definitions: schemas, + Parameters: parameters, Responses: resultResponses, Tags: swagger.Tags, ExtensionProps: swagger.ExtensionProps, @@ -520,7 +586,7 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { } params := openapi2.Parameters{} for _, param := range pathItem.Parameters { - p, err := FromV3Parameter(param) + p, err := FromV3Parameter(param, &swagger.Components) if err != nil { return nil, err } @@ -528,12 +594,35 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { } result.Paths[path].Parameters = params } - result.Parameters = map[string]*openapi2.Parameter{} + for name, param := range swagger.Components.Parameters { - if result.Parameters[name], err = FromV3Parameter(param); err != nil { + if result.Parameters[name], err = FromV3Parameter(param, &swagger.Components); err != nil { return nil, err } } + + for name, requestBody := range swagger.Components.RequestBodies { + parameters := FromV3RequestBodyFormData(requestBody) + for _, param := range parameters { + result.Parameters[param.Name] = param + } + + if len(parameters) == 0 { + paramName := name + if requestBody.Value != nil { + if originalName, ok := requestBody.Value.Extensions["x-originalParamName"]; ok { + json.Unmarshal(originalName.(json.RawMessage), ¶mName) + } + } + + r, err := FromV3RequestBody(swagger, paramName, requestBody) + if err != nil { + return nil, err + } + result.Parameters[name] = r + } + } + if m := swagger.Components.SecuritySchemes; m != nil { resultSecuritySchemes := make(map[string]*openapi2.SecurityScheme) for id, securityScheme := range m { @@ -549,34 +638,89 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { return result, nil } -func FromV3Schemas(schemas map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef { - v2Defs := make(map[string]*openapi3.SchemaRef, len(schemas)) +func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) { + v2Defs := make(map[string]*openapi3.SchemaRef) + v2Params := make(map[string]*openapi2.Parameter) for name, schema := range schemas { - v2Defs[name] = FromV3SchemaRef(schema) + schemaConv, parameterConv := FromV3SchemaRef(schema, components) + if schemaConv != nil { + v2Defs[name] = schemaConv + } else if parameterConv != nil { + if parameterConv.Name == "" { + parameterConv.Name = name + } + v2Params[name] = parameterConv + } } - return v2Defs + return v2Defs, v2Params } -func FromV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { +func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) { if ref := schema.Ref; ref != "" { - return &openapi3.SchemaRef{Ref: FromV3Ref(ref)} + name := getParameterNameFromNewRef(ref) + if val, ok := components.Schemas[name]; ok { + if val.Value.Format == "binary" { + v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1) + return nil, &openapi2.Parameter{Ref: v2Ref} + } + } + + return &openapi3.SchemaRef{Ref: FromV3Ref(ref)}, nil } if schema.Value == nil { - return schema + return schema, nil + } + + if schema.Value != nil { + if schema.Value.Type == "string" && schema.Value.Format == "binary" { + paramType := "file" + required := false + + value, _ := schema.Value.Extensions["x-formData-name"] + var originalName string + json.Unmarshal(value.(json.RawMessage), &originalName) + for _, prop := range schema.Value.Required { + if originalName == prop { + required = true + } + } + return nil, &openapi2.Parameter{ + In: "formData", + Name: originalName, + Description: schema.Value.Description, + Type: paramType, + Enum: schema.Value.Enum, + Minimum: schema.Value.Min, + Maximum: schema.Value.Max, + ExclusiveMin: schema.Value.ExclusiveMin, + ExclusiveMax: schema.Value.ExclusiveMax, + MinLength: schema.Value.MinLength, + MaxLength: schema.Value.MaxLength, + Default: schema.Value.Default, + Items: schema.Value.Items, + MinItems: schema.Value.MinItems, + MaxItems: schema.Value.MaxItems, + AllowEmptyValue: schema.Value.AllowEmptyValue, + UniqueItems: schema.Value.UniqueItems, + MultipleOf: schema.Value.MultipleOf, + ExtensionProps: schema.Value.ExtensionProps, + Required: required, + } + } } if v := schema.Value.Items; v != nil { - schema.Value.Items = FromV3SchemaRef(v) + schema.Value.Items, _ = FromV3SchemaRef(v, components) } for k, v := range schema.Value.Properties { - schema.Value.Properties[k] = FromV3SchemaRef(v) + schema.Value.Properties[k], _ = FromV3SchemaRef(v, components) } if v := schema.Value.AdditionalProperties; v != nil { - schema.Value.AdditionalProperties = FromV3SchemaRef(v) + schema.Value.AdditionalProperties, _ = FromV3SchemaRef(v, components) } for i, v := range schema.Value.AllOf { - schema.Value.AllOf[i] = FromV3SchemaRef(v) + schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) } - return schema + return schema, nil } func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements { @@ -603,7 +747,7 @@ func FromV3PathItem(swagger *openapi3.Swagger, pathItem *openapi3.PathItem) (*op result.SetOperation(method, r) } for _, parameter := range pathItem.Parameters { - p, err := FromV3Parameter(parameter) + p, err := FromV3Parameter(parameter, &swagger.Components) if err != nil { return nil, err } @@ -633,6 +777,11 @@ func FromV3RequestBodyFormData(requestBodyRef *openapi3.RequestBodyRef) openapi2 } parameters := openapi2.Parameters{} for propName, schemaRef := range mediaType.Schema.Value.Properties { + if ref := schemaRef.Ref; ref != "" { + v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1) + parameters = append(parameters, &openapi2.Parameter{Ref: v2Ref}) + continue + } val := schemaRef.Value typ := val.Type if val.Format == "binary" { @@ -692,7 +841,7 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( result.Security = &resultSecurity } for _, parameter := range operation.Parameters { - r, err := FromV3Parameter(parameter) + r, err := FromV3Parameter(parameter, &swagger.Components) if err != nil { return nil, err } @@ -703,20 +852,28 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( if len(parameters) > 0 { result.Parameters = append(result.Parameters, parameters...) } else { - r, err := FromV3RequestBody(swagger, operation, v) + // Find parameter name that we can use for the body + name := findNameForRequestBody(operation) + if name == "" { + return nil, errors.New("could not find a name for request body") + } + r, err := FromV3RequestBody(swagger, name, v) if err != nil { return nil, err } result.Parameters = append(result.Parameters, r) } } + for _, param := range result.Parameters { if param.Type == "file" { result.Consumes = append(result.Consumes, "multipart/form-data") + break } } + if responses := operation.Responses; responses != nil { - resultResponses, err := FromV3Responses(responses) + resultResponses, err := FromV3Responses(responses, &swagger.Components) if err != nil { return nil, err } @@ -725,19 +882,12 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( return result, nil } -func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation, requestBodyRef *openapi3.RequestBodyRef) (*openapi2.Parameter, error) { +func FromV3RequestBody(swagger *openapi3.Swagger, name string, requestBodyRef *openapi3.RequestBodyRef) (*openapi2.Parameter, error) { if ref := requestBodyRef.Ref; ref != "" { return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil } requestBody := requestBodyRef.Value - // Find parameter name that we can use for the body - name := findNameForRequestBody(operation) - - // If found an available name - if name == "" { - return nil, errors.New("Could not find a name for request body") - } stripNonCustomExtensions(requestBody.Extensions) result := &openapi2.Parameter{ In: "body", @@ -750,12 +900,12 @@ func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation, // Assuming JSON mediaType := requestBody.GetMediaType("application/json") if mediaType != nil { - result.Schema = FromV3SchemaRef(mediaType.Schema) + result.Schema, _ = FromV3SchemaRef(mediaType.Schema, &swagger.Components) } return result, nil } -func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) { +func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components) (*openapi2.Parameter, error) { if ref := ref.Ref; ref != "" { return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil } @@ -772,7 +922,7 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) { ExtensionProps: parameter.ExtensionProps, } if schemaRef := parameter.Schema; schemaRef != nil { - schemaRef = FromV3SchemaRef(schemaRef) + schemaRef, _ = FromV3SchemaRef(schemaRef, components) schema := schemaRef.Value result.Type = schema.Type result.Format = schema.Format @@ -796,10 +946,10 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) { return result, nil } -func FromV3Responses(responses map[string]*openapi3.ResponseRef) (map[string]*openapi2.Response, error) { +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 { - r, err := FromV3Response(response) + r, err := FromV3Response(response, components) if err != nil { return nil, err } @@ -808,7 +958,7 @@ func FromV3Responses(responses map[string]*openapi3.ResponseRef) (map[string]*op return v2Responses, nil } -func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) { +func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error) { if ref := ref.Ref; ref != "" { return &openapi2.Response{Ref: FromV3Ref(ref)}, nil } @@ -828,7 +978,7 @@ func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) { } if content := response.Content; content != nil { if ct := content["application/json"]; ct != nil { - result.Schema = FromV3SchemaRef(ct.Schema) + result.Schema, _ = FromV3SchemaRef(ct.Schema, components) } } return result, nil diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index 8ccc32ce4..c5379ae06 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -94,6 +94,24 @@ const exampleV2 = ` "name": "banana", "required": true, "type": "string" + }, + "put_body": { + "in": "body", + "name": "banana", + "required": true, + "schema": { + "type": "string" + }, + "x-originalParamName":"banana" + }, + "post_form_ref": { + "required": true, + "description": "param description", + "in": "formData", + "name": "fileUpload2", + "type": "file", + "x-formData-name":"fileUpload2", + "x-mimetype": "text/plain" } }, "paths": { @@ -214,6 +232,7 @@ const exampleV2 = ` "schema": { "allOf": [{"$ref": "#/definitions/Item"}] }, + "x-originalParamName":"body", "x-requestBody": "requestbody extension 1" } ], @@ -238,13 +257,18 @@ const exampleV2 = ` "in": "formData", "name": "fileUpload", "type": "file", + "x-formData-name":"fileUpload", "x-mimetype": "text/plain" }, { "description": "Description of file contents", "in": "formData", "name": "note", - "type": "integer" + "type": "integer", + "x-formData-name":"note" + }, + { + "$ref": "#/parameters/post_form_ref" } ], "responses": { @@ -255,6 +279,11 @@ const exampleV2 = ` }, "put": { "description": "example put", + "parameters": [ + { + "$ref": "#/parameters/put_body" + } + ], "responses": { "default": { "description": "default response" @@ -309,6 +338,19 @@ const exampleV3 = ` } } }, + "requestBodies": { + "put_body": { + "content":{ + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true, + "x-originalParamName":"banana" + } + }, "responses": { "ForbiddenError": { "content": { @@ -349,6 +391,14 @@ const exampleV3 = ` "ItemExtension": { "type": "boolean", "description": "It could be anything." + }, + "post_form_ref": { + "description": "param description", + "format": "binary", + "required": ["fileUpload2"], + "type": "string", + "x-formData-name": "fileUpload2", + "x-mimetype":"text/plain" } } }, @@ -491,6 +541,7 @@ const exampleV3 = ` } } }, + "x-originalParamName":"body", "x-requestBody": "requestbody extension 1" }, "responses": { @@ -520,13 +571,19 @@ const exampleV3 = ` "description": "param description", "type": "string", "format": "binary", + "x-formData-name":"fileUpload", "x-mimetype": "text/plain" }, "note": { "description": "Description of file contents", - "type": "integer" + "type": "integer", + "x-formData-name": "note" + }, + "fileUpload2": { + "$ref": "#/components/schemas/post_form_ref" } }, + "required": ["fileUpload2"], "type": "object" } } @@ -540,6 +597,9 @@ const exampleV3 = ` }, "put": { "description": "example put", + "requestBody": { + "$ref": "#/components/requestBodies/put_body" + }, "responses": { "default": { "description": "default response" From 25cec2f721904d8d181c7dc73c3511708f8e3d50 Mon Sep 17 00:00:00 2001 From: Zachary Lozano Date: Mon, 26 Oct 2020 17:21:28 -0500 Subject: [PATCH 023/260] Add support for error aggregation for request/response validation (#259) --- openapi3/errors.go | 43 ++++++ openapi3/schema.go | 187 +++++++++++++++++++++---- openapi3/schema_test.go | 154 +++++++++++++++++++- openapi3/schema_validation_settings.go | 5 + openapi3filter/options.go | 1 + openapi3filter/validate_request.go | 61 +++++++- openapi3filter/validate_response.go | 8 +- 7 files changed, 420 insertions(+), 39 deletions(-) create mode 100644 openapi3/errors.go diff --git a/openapi3/errors.go b/openapi3/errors.go new file mode 100644 index 000000000..ce52cd483 --- /dev/null +++ b/openapi3/errors.go @@ -0,0 +1,43 @@ +package openapi3 + +import ( + "bytes" + "errors" +) + +// MultiError is a collection of errors, intended for when +// multiple issues need to be reported upstream +type MultiError []error + +func (me MultiError) Error() string { + buff := &bytes.Buffer{} + for _, e := range me { + buff.WriteString(e.Error()) + buff.WriteString(" | ") + } + return buff.String() +} + +//Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()` +//It will also return true if any of the contained errors match target +func (me MultiError) Is(target error) bool { + if _, ok := target.(MultiError); ok { + return true + } + for _, e := range me { + if errors.Is(e, target) { + return true + } + } + return false +} + +//As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type +func (me MultiError) As(target interface{}) bool { + for _, e := range me { + if errors.As(e, target) { + return true + } + } + return false +} diff --git a/openapi3/schema.go b/openapi3/schema.go index a4e3b8d00..41d6fd909 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -802,19 +802,24 @@ func (schema *Schema) VisitJSONNumber(value float64) error { return schema.visitJSONNumber(settings, value) } -func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) (err error) { +func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error { + var me MultiError schemaType := schema.Type if schemaType == "integer" { if bigFloat := big.NewFloat(value); !bigFloat.IsInt() { if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "type", Reason: "Value must be an integer", } + if !settings.multiError { + return err + } + me = append(me, err) } } else if schemaType != "" && schemaType != "number" { return schema.expectedType(settings, "number, integer") @@ -825,12 +830,16 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "exclusiveMinimum", Reason: fmt.Sprintf("Number must be more than %g", *schema.Min), } + if !settings.multiError { + return err + } + me = append(me, err) } // "exclusiveMaximum" @@ -838,12 +847,16 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "exclusiveMaximum", Reason: fmt.Sprintf("Number must be less than %g", *schema.Max), } + if !settings.multiError { + return err + } + me = append(me, err) } // "minimum" @@ -851,12 +864,16 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "minimum", Reason: fmt.Sprintf("Number must be at least %g", *v), } + if !settings.multiError { + return err + } + me = append(me, err) } // "maximum" @@ -864,12 +881,16 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "maximum", Reason: fmt.Sprintf("Number must be most %g", *v), } + if !settings.multiError { + return err + } + me = append(me, err) } // "multipleOf" @@ -880,14 +901,23 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "multipleOf", } + if !settings.multiError { + return err + } + me = append(me, err) } } - return + + if len(me) > 0 { + return me + } + + return nil } func (schema *Schema) VisitJSONString(value string) error { @@ -895,11 +925,13 @@ func (schema *Schema) VisitJSONString(value string) error { return schema.visitJSONString(settings, value) } -func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) (err error) { +func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error { if schemaType := schema.Type; schemaType != "" && schemaType != "string" { return schema.expectedType(settings, "string") } + var me MultiError + // "minLength" and "maxLength" minLength := schema.MinLength maxLength := schema.MaxLength @@ -917,23 +949,31 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "minLength", Reason: fmt.Sprintf("Minimum string length is %d", minLength), } + if !settings.multiError { + return err + } + me = append(me, err) } if maxLength != nil && length > int64(*maxLength) { if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "maxLength", Reason: fmt.Sprintf("Maximum string length is %d", *maxLength), } + if !settings.multiError { + return err + } + me = append(me, err) } } @@ -970,15 +1010,24 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value if schema.Pattern != "" { field = "pattern" } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: field, Reason: cp.ErrReason, } + if !settings.multiError { + return err + } + me = append(me, err) } } - return + + if len(me) > 0 { + return me + } + + return nil } func (schema *Schema) VisitJSONArray(value []interface{}) error { @@ -986,11 +1035,13 @@ func (schema *Schema) VisitJSONArray(value []interface{}) error { return schema.visitJSONArray(settings, value) } -func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) (err error) { +func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error { if schemaType := schema.Type; schemaType != "" && schemaType != "array" { return schema.expectedType(settings, "array") } + var me MultiError + lenValue := int64(len(value)) // "minItems" @@ -998,12 +1049,16 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "minItems", Reason: fmt.Sprintf("Minimum number of items is %d", v), } + if !settings.multiError { + return err + } + me = append(me, err) } // "maxItems" @@ -1011,12 +1066,16 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "maxItems", Reason: fmt.Sprintf("Maximum number of items is %d", *v), } + if !settings.multiError { + return err + } + me = append(me, err) } // "uniqueItems" @@ -1027,12 +1086,16 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "uniqueItems", Reason: fmt.Sprintf("Duplicate items found"), } + if !settings.multiError { + return err + } + me = append(me, err) } // "items" @@ -1042,12 +1105,25 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ return foundUnresolvedRef(itemSchemaRef.Ref) } for i, item := range value { - if err := itemSchema.VisitJSON(item); err != nil { - return markSchemaErrorIndex(err, i) + if err := itemSchema.visitJSON(settings, item); err != nil { + err = markSchemaErrorIndex(err, i) + if !settings.multiError { + return err + } + if itemMe, ok := err.(MultiError); ok { + me = append(me, itemMe...) + } else { + me = append(me, err) + } } } } - return + + if len(me) > 0 { + return me + } + + return nil } func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { @@ -1055,11 +1131,13 @@ func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { return schema.visitJSONObject(settings, value) } -func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) (err error) { +func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error { if schemaType := schema.Type; schemaType != "" && schemaType != "object" { return schema.expectedType(settings, "object") } + var me MultiError + // "properties" properties := schema.Properties lenValue := int64(len(value)) @@ -1069,12 +1147,16 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "minProperties", Reason: fmt.Sprintf("There must be at least %d properties", v), } + if !settings.multiError { + return err + } + me = append(me, err) } // "maxProperties" @@ -1082,12 +1164,16 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "maxProperties", Reason: fmt.Sprintf("There must be at most %d properties", *v), } + if !settings.multiError { + return err + } + me = append(me, err) } // "additionalProperties" @@ -1103,11 +1189,19 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value if p == nil { return foundUnresolvedRef(propertyRef.Ref) } - if err := p.VisitJSON(v); err != nil { + if err := p.visitJSON(settings, v); err != nil { if settings.failfast { return errSchema } - return markSchemaErrorKey(err, k) + err = markSchemaErrorKey(err, k) + if !settings.multiError { + return err + } + if v, ok := err.(MultiError); ok { + me = append(me, v...) + continue + } + me = append(me, err) } continue } @@ -1115,11 +1209,19 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value allowed := schema.AdditionalPropertiesAllowed if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) { if additionalProperties != nil { - if err := additionalProperties.VisitJSON(v); err != nil { + if err := additionalProperties.visitJSON(settings, v); err != nil { if settings.failfast { return errSchema } - return markSchemaErrorKey(err, k) + err = markSchemaErrorKey(err, k) + if !settings.multiError { + return err + } + if v, ok := err.(MultiError); ok { + me = append(me, v...) + continue + } + me = append(me, err) } } continue @@ -1127,12 +1229,16 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return &SchemaError{ + err := &SchemaError{ Value: value, Schema: schema, SchemaField: "properties", Reason: fmt.Sprintf("Property '%s' is unsupported", k), } + if !settings.multiError { + return err + } + me = append(me, err) } // "required" @@ -1147,15 +1253,24 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value if settings.failfast { return errSchema } - return markSchemaErrorKey(&SchemaError{ + err := markSchemaErrorKey(&SchemaError{ Value: value, Schema: schema, SchemaField: "required", Reason: fmt.Sprintf("Property '%s' is missing", k), }, k) + if !settings.multiError { + return err + } + me = append(me, err) } } - return + + if len(me) > 0 { + return me + } + + return nil } func (schema *Schema) expectedType(settings *schemaValidationSettings, typ string) error { @@ -1184,6 +1299,12 @@ func markSchemaErrorKey(err error, key string) error { v.reversePath = append(v.reversePath, key) return v } + if v, ok := err.(MultiError); ok { + for _, e := range v { + _ = markSchemaErrorKey(e, key) + } + return v + } return err } @@ -1192,6 +1313,12 @@ func markSchemaErrorIndex(err error, index int) error { v.reversePath = append(v.reversePath, strconv.FormatInt(int64(index), 10)) return v } + if v, ok := err.(MultiError); ok { + for _, e := range v { + _ = markSchemaErrorIndex(e, index) + } + return v + } return err } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 10e1d0589..6650ad547 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -4,7 +4,9 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" "math" + "reflect" "strings" "testing" @@ -59,13 +61,13 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) { } } -func validateSchema(t *testing.T, schema *Schema, value interface{}) error { +func validateSchema(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error { data, err := json.Marshal(value) require.NoError(t, err) var val interface{} err = json.Unmarshal(data, &val) require.NoError(t, err) - return schema.VisitJSON(val) + return schema.VisitJSON(val, opts...) } var schemaExamples = []schemaExample{ @@ -1042,3 +1044,151 @@ var schemaErrorExamples = []schemaErrorExample{ Want: "NEST", }, } + +type schemaMultiErrorExample struct { + Title string + Schema *Schema + Values []interface{} + ExpectedErrors []MultiError +} + +func TestSchemasMultiError(t *testing.T) { + for _, example := range schemaMultiErrorExamples { + t.Run(example.Title, testSchemaMultiError(t, example)) + } +} + +func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*testing.T) { + return func(t *testing.T) { + schema := example.Schema + for i, value := range example.Values { + err := validateSchema(t, schema, value, MultiErrors()) + require.Error(t, err) + require.IsType(t, MultiError{}, err) + + merr, _ := err.(MultiError) + expected := example.ExpectedErrors[i] + require.True(t, len(merr) > 0) + require.Len(t, merr, len(expected)) + for _, e := range merr { + require.IsType(t, &SchemaError{}, e) + var found bool + scherr, _ := e.(*SchemaError) + for _, expectedErr := range expected { + expectedScherr, _ := expectedErr.(*SchemaError) + if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) && + expectedScherr.SchemaField == scherr.SchemaField { + found = true + break + } + } + require.True(t, found, fmt.Sprintf("Missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))) + } + } + } +} + +var schemaMultiErrorExamples = []schemaMultiErrorExample{ + { + Title: "STRING", + Schema: NewStringSchema(). + WithMinLength(2). + WithMaxLength(3). + WithPattern("^[abc]+$"), + Values: []interface{}{ + "f", + "foobar", + }, + ExpectedErrors: []MultiError{ + {&SchemaError{SchemaField: "minLength"}, &SchemaError{SchemaField: "pattern"}}, + {&SchemaError{SchemaField: "maxLength"}, &SchemaError{SchemaField: "pattern"}}, + }, + }, + { + Title: "NUMBER", + Schema: NewIntegerSchema(). + WithMin(1). + WithMax(10), + Values: []interface{}{ + 0.5, + 10.1, + }, + ExpectedErrors: []MultiError{ + {&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "minimum"}}, + {&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "maximum"}}, + }, + }, + { + Title: "ARRAY: simple", + Schema: NewArraySchema(). + WithMinItems(2). + WithMaxItems(2). + WithItems(NewStringSchema(). + WithPattern("^[abc]+$")), + Values: []interface{}{ + []interface{}{"foo"}, + []interface{}{"foo", "bar", "fizz"}, + }, + ExpectedErrors: []MultiError{ + { + &SchemaError{SchemaField: "minItems"}, + &SchemaError{SchemaField: "pattern", reversePath: []string{"0"}}, + }, + { + &SchemaError{SchemaField: "maxItems"}, + &SchemaError{SchemaField: "pattern", reversePath: []string{"0"}}, + &SchemaError{SchemaField: "pattern", reversePath: []string{"1"}}, + &SchemaError{SchemaField: "pattern", reversePath: []string{"2"}}, + }, + }, + }, + { + Title: "ARRAY: object", + Schema: NewArraySchema(). + WithItems(NewObjectSchema(). + WithProperties(map[string]*Schema{ + "key1": NewStringSchema(), + "key2": NewIntegerSchema(), + }), + ), + Values: []interface{}{ + []interface{}{ + map[string]interface{}{ + "key1": 100, // not a string + "key2": "not an integer", + }, + }, + }, + ExpectedErrors: []MultiError{ + { + &SchemaError{SchemaField: "type", reversePath: []string{"key1", "0"}}, + &SchemaError{SchemaField: "type", reversePath: []string{"key2", "0"}}, + }, + }, + }, + { + Title: "OBJECT", + Schema: NewObjectSchema(). + WithProperties(map[string]*Schema{ + "key1": NewStringSchema(), + "key2": NewIntegerSchema(), + "key3": NewArraySchema(). + WithItems(NewStringSchema(). + WithPattern("^[abc]+$")), + }), + Values: []interface{}{ + map[string]interface{}{ + "key1": 100, // not a string + "key2": "not an integer", + "key3": []interface{}{"abc", "def"}, + }, + }, + ExpectedErrors: []MultiError{ + { + &SchemaError{SchemaField: "type", reversePath: []string{"key1"}}, + &SchemaError{SchemaField: "type", reversePath: []string{"key2"}}, + &SchemaError{SchemaField: "pattern", reversePath: []string{"1", "key3"}}, + }, + }, + }, +} diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 6c073cd43..71db5f237 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -5,6 +5,7 @@ type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { failfast bool + multiError bool asreq, asrep bool // exclusive (XOR) fields } @@ -13,6 +14,10 @@ func FailFast() SchemaValidationOption { return func(s *schemaValidationSettings) { s.failfast = true } } +func MultiErrors() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.multiError = true } +} + func VisitAsRequest() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false } } diff --git a/openapi3filter/options.go b/openapi3filter/options.go index 510b77756..60e5475f1 100644 --- a/openapi3filter/options.go +++ b/openapi3filter/options.go @@ -10,5 +10,6 @@ type Options struct { ExcludeRequestBody bool ExcludeResponseBody bool IncludeResponseStatus bool + MultiError bool AuthenticationFunc func(c context.Context, input *AuthenticationInput) error } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 69fc58bd1..0af54c299 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -22,6 +22,11 @@ var ErrInvalidRequired = errors.New("must have a value") // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker func ValidateRequest(c context.Context, input *RequestValidationInput) error { + var ( + err error + me openapi3.MultiError + ) + options := input.Options if options == nil { options = DefaultOptions @@ -45,24 +50,37 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { continue } } - if err := ValidateParameter(c, input, parameter); err != nil { + + if err = ValidateParameter(c, input, parameter); err != nil && !options.MultiError { return err } + + if err != nil { + me = append(me, err) + } } // For each parameter of the Operation for _, parameter := range operationParameters { - if err := ValidateParameter(c, input, parameter.Value); err != nil { + if err = ValidateParameter(c, input, parameter.Value); err != nil && !options.MultiError { return err } + + if err != nil { + me = append(me, err) + } } // RequestBody requestBody := operation.RequestBody if requestBody != nil && !options.ExcludeRequestBody { - if err := ValidateRequestBody(c, input, requestBody.Value); err != nil { + if err = ValidateRequestBody(c, input, requestBody.Value); err != nil && !options.MultiError { return err } + + if err != nil { + me = append(me, err) + } } // Security @@ -76,10 +94,19 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { security = &route.Swagger.Security } if security != nil { - if err := ValidateSecurityRequirements(c, input, *security); err != nil { + if err = ValidateSecurityRequirements(c, input, *security); err != nil && !options.MultiError { return err } + + if err != nil { + me = append(me, err) + } } + + if len(me) > 0 { + return me + } + return nil } @@ -95,6 +122,11 @@ func ValidateParameter(c context.Context, input *RequestValidationInput, paramet return nil } + options := input.Options + if options == nil { + options = DefaultOptions + } + var value interface{} var err error var schema *openapi3.Schema @@ -121,7 +153,13 @@ func ValidateParameter(c context.Context, input *RequestValidationInput, paramet // A parameter's schema is not defined so skip validation of a parameter's value. return nil } - if err = schema.VisitJSON(value); err != nil { + + var opts []openapi3.SchemaValidationOption + if options.MultiError { + opts = make([]openapi3.SchemaValidationOption, 0, 1) + opts = append(opts, openapi3.MultiErrors()) + } + if err = schema.VisitJSON(value, opts...); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } return nil @@ -137,6 +175,11 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque data []byte ) + options := input.Options + if options == nil { + options = DefaultOptions + } + if req.Body != http.NoBody && req.Body != nil { defer req.Body.Close() var err error @@ -191,8 +234,14 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque } } + opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here + opts = append(opts, openapi3.VisitAsRequest()) + if options.MultiError { + opts = append(opts, openapi3.MultiErrors()) + } + // Validate JSON with the schema - if err := contentType.Schema.Value.VisitJSON(value, openapi3.VisitAsRequest()); err != nil { + if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { return &RequestError{ Input: input, RequestBody: requestBody, diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index 9a458aa1b..f203802a4 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -128,8 +128,14 @@ func ValidateResponse(c context.Context, input *ResponseValidationInput) error { } } + opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here + opts = append(opts, openapi3.VisitAsRequest()) + if options.MultiError { + opts = append(opts, openapi3.MultiErrors()) + } + // Validate data with the schema. - if err := contentType.Schema.Value.VisitJSON(value, openapi3.VisitAsResponse()); err != nil { + if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { return &ResponseError{ Input: input, Reason: "response body doesn't match the schema", From fc3f90d8183b71442d5730b8e6cff877c9b93e45 Mon Sep 17 00:00:00 2001 From: Richard Rance Date: Tue, 27 Oct 2020 10:07:26 -0600 Subject: [PATCH 024/260] Prevent a panic in the error encoder (#262) --- openapi3filter/fixtures/petstore.json | 22 ++++++++++--- openapi3filter/validation_error_encoder.go | 5 +-- openapi3filter/validation_error_test.go | 36 ++++++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/openapi3filter/fixtures/petstore.json b/openapi3filter/fixtures/petstore.json index 6c229a672..398e9b861 100644 --- a/openapi3filter/fixtures/petstore.json +++ b/openapi3filter/fixtures/petstore.json @@ -67,7 +67,21 @@ ], "requestBody": { "$ref": "#/components/requestBodies/PetWithRequired" - } + }, + "parameters": [ + { + "schema": { + "type": "string", + "enum": [ + "demo", + "prod" + ] + }, + "in": "header", + "name": "x-environment", + "description": "Where to send the data for processing" + } + ] }, "patch": { "tags": [ @@ -1136,7 +1150,7 @@ }, "name": { "type": "string", - "example": "doggie", + "example": "doggie" }, "photoUrls": { "type": "array", @@ -1196,7 +1210,7 @@ }, "name": { "type": "string", - "example": "doggie", + "example": "doggie" }, "photoUrls": { "type": "array", @@ -1330,4 +1344,4 @@ } } } -} \ No newline at end of file +} diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 6bde134a5..34e4af94d 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -148,7 +148,7 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida } // Add error source - if e.Parameter != nil && e.Parameter.In == "query" { + if e.Parameter != nil { // We have a JSONPointer in the query param too so need to // make sure 'Parameter' check takes priority over 'Pointer' cErr.Source = &ValidationErrorSource{ @@ -172,7 +172,8 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida cErr.Detail = fmt.Sprintf("Value '%v' at %s must be one of: %s", innerErr.Value, toJSONPointer(innerErr.JSONPointer()), strings.Join(enums, ", ")) value := fmt.Sprintf("%v", innerErr.Value) - if (e.Parameter.Explode == nil || *e.Parameter.Explode == true) && + if e.Parameter != nil && + (e.Parameter.Explode == nil || *e.Parameter.Explode == true) && (e.Parameter.Style == "" || e.Parameter.Style == "form") && strings.Contains(value, ",") { parts := strings.Split(value, ",") diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 737ceeb9d..0a28ab6a3 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -74,6 +74,9 @@ func getValidationTests(t *testing.T) []*validationTest { unsupportedContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) unsupportedContentType.Header.Set("Content-Type", "text/plain") + unsupportedHeaderValue := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) + unsupportedHeaderValue.Header.Set("x-environment", "watdis") + return []*validationTest{ // // Basics @@ -270,10 +273,43 @@ func getValidationTests(t *testing.T) []*validationTest { }, }, + // + // Request header params + // + { + name: "error - invalid enum value for header string parameter", + args: validationArgs{ + r: unsupportedHeaderValue, + }, + wantErrParam: "x-environment", + wantErrParamIn: "header", + wantErrSchemaReason: "JSON value is not one of the allowed values", + wantErrSchemaPath: "/", + wantErrSchemaValue: "watdis", + wantErrResponse: &ValidationError{Status: http.StatusBadRequest, + Title: "JSON value is not one of the allowed values", + Detail: "Value 'watdis' at / must be one of: demo, prod", + Source: &ValidationErrorSource{Parameter: "x-environment"}}, + }, + // // Request bodies // + { + name: "error - invalid enum value for header object attribute", + args: validationArgs{ + r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), + }, + wantErrReason: "doesn't match the schema", + wantErrSchemaReason: "JSON value is not one of the allowed values", + wantErrSchemaValue: "watdis", + wantErrSchemaPath: "/status", + wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, + Title: "JSON value is not one of the allowed values", + Detail: "Value 'watdis' at /status must be one of: available, pending, sold", + Source: &ValidationErrorSource{Pointer: "/status"}}, + }, { name: "error - missing required object attribute", args: validationArgs{ From d96b8169b6eb7b86a6e4bbed680c3e155d1692cf Mon Sep 17 00:00:00 2001 From: Riccardo Manfrin Date: Thu, 12 Nov 2020 11:50:20 +0100 Subject: [PATCH 025/260] Adds ipv4 and ipv6 formats support (#258) Co-authored-by: Pierre Fenoll --- openapi3/schema.go | 17 +++++---- openapi3/schema_formats.go | 71 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 41d6fd909..dd59d3f4a 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -994,13 +994,18 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value schema.compiledPattern = cp } else if v := schema.Format; len(v) > 0 { // No pattern, but does have a format - re := SchemaStringFormats[v] - if re != nil { - cp = &compiledPattern{ - Regexp: re, - ErrReason: "JSON string doesn't match the format '" + v + " (regular expression `" + re.String() + "`)'", + if f, ok := SchemaStringFormats[v]; ok { + if f.regexp != nil && f.callback == nil { + schema.compiledPattern = &compiledPattern{ + Regexp: f.regexp, + ErrReason: "JSON string doesn't match the format '" + v + " (regular expression `" + f.regexp.String() + "`)'", + } + + } else if f.regexp == nil && f.callback != nil { + return f.callback(value) + } else { + return fmt.Errorf("corrupted entry %q in SchemaStringFormats", v) } - schema.compiledPattern = cp } } } diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 746e40882..ab2992bc9 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -2,6 +2,7 @@ package openapi3 import ( "fmt" + "net" "regexp" ) @@ -10,15 +11,70 @@ const ( FormatOfStringForUUIDOfRFC4122 = `^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$` ) -var SchemaStringFormats = make(map[string]*regexp.Regexp, 8) +//FormatCallback custom check on exotic formats +type FormatCallback func(Val string) error +type Format struct { + regexp *regexp.Regexp + callback FormatCallback +} + +//SchemaStringFormats allows for validating strings format +var SchemaStringFormats = make(map[string]Format, 8) + +//DefineStringFormat Defines a new regexp pattern for a given format func DefineStringFormat(name string, pattern string) { re, err := regexp.Compile(pattern) if err != nil { err := fmt.Errorf("Format '%v' has invalid pattern '%v': %v", name, pattern, err) panic(err) } - SchemaStringFormats[name] = re + SchemaStringFormats[name] = Format{regexp: re} +} + +// DefineStringFormatCallback adds a validation function for a specific schema format entry +func DefineStringFormatCallback(name string, callback FormatCallback) { + SchemaStringFormats[name] = Format{callback: callback} +} + +func validateIP(ip string) (*net.IP, error) { + parsed := net.ParseIP(ip) + if parsed == nil { + return nil, &SchemaError{ + Value: ip, + Reason: "Not an IP address", + } + } + return &parsed, nil +} + +func validateIPv4(ip string) error { + parsed, err := validateIP(ip) + if err != nil { + return err + } + + if parsed.To4() == nil { + return &SchemaError{ + Value: ip, + Reason: "Not an IPv4 address (it's IPv6)", + } + } + return nil +} +func validateIPv6(ip string) error { + parsed, err := validateIP(ip) + if err != nil { + return err + } + + if parsed.To4() != nil { + return &SchemaError{ + Value: ip, + Reason: "Not an IPv6 address (it's IPv4)", + } + } + return nil } func init() { @@ -35,4 +91,15 @@ func init() { // date-time DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`) + +} + +// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec +func DefineIPv4Format() { + DefineStringFormatCallback("ipv4", validateIPv4) +} + +// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec +func DefineIPv6Format() { + DefineStringFormatCallback("ipv6", validateIPv6) } From 738fe87e5f58238a6b1a17fc3d15dbd9f8f3551f Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 12 Nov 2020 12:31:32 +0100 Subject: [PATCH 026/260] validate pattern or schema, not pattern xor schema anymore (#265) --- .github/workflows/go.yml | 4 +- openapi3/schema.go | 101 +++++++++++++++++++++------------------ 2 files changed, 57 insertions(+), 48 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 37e7d3825..1d3c890e6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -26,9 +26,11 @@ jobs: - run: go version - run: go get ./... - - run: go test ./... - run: go vet ./... - run: go fmt ./... + - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + shell: bash + - run: go test ./... - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] shell: bash - run: go get -u -a -v ./... && go mod tidy && go mod verify diff --git a/openapi3/schema.go b/openapi3/schema.go index dd59d3f4a..d020c0e3c 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -24,7 +24,9 @@ var ( errSchema = errors.New("Input does not match the schema") + // ErrSchemaInputNaN may be returned when validating a number ErrSchemaInputNaN = errors.New("NaN is not allowed") + // ErrSchemaInputInf may be returned when validating a number ErrSchemaInputInf = errors.New("Inf is not allowed") ) @@ -89,7 +91,7 @@ type Schema struct { MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` - compiledPattern *compiledPattern + compiledPattern *regexp.Regexp // Array MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` @@ -225,11 +227,6 @@ func NewObjectSchema() *Schema { } } -type compiledPattern struct { - Regexp *regexp.Regexp - ErrReason string -} - func (schema *Schema) WithNullable() *Schema { schema.Nullable = true return schema @@ -977,49 +974,15 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } } - // "format" and "pattern" - cp := schema.compiledPattern - if cp == nil { - pattern := schema.Pattern - if v := schema.Pattern; len(v) > 0 { - // Pattern - re, err := regexp.Compile(v) - if err != nil { - return fmt.Errorf("Error while compiling regular expression '%s': %v", pattern, err) - } - cp = &compiledPattern{ - Regexp: re, - ErrReason: "JSON string doesn't match the regular expression '" + v + "'", - } - schema.compiledPattern = cp - } else if v := schema.Format; len(v) > 0 { - // No pattern, but does have a format - if f, ok := SchemaStringFormats[v]; ok { - if f.regexp != nil && f.callback == nil { - schema.compiledPattern = &compiledPattern{ - Regexp: f.regexp, - ErrReason: "JSON string doesn't match the format '" + v + " (regular expression `" + f.regexp.String() + "`)'", - } - - } else if f.regexp == nil && f.callback != nil { - return f.callback(value) - } else { - return fmt.Errorf("corrupted entry %q in SchemaStringFormats", v) - } - } - } - } - if cp != nil { - if !cp.Regexp.MatchString(value) { - field := "format" - if schema.Pattern != "" { - field = "pattern" - } - err := &SchemaError{ + // "pattern" + if pattern := schema.Pattern; pattern != "" && schema.compiledPattern == nil { + var err error + if schema.compiledPattern, err = regexp.Compile(pattern); err != nil { + err = &SchemaError{ Value: value, Schema: schema, - SchemaField: field, - Reason: cp.ErrReason, + SchemaField: "pattern", + Reason: fmt.Sprintf("cannot compile pattern %q: %v", pattern, err), } if !settings.multiError { return err @@ -1027,6 +990,50 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value me = append(me, err) } } + if cp := schema.compiledPattern; cp != nil && !cp.MatchString(value) { + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "pattern", + Reason: fmt.Sprintf("JSON string doesn't match the regular expression %q", schema.Pattern), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "format" + var formatErr string + if format := schema.Format; format != "" { + if f, ok := SchemaStringFormats[format]; ok { + switch { + case f.regexp != nil && f.callback == nil: + if cp := f.regexp; !cp.MatchString(value) { + formatErr = fmt.Sprintf("JSON string doesn't match the format %q (regular expression %q)", format, cp.String()) + } + case f.regexp == nil && f.callback != nil: + if err := f.callback(value); err != nil { + formatErr = err.Error() + } + default: + formatErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format) + } + } + } + if formatErr != "" { + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "format", + Reason: formatErr, + } + if !settings.multiError { + return err + } + me = append(me, err) + + } if len(me) > 0 { return me From 2e4cbb269bfc6dfb39a79ffcaf671c7aa2d8951a Mon Sep 17 00:00:00 2001 From: FrancisLennon17 Date: Sat, 14 Nov 2020 14:12:13 +0000 Subject: [PATCH 027/260] Consumes request bodies (#263) Co-authored-by: Francis Lennon Co-authored-by: Pierre Fenoll --- .github/workflows/go.yml | 3 +- openapi2/openapi2.go | 1 + openapi2conv/issue187_test.go | 2 +- openapi2conv/openapi2_conv.go | 170 ++++++++++++++++++----------- openapi2conv/openapi2_conv_test.go | 24 +++- openapi3/content.go | 26 +++++ openapi3/request_body.go | 10 ++ 7 files changed, 169 insertions(+), 67 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1d3c890e6..f8f3669fc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -25,7 +25,8 @@ jobs: go-version: 1.x - run: go version - - run: go get ./... + - run: go mod download && go mod verify + - run: go test ./... - run: go vet ./... - run: go fmt ./... - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 247775257..5e4877b96 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -21,6 +21,7 @@ type Swagger struct { Info openapi3.Info `json:"info"` ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"` Schemes []string `json:"schemes,omitempty"` + Consumes []string `json:"consumes,omitempty"` Host string `json:"host,omitempty"` BasePath string `json:"basePath,omitempty"` Paths map[string]*PathItem `json:"paths,omitempty"` diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index 979866c34..16a6d1a1c 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -155,7 +155,7 @@ paths: get: requestBody: content: - application/json: + '*/*': schema: $ref: '#/components/schemas/TestRef' responses: diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index fbf2b099c..42c4f6f75 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/url" + "sort" "strings" "github.com/getkin/kin-openapi/openapi2" @@ -46,7 +47,7 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { result.Components.Parameters = make(map[string]*openapi3.ParameterRef) result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) for k, parameter := range parameters { - v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&result.Components, parameter) + v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&result.Components, parameter, swagger.Consumes) switch { case err != nil: return nil, err @@ -65,7 +66,7 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { if paths := swagger.Paths; len(paths) != 0 { resultPaths := make(map[string]*openapi3.PathItem, len(paths)) for path, pathItem := range paths { - r, err := ToV3PathItem(swagger, &result.Components, pathItem) + r, err := ToV3PathItem(swagger, &result.Components, pathItem, swagger.Consumes) if err != nil { return nil, err } @@ -111,20 +112,20 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { return result, nil } -func ToV3PathItem(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem) (*openapi3.PathItem, error) { +func ToV3PathItem(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) { stripNonCustomExtensions(pathItem.Extensions) result := &openapi3.PathItem{ ExtensionProps: pathItem.ExtensionProps, } for method, operation := range pathItem.Operations() { - resultOperation, err := ToV3Operation(swagger, components, pathItem, operation) + resultOperation, err := ToV3Operation(swagger, components, pathItem, operation, consumes) if err != nil { return nil, err } result.SetOperation(method, resultOperation) } for _, parameter := range pathItem.Parameters { - v3Parameter, v3RequestBody, v3Schema, err := ToV3Parameter(components, parameter) + v3Parameter, v3RequestBody, v3Schema, err := ToV3Parameter(components, parameter, consumes) switch { case err != nil: return nil, err @@ -139,7 +140,7 @@ func ToV3PathItem(swagger *openapi2.Swagger, components *openapi3.Components, pa return result, nil } -func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation) (*openapi3.Operation, error) { +func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation, consumes []string) (*openapi3.Operation, error) { if operation == nil { return nil, nil } @@ -156,10 +157,14 @@ func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, p result.Security = &resultSecurity } + if len(operation.Consumes) > 0 { + consumes = operation.Consumes + } + var reqBodies []*openapi3.RequestBodyRef formDataSchemas := make(map[string]*openapi3.SchemaRef) for _, parameter := range operation.Parameters { - v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(components, parameter) + v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(components, parameter, consumes) switch { case err != nil: return nil, err @@ -174,7 +179,7 @@ func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, p } } var err error - if result.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components); err != nil { + if result.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components, consumes); err != nil { return nil, err } @@ -199,7 +204,7 @@ func getParameterNameFromOldRef(ref string) string { return pathSections[0] } -func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, map[string]*openapi3.SchemaRef, error) { +func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter, consumes []string) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, map[string]*openapi3.SchemaRef, error) { if ref := parameter.Ref; ref != "" { if strings.HasPrefix(ref, "#/parameters/") { name := getParameterNameFromOldRef(ref) @@ -233,7 +238,7 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete if schemaRef := parameter.Schema; schemaRef != nil { // Assuming JSON - result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef)) + result.WithSchemaRef(ToV3SchemaRef(schemaRef), consumes) } return nil, &openapi3.RequestBodyRef{Value: result}, nil, nil @@ -314,7 +319,7 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete } } -func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool) *openapi3.RequestBodyRef { +func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool, consumes []string) *openapi3.RequestBodyRef { if len(bodies) != len(reqs) { panic(`request bodies and them being required must match`) } @@ -333,7 +338,7 @@ func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool) * Required: requireds, } return &openapi3.RequestBodyRef{ - Value: openapi3.NewRequestBody().WithFormDataSchema(schema), + Value: openapi3.NewRequestBody().WithSchema(schema, consumes), } } @@ -344,7 +349,7 @@ func getParameterNameFromNewRef(ref string) string { return pathSections[0] } -func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (*openapi3.RequestBodyRef, error) { +func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[string]*openapi3.SchemaRef, components *openapi3.Components, consumes []string) (*openapi3.RequestBodyRef, error) { if len(bodies) > 1 { return nil, errors.New("multiple body parameters cannot exist for the same operation") } @@ -386,7 +391,7 @@ func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[ } } - return formDataBody(formDataParams, formDataReqs), nil + return formDataBody(formDataParams, formDataReqs, consumes), nil } return nil, nil @@ -601,25 +606,23 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { } } - for name, requestBody := range swagger.Components.RequestBodies { - parameters := FromV3RequestBodyFormData(requestBody) - for _, param := range parameters { - result.Parameters[param.Name] = param + for name, requestBodyRef := range swagger.Components.RequestBodies { + bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, &swagger.Components) + if err != nil { + return nil, err } - - if len(parameters) == 0 { - paramName := name - if requestBody.Value != nil { - if originalName, ok := requestBody.Value.Extensions["x-originalParamName"]; ok { - json.Unmarshal(originalName.(json.RawMessage), ¶mName) - } + if len(formDataParameters) != 0 { + for _, param := range formDataParameters { + result.Parameters[param.Name] = param } - - r, err := FromV3RequestBody(swagger, paramName, requestBody) - if err != nil { - return nil, err + } else if len(bodyOrRefParameters) != 0 { + for _, param := range bodyOrRefParameters { + result.Parameters[name] = param } - result.Parameters[name] = r + } + + if len(consumes) != 0 { + result.Consumes = consumesToArray(consumes) } } @@ -638,6 +641,52 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { return result, nil } +func consumesToArray(consumes map[string]struct{}) []string { + consumesArr := make([]string, 0, len(consumes)) + for key := range consumes { + consumesArr = append(consumesArr, key) + } + sort.Strings(consumesArr) + return consumesArr +} + +func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, components *openapi3.Components) ( + bodyOrRefParameters openapi2.Parameters, + formParameters openapi2.Parameters, + consumes map[string]struct{}, + err error, +) { + if ref := requestBodyRef.Ref; ref != "" { + bodyOrRefParameters = append(bodyOrRefParameters, &openapi2.Parameter{Ref: FromV3Ref(ref)}) + return + } + + //Only select one formData or request body for an individual requesstBody as swagger 2 does not support multiples + if requestBodyRef.Value != nil { + for contentType, mediaType := range requestBodyRef.Value.Content { + if consumes == nil { + consumes = make(map[string]struct{}) + } + consumes[contentType] = struct{}{} + if formParams := FromV3RequestBodyFormData(mediaType); len(formParams) != 0 { + formParameters = formParams + } else { + paramName := name + if originalName, ok := requestBodyRef.Value.Extensions["x-originalParamName"]; ok { + json.Unmarshal(originalName.(json.RawMessage), ¶mName) + } + + var r *openapi2.Parameter + if r, err = FromV3RequestBody(paramName, requestBodyRef, mediaType, components); err != nil { + return + } + bodyOrRefParameters = append(bodyOrRefParameters, r) + } + } + } + return +} + func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) { v2Defs := make(map[string]*openapi3.SchemaRef) v2Params := make(map[string]*openapi2.Parameter) @@ -711,8 +760,13 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components if v := schema.Value.Items; v != nil { schema.Value.Items, _ = FromV3SchemaRef(v, components) } - for k, v := range schema.Value.Properties { - schema.Value.Properties[k], _ = FromV3SchemaRef(v, components) + keys := make([]string, 0, len(schema.Value.Properties)) + for k := range schema.Value.Properties { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + schema.Value.Properties[key], _ = FromV3SchemaRef(schema.Value.Properties[key], components) } if v := schema.Value.AdditionalProperties; v != nil { schema.Value.AdditionalProperties, _ = FromV3SchemaRef(v, components) @@ -770,11 +824,7 @@ nameSearch: return "" } -func FromV3RequestBodyFormData(requestBodyRef *openapi3.RequestBodyRef) openapi2.Parameters { - mediaType := requestBodyRef.Value.GetMediaType("multipart/form-data") - if mediaType == nil { - return nil - } +func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameters { parameters := openapi2.Parameters{} for propName, schemaRef := range mediaType.Schema.Value.Properties { if ref := schemaRef.Ref; ref != "" { @@ -848,27 +898,28 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( result.Parameters = append(result.Parameters, r) } if v := operation.RequestBody; v != nil { - parameters := FromV3RequestBodyFormData(operation.RequestBody) - if len(parameters) > 0 { - result.Parameters = append(result.Parameters, parameters...) - } else { - // Find parameter name that we can use for the body - name := findNameForRequestBody(operation) - if name == "" { - return nil, errors.New("could not find a name for request body") - } - r, err := FromV3RequestBody(swagger, name, v) - if err != nil { - return nil, err + // Find parameter name that we can use for the body + name := findNameForRequestBody(operation) + if name == "" { + return nil, errors.New("could not find a name for request body") + } + + bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, &swagger.Components) + if err != nil { + return nil, err + } + if len(formDataParameters) != 0 { + result.Parameters = append(result.Parameters, formDataParameters...) + } else if len(bodyOrRefParameters) != 0 { + for _, param := range bodyOrRefParameters { + result.Parameters = append(result.Parameters, param) + break // add a single request body } - result.Parameters = append(result.Parameters, r) + } - } - for _, param := range result.Parameters { - if param.Type == "file" { - result.Consumes = append(result.Consumes, "multipart/form-data") - break + if len(consumes) != 0 { + result.Consumes = consumesToArray(consumes) } } @@ -882,10 +933,7 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( return result, nil } -func FromV3RequestBody(swagger *openapi3.Swagger, name string, requestBodyRef *openapi3.RequestBodyRef) (*openapi2.Parameter, error) { - if ref := requestBodyRef.Ref; ref != "" { - return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil - } +func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, mediaType *openapi3.MediaType, components *openapi3.Components) (*openapi2.Parameter, error) { requestBody := requestBodyRef.Value stripNonCustomExtensions(requestBody.Extensions) @@ -897,10 +945,8 @@ func FromV3RequestBody(swagger *openapi3.Swagger, name string, requestBodyRef *o ExtensionProps: requestBody.ExtensionProps, } - // Assuming JSON - mediaType := requestBody.GetMediaType("application/json") if mediaType != nil { - result.Schema, _ = FromV3SchemaRef(mediaType.Schema, &swagger.Components) + result.Schema, _ = FromV3SchemaRef(mediaType.Schema, components) } return result, nil } diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index c5379ae06..ad3a3e8b7 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -88,6 +88,10 @@ const exampleV2 = ` "version": "0.1", "x-info": "info extension" }, + "consumes": [ + "application/json", + "application/xml" + ], "parameters": { "banana": { "in": "path", @@ -224,15 +228,19 @@ const exampleV2 = ` } }, "patch": { + "consumes": [ + "application/json", + "application/xml" + ], "description": "example patch", "parameters": [ { "in": "body", - "name": "body", + "name": "patch_body", "schema": { "allOf": [{"$ref": "#/definitions/Item"}] }, - "x-originalParamName":"body", + "x-originalParamName":"patch_body", "x-requestBody": "requestbody extension 1" } ], @@ -345,6 +353,11 @@ const exampleV3 = ` "schema": { "type": "string" } + }, + "application/xml": { + "schema": { + "type": "string" + } } }, "required": true, @@ -539,9 +552,14 @@ const exampleV3 = ` "schema": { "allOf": [{"$ref": "#/components/schemas/Item"}] } + }, + "application/xml": { + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}] + } } }, - "x-originalParamName":"body", + "x-originalParamName":"patch_body", "x-requestBody": "requestbody extension 1" }, "responses": { diff --git a/openapi3/content.go b/openapi3/content.go index f28912c66..abe376e3e 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -12,6 +12,32 @@ func NewContent() Content { return make(map[string]*MediaType, 4) } +func NewContentWithSchema(schema *Schema, consumes []string) Content { + if len(consumes) == 0 { + return Content{ + "*/*": NewMediaType().WithSchema(schema), + } + } + content := make(map[string]*MediaType, len(consumes)) + for _, mediaType := range consumes { + content[mediaType] = NewMediaType().WithSchema(schema) + } + return content +} + +func NewContentWithSchemaRef(schema *SchemaRef, consumes []string) Content { + if len(consumes) == 0 { + return Content{ + "*/*": NewMediaType().WithSchemaRef(schema), + } + } + content := make(map[string]*MediaType, len(consumes)) + for _, mediaType := range consumes { + content[mediaType] = NewMediaType().WithSchemaRef(schema) + } + return content +} + func NewContentWithJSONSchema(schema *Schema) Content { return Content{ "application/json": NewMediaType().WithSchema(schema), diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 56a055ba2..6d649ca4b 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -33,6 +33,16 @@ func (requestBody *RequestBody) WithContent(content Content) *RequestBody { return requestBody } +func (requestBody *RequestBody) WithSchemaRef(value *SchemaRef, consumes []string) *RequestBody { + requestBody.Content = NewContentWithSchemaRef(value, consumes) + return requestBody +} + +func (requestBody *RequestBody) WithSchema(value *Schema, consumes []string) *RequestBody { + requestBody.Content = NewContentWithSchema(value, consumes) + return requestBody +} + func (requestBody *RequestBody) WithJSONSchemaRef(value *SchemaRef) *RequestBody { requestBody.Content = NewContentWithJSONSchemaRef(value) return requestBody From da714f42fa6c62b1eead22b7545b843c24bb8644 Mon Sep 17 00:00:00 2001 From: Samuel Monderer Date: Sun, 15 Nov 2020 21:06:41 +0200 Subject: [PATCH 028/260] fixed panic in path validation (issue #264) (#266) Co-authored-by: Samuel Monderer --- openapi3/paths.go | 5 +++++ openapi3/paths_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 openapi3/paths_test.go diff --git a/openapi3/paths.go b/openapi3/paths.go index 0bfeb2c79..baafaaabc 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -16,6 +16,11 @@ func (paths Paths) Validate(c context.Context) error { return fmt.Errorf("path %q does not start with a forward slash (/)", path) } + if pathItem == nil { + paths[path] = &PathItem{} + pathItem = paths[path] + } + normalizedPath, pathParamsCount := normalizeTemplatedPath(path) if oldPath, ok := normalizedPaths[normalizedPath]; ok { return fmt.Errorf("conflicting paths %q and %q", path, oldPath) diff --git a/openapi3/paths_test.go b/openapi3/paths_test.go new file mode 100644 index 000000000..cc8d9306c --- /dev/null +++ b/openapi3/paths_test.go @@ -0,0 +1,28 @@ +package openapi3 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +var emptyPathSpec = ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: +` + +func TestPathValidate(t *testing.T) { + swagger, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(emptyPathSpec)) + require.NoError(t, err) + err = swagger.Paths.Validate(context.Background()) + require.NoError(t, err) +} From c928496cd13ef6bcf7e9e865cc4b2a7786338a4d Mon Sep 17 00:00:00 2001 From: duohedron <40067856+duohedron@users.noreply.github.com> Date: Fri, 20 Nov 2020 09:50:10 +0100 Subject: [PATCH 029/260] Update doc.go (#272) --- openapi3/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi3/doc.go b/openapi3/doc.go index 9f9554962..efe8b4a0c 100644 --- a/openapi3/doc.go +++ b/openapi3/doc.go @@ -1,5 +1,5 @@ // Package openapi3 parses and writes OpenAPI 3 specifications. // // The OpenAPI 3.0 specification can be found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.md +// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md package openapi3 From 254be1228068a4d2dc5210a03e5680ff1024a3a6 Mon Sep 17 00:00:00 2001 From: heyvister <41934916+heyvister@users.noreply.github.com> Date: Sun, 22 Nov 2020 19:16:37 +0200 Subject: [PATCH 030/260] Exposing Components 'IdentifierRegExp' to enable customized component key #270 (#273) --- openapi3/components.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openapi3/components.go b/openapi3/components.go index a8ebd7926..2708f95fb 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -94,10 +94,13 @@ func (components *Components) Validate(c context.Context) (err error) { const identifierPattern = `^[a-zA-Z0-9._-]+$` -var identifierRegExp = regexp.MustCompile(identifierPattern) +// IdentifierRegExp verifies whether Component object key matches 'identifierPattern' pattern, according to OapiAPI v3.x.0. +// Hovever, to be able supporting legacy OpenAPI v2.x, there is a need to customize above pattern in orde not to fail +// converted v2-v3 validation +var IdentifierRegExp = regexp.MustCompile(identifierPattern) func ValidateIdentifier(value string) error { - if identifierRegExp.MatchString(value) { + if IdentifierRegExp.MatchString(value) { return nil } return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (regexp: %q)", value, identifierPattern) From e8b3436bcf8d9ef0a90735b83ba23a0f651540dc Mon Sep 17 00:00:00 2001 From: DanielXu77 <52269333+DanielXu77@users.noreply.github.com> Date: Wed, 25 Nov 2020 17:54:36 -0500 Subject: [PATCH 031/260] Add support for application/problem+json (#275) Add support for content type application/problem+json for response validation --- openapi3filter/req_resp_decoder.go | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 3b1de11f1..163bc69b2 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -826,6 +826,7 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, func init() { RegisterBodyDecoder("text/plain", plainBodyDecoder) RegisterBodyDecoder("application/json", jsonBodyDecoder) + RegisterBodyDecoder("application/problem+json", jsonBodyDecoder) RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder) RegisterBodyDecoder("multipart/form-data", multipartBodyDecoder) RegisterBodyDecoder("application/octet-stream", FileBodyDecoder) From 9b9280d707773bb764ec07ac7363248d8240343d Mon Sep 17 00:00:00 2001 From: Gordon Allott Date: Tue, 8 Dec 2020 09:11:26 +0000 Subject: [PATCH 032/260] Enables jsonpointer support in openapi3 (#276) --- go.mod | 1 + go.sum | 14 +++ openapi3/callback.go | 23 ++++- openapi3/components.go | 18 ++-- openapi3/encoding.go | 10 +-- openapi3/examples.go | 19 +++++ openapi3/header.go | 61 +++++++++++-- openapi3/link.go | 17 ++++ openapi3/media_type.go | 31 ++++++- openapi3/operation.go | 42 ++++++++- openapi3/parameter.go | 105 ++++++++++++++++++++--- openapi3/refs.go | 93 ++++++++++++++++++++ openapi3/refs_test.go | 165 ++++++++++++++++++++++++++++++++++++ openapi3/request_body.go | 18 ++++ openapi3/response.go | 24 +++++- openapi3/schema.go | 156 ++++++++++++++++++++++++++++++++-- openapi3/security_scheme.go | 17 ++++ 17 files changed, 761 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index cdd681fc3..8ccc13917 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/ghodss/yaml v1.0.0 + github.com/go-openapi/jsonpointer v0.19.5 github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 998d6cb72..60f84360b 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,28 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/openapi3/callback.go b/openapi3/callback.go index 60196ba16..334233104 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -1,6 +1,27 @@ package openapi3 -import "context" +import ( + "context" + "fmt" + + "github.com/go-openapi/jsonpointer" +) + +type Callbacks map[string]*CallbackRef + +var _ jsonpointer.JSONPointable = (*Callbacks)(nil) + +func (c Callbacks) JSONLookup(token string) (interface{}, error) { + ref, ok := c[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} // Callback is specified by OpenAPI/Swagger standard version 3.0. type Callback map[string]*PathItem diff --git a/openapi3/components.go b/openapi3/components.go index 2708f95fb..e01f961d2 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -11,15 +11,15 @@ import ( // Components is specified by OpenAPI/Swagger standard version 3.0. type Components struct { ExtensionProps - Schemas map[string]*SchemaRef `json:"schemas,omitempty" yaml:"schemas,omitempty"` - Parameters map[string]*ParameterRef `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"` - RequestBodies map[string]*RequestBodyRef `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` - Responses map[string]*ResponseRef `json:"responses,omitempty" yaml:"responses,omitempty"` - SecuritySchemes map[string]*SecuritySchemeRef `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` - Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` - Links map[string]*LinkRef `json:"links,omitempty" yaml:"links,omitempty"` - Callbacks map[string]*CallbackRef `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` + 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"` + SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Links Links `json:"links,omitempty" yaml:"links,omitempty"` + Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` } func NewComponents() Components { diff --git a/openapi3/encoding.go b/openapi3/encoding.go index a60bddf82..16b7a2694 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -11,11 +11,11 @@ import ( type Encoding struct { ExtensionProps - ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` - Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` + Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` } func NewEncoding() *Encoding { diff --git a/openapi3/examples.go b/openapi3/examples.go index d89263ebc..5f6255bf3 100644 --- a/openapi3/examples.go +++ b/openapi3/examples.go @@ -1,9 +1,28 @@ package openapi3 import ( + "fmt" + "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type Examples map[string]*ExampleRef + +var _ jsonpointer.JSONPointable = (*Examples)(nil) + +func (e Examples) JSONLookup(token string) (interface{}, error) { + ref, ok := e[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // Example is specified by OpenAPI/Swagger 3.0 standard. type Example struct { ExtensionProps diff --git a/openapi3/header.go b/openapi3/header.go index 310ef9f92..3adb2ea5a 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -2,23 +2,43 @@ package openapi3 import ( "context" + "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type Headers map[string]*HeaderRef + +var _ jsonpointer.JSONPointable = (*Headers)(nil) + +func (h Headers) JSONLookup(token string) (interface{}, error) { + ref, ok := h[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + type Header struct { ExtensionProps // Optional description. Should use CommonMark syntax. - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Content Content `json:"content,omitempty" yaml:"content,omitempty"` } +var _ jsonpointer.JSONPointable = (*Header)(nil) + func (value *Header) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } @@ -31,3 +51,30 @@ func (value *Header) Validate(c context.Context) error { } return nil } + +func (value Header) JSONLookup(token string) (interface{}, error) { + switch token { + case "schema": + if value.Schema != nil { + if value.Schema.Ref != "" { + return &Ref{Ref: value.Schema.Ref}, nil + } + return value.Schema.Value, nil + } + case "description": + return value.Description, nil + case "deprecated": + return value.Deprecated, nil + case "required": + return value.Required, nil + case "example": + return value.Example, nil + case "examples": + return value.Examples, nil + case "content": + return value.Content, nil + } + + v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token) + return v, err +} diff --git a/openapi3/link.go b/openapi3/link.go index 2c1ec013f..0fe1a1c74 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -6,8 +6,25 @@ import ( "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type Links map[string]*LinkRef + +func (l Links) JSONLookup(token string) (interface{}, error) { + ref, ok := l[token] + if ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref != nil && ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + +var _ jsonpointer.JSONPointable = (*Links)(nil) + // Link is specified by OpenAPI/Swagger standard version 3.0. type Link struct { ExtensionProps diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 942ecd9e0..6d2f2cb7a 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -4,18 +4,21 @@ import ( "context" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) // MediaType is specified by OpenAPI/Swagger 3.0 standard. type MediaType struct { ExtensionProps - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` - Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` } +var _ jsonpointer.JSONPointable = (*MediaType)(nil) + func NewMediaType() *MediaType { return &MediaType{} } @@ -75,3 +78,23 @@ func (mediaType *MediaType) Validate(c context.Context) error { } return nil } + +func (mediaType MediaType) JSONLookup(token string) (interface{}, error) { + switch token { + case "schema": + if mediaType.Schema != nil { + if mediaType.Schema.Ref != "" { + return &Ref{Ref: mediaType.Schema.Ref}, nil + } + return mediaType.Schema.Value, nil + } + case "example": + return mediaType.Example, nil + case "examples": + return mediaType.Examples, nil + case "encoding": + return mediaType.Encoding, nil + } + v, _, err := jsonpointer.GetForToken(mediaType.ExtensionProps, token) + return v, err +} diff --git a/openapi3/operation.go b/openapi3/operation.go index 9e64f031e..08b43127b 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. @@ -34,7 +35,7 @@ type Operation struct { Responses Responses `json:"responses" yaml:"responses"` // Required // Optional callbacks - Callbacks map[string]*CallbackRef `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` @@ -47,6 +48,8 @@ type Operation struct { ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } +var _ jsonpointer.JSONPointable = (*Operation)(nil) + func NewOperation() *Operation { return &Operation{} } @@ -59,6 +62,43 @@ func (operation *Operation) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, operation) } +func (operation Operation) JSONLookup(token string) (interface{}, error) { + switch token { + case "requestBody": + if operation.RequestBody != nil { + if operation.RequestBody.Ref != "" { + return &Ref{Ref: operation.RequestBody.Ref}, nil + } + return operation.RequestBody.Value, nil + } + case "tags": + return operation.Tags, nil + case "summary": + return operation.Summary, nil + case "description": + return operation.Description, nil + case "operationID": + return operation.OperationID, nil + case "parameters": + return operation.Parameters, nil + case "responses": + return operation.Responses, nil + case "callbacks": + return operation.Callbacks, nil + case "deprecated": + return operation.Deprecated, nil + case "security": + return operation.Security, nil + case "servers": + return operation.Servers, nil + case "externalDocs": + return operation.ExternalDocs, nil + } + + v, _, err := jsonpointer.GetForToken(operation.ExtensionProps, token) + return v, err +} + func (operation *Operation) AddParameter(p *Parameter) { operation.Parameters = append(operation.Parameters, &ParameterRef{ Value: p, diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 1e2f55e17..8603bd7bc 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -4,13 +4,51 @@ import ( "context" "errors" "fmt" + "strconv" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type ParametersMap map[string]*ParameterRef + +var _ jsonpointer.JSONPointable = (*ParametersMap)(nil) + +func (p ParametersMap) JSONLookup(token string) (interface{}, error) { + ref, ok := p[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // Parameters is specified by OpenAPI/Swagger 3.0 standard. type Parameters []*ParameterRef +var _ jsonpointer.JSONPointable = (*Parameters)(nil) + +func (p Parameters) JSONLookup(token string) (interface{}, error) { + index, err := strconv.Atoi(token) + if err != nil { + return nil, err + } + + if index < 0 || index >= len(p) { + return nil, fmt.Errorf("index out of bounds array[0,%d] index '%d'", len(p), index) + } + + ref := p[index] + + if ref != nil && ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + func NewParameters() Parameters { return make(Parameters, 0, 4) } @@ -47,21 +85,23 @@ func (parameters Parameters) Validate(c context.Context) error { // Parameter is specified by OpenAPI/Swagger 3.0 standard. type Parameter struct { ExtensionProps - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Content Content `json:"content,omitempty" yaml:"content,omitempty"` } +var _ jsonpointer.JSONPointable = (*Parameter)(nil) + const ( ParameterInPath = "path" ParameterInQuery = "query" @@ -127,6 +167,45 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, parameter) } +func (parameter Parameter) JSONLookup(token string) (interface{}, error) { + switch token { + case "schema": + if parameter.Schema != nil { + if parameter.Schema.Ref != "" { + return &Ref{Ref: parameter.Schema.Ref}, nil + } + return parameter.Schema.Value, nil + } + case "name": + return parameter.Name, nil + case "in": + return parameter.In, nil + case "description": + return parameter.Description, nil + case "style": + return parameter.Style, nil + case "explode": + return parameter.Explode, nil + case "allowEmptyValue": + return parameter.AllowEmptyValue, nil + case "allowReserved": + return parameter.AllowReserved, nil + case "deprecated": + return parameter.Deprecated, nil + case "required": + return parameter.Required, nil + case "example": + return parameter.Example, nil + case "examples": + return parameter.Examples, nil + case "content": + return parameter.Content, nil + } + + v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token) + return v, err +} + // SerializationMethod returns a parameter's serialization method. // When a parameter's serialization method is not defined the method returns // the default serialization method corresponding to a parameter's location. diff --git a/openapi3/refs.go b/openapi3/refs.go index 9790b4705..a086e367e 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -4,13 +4,21 @@ import ( "context" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +// Ref is specified by OpenAPI/Swagger 3.0 standard. +type Ref struct { + Ref string `json:"$ref" yaml:"$ref"` +} + type CallbackRef struct { Ref string Value *Callback } +var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) + func (value *CallbackRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -27,11 +35,22 @@ func (value *CallbackRef) Validate(c context.Context) error { return v.Validate(c) } +func (value CallbackRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type ExampleRef struct { Ref string Value *Example } +var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) + func (value *ExampleRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -44,11 +63,22 @@ func (value *ExampleRef) Validate(c context.Context) error { return nil } +func (value ExampleRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type HeaderRef struct { Ref string Value *Header } +var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) + func (value *HeaderRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -64,6 +94,14 @@ func (value *HeaderRef) Validate(c context.Context) error { } return v.Validate(c) } +func (value HeaderRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} type LinkRef struct { Ref string @@ -91,6 +129,8 @@ type ParameterRef struct { Value *Parameter } +var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) + func (value *ParameterRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -107,11 +147,22 @@ func (value *ParameterRef) Validate(c context.Context) error { return v.Validate(c) } +func (value ParameterRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type ResponseRef struct { Ref string Value *Response } +var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) + func (value *ResponseRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -128,11 +179,22 @@ func (value *ResponseRef) Validate(c context.Context) error { return v.Validate(c) } +func (value ResponseRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type RequestBodyRef struct { Ref string Value *RequestBody } +var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) + func (value *RequestBodyRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -149,11 +211,22 @@ func (value *RequestBodyRef) Validate(c context.Context) error { return v.Validate(c) } +func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type SchemaRef struct { Ref string Value *Schema } +var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) + func NewSchemaRef(ref string, value *Schema) *SchemaRef { return &SchemaRef{ Ref: ref, @@ -177,11 +250,22 @@ func (value *SchemaRef) Validate(c context.Context) error { return v.Validate(c) } +func (value SchemaRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type SecuritySchemeRef struct { Ref string Value *SecurityScheme } +var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) + func (value *SecuritySchemeRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -197,3 +281,12 @@ func (value *SecuritySchemeRef) Validate(c context.Context) error { } return v.Validate(c) } + +func (value SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index ed12b894d..0c8b84570 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -1,8 +1,11 @@ package openapi3 import ( + "reflect" "testing" + "github.com/go-openapi/jsonpointer" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -109,3 +112,165 @@ components: _, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) require.EqualError(t, err, `invalid response: value MUST be a JSON object`) } + +func TestIssue247(t *testing.T) { + spec := `openapi: 3.0.2 +info: + title: Swagger Petstore - OpenAPI 3.0 + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.5 +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io +- name: store + description: Operations about user +- name: user + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + OneOfTest: + type: object + oneOf: + - type: string + - type: integer + format: int32 + ` + root, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + require.NoError(t, err) + + ptr, err := jsonpointer.New("/paths/~1pet/put/responses/200/content") + require.NoError(t, err) + v, kind, err := ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.TypeOf(Content{}).Kind(), kind) + assert.IsType(t, Content{}, v) + + ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content/application~1json/schema") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + assert.IsType(t, &Ref{}, v) + assert.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) + + ptr, err = jsonpointer.New("/components/schemas/Pets/items") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Ref{}, v) + assert.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) + + ptr, err = jsonpointer.New("/components/schemas/Error/properties/code") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Schema{}, v) + assert.Equal(t, "integer", v.(*Schema).Type) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/0") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Schema{}, v) + assert.Equal(t, "string", v.(*Schema).Type) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/1") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Schema{}, v) + assert.Equal(t, "integer", v.(*Schema).Type) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/5") + require.NoError(t, err) + _, _, err = ptr.Get(root) + assert.Error(t, err) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/-1") + require.NoError(t, err) + _, _, err = ptr.Get(root) + assert.Error(t, err) +} diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 6d649ca4b..ad871e8fd 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -2,10 +2,28 @@ package openapi3 import ( "context" + "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type RequestBodies map[string]*RequestBodyRef + +var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) + +func (r RequestBodies) JSONLookup(token string) (interface{}, error) { + ref, ok := r[token] + if ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref != nil && ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // RequestBody is specified by OpenAPI/Swagger 3.0 standard. type RequestBody struct { ExtensionProps diff --git a/openapi3/response.go b/openapi3/response.go index a89b28e2f..7c4da1dc2 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -3,14 +3,18 @@ package openapi3 import ( "context" "errors" + "fmt" "strconv" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) // Responses is specified by OpenAPI/Swagger 3.0 standard. type Responses map[string]*ResponseRef +var _ jsonpointer.JSONPointable = (*Responses)(nil) + func NewResponses() Responses { r := make(Responses) r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")} @@ -37,13 +41,25 @@ func (responses Responses) Validate(c context.Context) error { return nil } +func (responses Responses) JSONLookup(token string) (interface{}, error) { + ref, ok := responses[token] + if ok == false { + return nil, fmt.Errorf("invalid token reference: %q", token) + } + + if ref != nil && ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // Response is specified by OpenAPI/Swagger 3.0 standard. type Response struct { ExtensionProps - Description *string `json:"description,omitempty" yaml:"description,omitempty"` - Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` - Links map[string]*LinkRef `json:"links,omitempty" yaml:"links,omitempty"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` + Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Links Links `json:"links,omitempty" yaml:"links,omitempty"` } func NewResponse() *Response { diff --git a/openapi3/schema.go b/openapi3/schema.go index d020c0e3c..228ce0623 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -13,6 +13,7 @@ import ( "unicode/utf16" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) var ( @@ -50,13 +51,51 @@ func Uint64Ptr(value uint64) *uint64 { return &value } +type Schemas map[string]*SchemaRef + +var _ jsonpointer.JSONPointable = (*Schemas)(nil) + +func (s Schemas) JSONLookup(token string) (interface{}, error) { + ref, ok := s[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + +type SchemaRefs []*SchemaRef + +var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil) + +func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { + i, err := strconv.ParseUint(token, 10, 64) + if err != nil { + return nil, err + } + + if i >= uint64(len(s)) { + return nil, fmt.Errorf("index out of range: %d", i) + } + + ref := s[i] + + if ref == nil || ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // Schema is specified by OpenAPI/Swagger 3.0 standard. type Schema struct { ExtensionProps - OneOf []*SchemaRef `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` - AnyOf []*SchemaRef `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` - AllOf []*SchemaRef `json:"allOf,omitempty" yaml:"allOf,omitempty"` + OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` + AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` + AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Title string `json:"title,omitempty" yaml:"title,omitempty"` @@ -99,14 +138,16 @@ type Schema struct { Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` // Object - Required []string `json:"required,omitempty" yaml:"required,omitempty"` - Properties map[string]*SchemaRef `json:"properties,omitempty" yaml:"properties,omitempty"` - MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` - MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - AdditionalProperties *SchemaRef `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"` - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` + MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + AdditionalProperties *SchemaRef `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"` + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } +var _ jsonpointer.JSONPointable = (*Schema)(nil) + func NewSchema() *Schema { return &Schema{} } @@ -119,6 +160,103 @@ func (schema *Schema) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, schema) } +func (schema Schema) JSONLookup(token string) (interface{}, error) { + switch token { + case "additionalProperties": + if schema.AdditionalProperties != nil { + if schema.AdditionalProperties.Ref != "" { + return &Ref{Ref: schema.AdditionalProperties.Ref}, nil + } + return schema.AdditionalProperties.Value, nil + } + case "not": + if schema.Not != nil { + if schema.Not.Ref != "" { + return &Ref{Ref: schema.Not.Ref}, nil + } + return schema.Not.Value, nil + } + case "items": + if schema.Items != nil { + if schema.Items.Ref != "" { + return &Ref{Ref: schema.Items.Ref}, nil + } + return schema.Items.Value, nil + } + case "oneOf": + return schema.OneOf, nil + case "anyOf": + return schema.AnyOf, nil + case "allOf": + return schema.AllOf, nil + case "type": + return schema.Type, nil + case "title": + return schema.Title, nil + case "format": + return schema.Format, nil + case "description": + return schema.Description, nil + case "enum": + return schema.Enum, nil + case "default": + return schema.Default, nil + case "example": + return schema.Example, nil + case "externalDocs": + return schema.ExternalDocs, nil + case "additionalPropertiesAllowed": + return schema.AdditionalPropertiesAllowed, nil + case "uniqueItems": + return schema.UniqueItems, nil + case "exclusiveMin": + return schema.ExclusiveMin, nil + case "exclusiveMax": + return schema.ExclusiveMax, nil + case "nullable": + return schema.Nullable, nil + case "readOnly": + return schema.ReadOnly, nil + case "writeOnly": + return schema.WriteOnly, nil + case "allowEmptyValue": + return schema.AllowEmptyValue, nil + case "xml": + return schema.XML, nil + case "deprecated": + return schema.Deprecated, nil + case "min": + return schema.Min, nil + case "max": + return schema.Max, nil + case "multipleOf": + return schema.MultipleOf, nil + case "minLength": + return schema.MinLength, nil + case "maxLength": + return schema.MaxLength, nil + case "pattern": + return schema.Pattern, nil + case "minItems": + return schema.MinItems, nil + case "maxItems": + return schema.MaxItems, nil + case "required": + return schema.Required, nil + case "properties": + return schema.Properties, nil + case "minProps": + return schema.MinProps, nil + case "maxProps": + return schema.MaxProps, nil + case "discriminator": + return schema.Discriminator, nil + } + + v, _, err := jsonpointer.GetForToken(schema.ExtensionProps, token) + return v, err +} + func (schema *Schema) NewRef() *SchemaRef { return &SchemaRef{ Value: schema, diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 0e991fb67..8dcb23086 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -6,8 +6,25 @@ import ( "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type SecuritySchemes map[string]*SecuritySchemeRef + +func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) { + ref, ok := s[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + +var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) + type SecurityScheme struct { ExtensionProps From f05a91328638d7ae73adfd4298cd9456c7692572 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 23 Dec 2020 12:17:14 +0100 Subject: [PATCH 033/260] Fix flaky CI (#278) --- openapi2/openapi2.go | 15 + openapi2conv/openapi2_conv.go | 2 + openapi2conv/openapi2_conv_test.go | 1218 ++++++++++++++-------------- 3 files changed, 636 insertions(+), 599 deletions(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 5e4877b96..8cf0f0117 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -10,6 +10,7 @@ package openapi2 import ( "fmt" "net/http" + "sort" "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" @@ -168,6 +169,20 @@ func (operation *Operation) UnmarshalJSON(data []byte) error { type Parameters []*Parameter +var _ sort.Interface = Parameters{} + +func (ps Parameters) Len() int { return len(ps) } +func (ps Parameters) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } +func (ps Parameters) Less(i, j int) bool { + if ps[i].Name != ps[j].Name { + return ps[i].Name < ps[j].Name + } + if ps[i].In != ps[j].In { + return ps[i].In < ps[i].In + } + return ps[i].Ref < ps[j].Ref +} + type Parameter struct { openapi3.ExtensionProps Ref string `json:"$ref,omitempty"` diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 42c4f6f75..03a880279 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -597,6 +597,7 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { } params = append(params, p) } + sort.Sort(params) result.Paths[path].Parameters = params } @@ -922,6 +923,7 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( result.Consumes = consumesToArray(consumes) } } + sort.Sort(result.Parameters) if responses := operation.Responses; responses != nil { resultResponses, err := FromV3Responses(responses, &swagger.Components) diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index ad3a3e8b7..f284ba4cc 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -46,609 +46,629 @@ func TestConvOpenAPIV2ToV3(t *testing.T) { const exampleV2 = ` { - "swagger": "2.0", - "basePath": "/v2", - "definitions": { - "Error": { - "description": "Error response.", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "Item": { - "additionalProperties": true, - "properties": { - "foo": { - "type": "string" - }, - "quux": { - "$ref": "#/definitions/ItemExtension" - } - }, - "type": "object" - }, - "ItemExtension": { - "description": "It could be anything.", - "type": "boolean" - } - }, - "externalDocs": { - "description": "Example Documentation", - "url": "https://example/doc/" - }, - "host": "test.example.com", - "info": { - "title": "MyAPI", - "version": "0.1", - "x-info": "info extension" - }, - "consumes": [ - "application/json", - "application/xml" - ], - "parameters": { - "banana": { - "in": "path", - "name": "banana", - "required": true, - "type": "string" - }, - "put_body": { - "in": "body", - "name": "banana", - "required": true, - "schema": { - "type": "string" - }, - "x-originalParamName":"banana" - }, - "post_form_ref": { - "required": true, - "description": "param description", - "in": "formData", - "name": "fileUpload2", - "type": "file", - "x-formData-name":"fileUpload2", - "x-mimetype": "text/plain" - } - }, - "paths": { - "/another/{banana}/{id}": { - "parameters": [ - { - "$ref": "#/parameters/banana" - }, - { - "in": "path", - "name": "id", - "required": true, - "type": "integer" - } - ] - }, - "/example": { - "get": { - "description": "example get", - "responses": { - "403": { - "$ref": "#/responses/ForbiddenError" - }, - "404": { - "description": "404 response" - }, - "default": { - "description": "default response" - } - }, - "x-operation": "operation extension 1" - }, - "delete": { - "description": "example delete", - "operationId": "example-delete", - "parameters": [ - { - "in": "query", - "name": "x", - "type": "string", - "x-parameter": "parameter extension 1" - }, - { - "default": 250, - "description": "The y parameter", - "in": "query", - "maximum": 10000, - "minimum": 1, - "name": "y", - "type": "integer" - }, - { - "description": "Only return results that intersect the provided bounding box.", - "in": "query", - "items": { - "type": "number" - }, - "maxItems": 4, - "minItems": 4, - "name": "bbox", - "type": "array" - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "items": { - "$ref": "#/definitions/Item" - }, - "type": "array" - } - }, - "404": { - "description": "404 response" - }, - "default": { - "description": "default response", - "x-response": "response extension 1" - } - }, - "security": [ - { - "get_security_0": [ - "scope0", - "scope1" - ], - "get_security_1": [] - } - ], - "summary": "example get", - "tags": [ - "Example" - ] - }, - "head": { - "description": "example head", - "responses": { - "default": { - "description": "default response" - } - } - }, - "options": { - "description": "example options", - "responses": { - "default": { - "description": "default response" - } - } - }, - "patch": { - "consumes": [ - "application/json", - "application/xml" - ], - "description": "example patch", - "parameters": [ - { - "in": "body", - "name": "patch_body", - "schema": { - "allOf": [{"$ref": "#/definitions/Item"}] - }, - "x-originalParamName":"patch_body", - "x-requestBody": "requestbody extension 1" - } - ], - "responses": { - "default": { - "description": "default response" - } - } - }, - "post": { - "consumes": ["multipart/form-data"], - "description": "example post", - "parameters": [ - { - "description": "File Id", - "in": "query", - "name": "id", - "type": "integer" - }, - { - "description": "param description", - "in": "formData", - "name": "fileUpload", - "type": "file", - "x-formData-name":"fileUpload", - "x-mimetype": "text/plain" - }, - { - "description": "Description of file contents", - "in": "formData", - "name": "note", - "type": "integer", - "x-formData-name":"note" - }, - { - "$ref": "#/parameters/post_form_ref" - } - ], - "responses": { - "default": { - "description": "default response" - } - } - }, - "put": { - "description": "example put", - "parameters": [ - { - "$ref": "#/parameters/put_body" - } - ], - "responses": { - "default": { - "description": "default response" - } - } - }, - "x-path": "path extension 1", - "x-path2": "path extension 2" - } - }, - "responses": { - "ForbiddenError": { - "description": "Insufficient permission to perform the requested action.", - "schema": { - "$ref": "#/definitions/Error" - } - } - }, - "schemes": [ - "https" - ], - "security": [ - { - "default_security_0": [ - "scope0", - "scope1" - ], - "default_security_1": [] - } - ], - "tags": [ - { - "description": "An example tag.", - "name": "Example" - } - ], - "x-root": "root extension 1", - "x-root2": "root extension 2" + "basePath": "/v2", + "consumes": [ + "application/json", + "application/xml" + ], + "definitions": { + "Error": { + "description": "Error response.", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "Item": { + "additionalProperties": true, + "properties": { + "foo": { + "type": "string" + }, + "quux": { + "$ref": "#/definitions/ItemExtension" + } + }, + "type": "object" + }, + "ItemExtension": { + "description": "It could be anything.", + "type": "boolean" + } + }, + "externalDocs": { + "description": "Example Documentation", + "url": "https://example/doc/" + }, + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1", + "x-info": "info extension" + }, + "parameters": { + "banana": { + "in": "path", + "name": "banana", + "required": true, + "type": "string" + }, + "post_form_ref": { + "description": "param description", + "in": "formData", + "name": "fileUpload2", + "required": true, + "type": "file", + "x-formData-name": "fileUpload2", + "x-mimetype": "text/plain" + }, + "put_body": { + "in": "body", + "name": "banana", + "required": true, + "schema": { + "type": "string" + }, + "x-originalParamName": "banana" + } + }, + "paths": { + "/another/{banana}/{id}": { + "parameters": [ + { + "$ref": "#/parameters/banana" + }, + { + "in": "path", + "name": "id", + "required": true, + "type": "integer" + } + ] + }, + "/example": { + "delete": { + "description": "example delete", + "operationId": "example-delete", + "parameters": [ + { + "description": "Only return results that intersect the provided bounding box.", + "in": "query", + "items": { + "type": "number" + }, + "maxItems": 4, + "minItems": 4, + "name": "bbox", + "type": "array" + }, + { + "in": "query", + "name": "x", + "type": "string", + "x-parameter": "parameter extension 1" + }, + { + "default": 250, + "description": "The y parameter", + "in": "query", + "maximum": 10000, + "minimum": 1, + "name": "y", + "type": "integer" + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "items": { + "$ref": "#/definitions/Item" + }, + "type": "array" + } + }, + "404": { + "description": "404 response" + }, + "default": { + "description": "default response", + "x-response": "response extension 1" + } + }, + "security": [ + { + "get_security_0": [ + "scope0", + "scope1" + ], + "get_security_1": [] + } + ], + "summary": "example get", + "tags": [ + "Example" + ] + }, + "get": { + "description": "example get", + "responses": { + "403": { + "$ref": "#/responses/ForbiddenError" + }, + "404": { + "description": "404 response" + }, + "default": { + "description": "default response" + } + }, + "x-operation": "operation extension 1" + }, + "head": { + "description": "example head", + "responses": { + "default": { + "description": "default response" + } + } + }, + "options": { + "description": "example options", + "responses": { + "default": { + "description": "default response" + } + } + }, + "patch": { + "consumes": [ + "application/json", + "application/xml" + ], + "description": "example patch", + "parameters": [ + { + "in": "body", + "name": "patch_body", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Item" + } + ] + }, + "x-originalParamName": "patch_body", + "x-requestBody": "requestbody extension 1" + } + ], + "responses": { + "default": { + "description": "default response" + } + } + }, + "post": { + "consumes": [ + "multipart/form-data" + ], + "description": "example post", + "parameters": [ + { + "$ref": "#/parameters/post_form_ref" + }, + { + "description": "param description", + "in": "formData", + "name": "fileUpload", + "type": "file", + "x-formData-name": "fileUpload", + "x-mimetype": "text/plain" + }, + { + "description": "File Id", + "in": "query", + "name": "id", + "type": "integer" + }, + { + "description": "Description of file contents", + "in": "formData", + "name": "note", + "type": "integer", + "x-formData-name": "note" + } + ], + "responses": { + "default": { + "description": "default response" + } + } + }, + "put": { + "description": "example put", + "parameters": [ + { + "$ref": "#/parameters/put_body" + } + ], + "responses": { + "default": { + "description": "default response" + } + } + }, + "x-path": "path extension 1", + "x-path2": "path extension 2" + } + }, + "responses": { + "ForbiddenError": { + "description": "Insufficient permission to perform the requested action.", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "schemes": [ + "https" + ], + "security": [ + { + "default_security_0": [ + "scope0", + "scope1" + ], + "default_security_1": [] + } + ], + "swagger": "2.0", + "tags": [ + { + "description": "An example tag.", + "name": "Example" + } + ], + "x-root": "root extension 1", + "x-root2": "root extension 2" } ` const exampleV3 = ` { - "components": { - "parameters": { - "banana": { - "in": "path", - "name": "banana", - "required": true, - "schema": { - "type": "string" - } - } - }, - "requestBodies": { - "put_body": { - "content":{ - "application/json": { - "schema": { - "type": "string" - } - }, - "application/xml": { - "schema": { - "type": "string" - } - } - }, - "required": true, - "x-originalParamName":"banana" - } - }, - "responses": { - "ForbiddenError": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - }, - "description": "Insufficient permission to perform the requested action." - } - }, - "schemas": { - "Error": { - "description": "Error response.", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "Item": { - "additionalProperties": true, - "properties": { - "foo": { - "type": "string" - }, - "quux": { - "$ref": "#/components/schemas/ItemExtension" - } - }, - "type": "object" - }, - "ItemExtension": { - "type": "boolean", - "description": "It could be anything." - }, - "post_form_ref": { - "description": "param description", - "format": "binary", - "required": ["fileUpload2"], - "type": "string", - "x-formData-name": "fileUpload2", - "x-mimetype":"text/plain" - } - } - }, - "externalDocs": { - "description": "Example Documentation", - "url": "https://example/doc/" - }, - "info": { - "title": "MyAPI", - "version": "0.1", - "x-info": "info extension" - }, - "openapi": "3.0.3", - "paths": { - "/another/{banana}/{id}": { - "parameters": [ - { - "$ref": "#/components/parameters/banana" - }, - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "/example": { - "get": { - "description": "example get", - "responses": { - "403": { - "$ref": "#/components/responses/ForbiddenError" - }, - "404": { - "description": "404 response" - }, - "default": { - "description": "default response" - } - }, - "x-operation": "operation extension 1" - }, - "delete": { - "description": "example delete", - "operationId": "example-delete", - "parameters": [ - { - "in": "query", - "name": "x", - "schema": {"type": "string"}, - "x-parameter": "parameter extension 1" - }, - { - "description": "The y parameter", - "in": "query", - "name": "y", - "schema": { - "default": 250, - "maximum": 10000, - "minimum": 1, - "type": "integer" - } - }, - { - "description": "Only return results that intersect the provided bounding box.", - "in": "query", - "name": "bbox", - "schema": { - "items": { - "type": "number" - }, - "maxItems": 4, - "minItems": 4, - "type": "array" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array" - } - } - }, - "description": "ok" - }, - "404": { - "description": "404 response" - }, - "default": { - "description": "default response", - "x-response": "response extension 1" - } - }, - "security": [ - { - "get_security_0": [ - "scope0", - "scope1" - ], - "get_security_1": [] - } - ], - "summary": "example get", - "tags": [ - "Example" - ] - }, - "head": { - "description": "example head", - "responses": { - "default": { - "description": "default response" - } - } - }, - "options": { - "description": "example options", - "responses": { - "default": { - "description": "default response" - } - } - }, - "patch": { - "description": "example patch", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}] - } - }, - "application/xml": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}] - } - } - }, - "x-originalParamName":"patch_body", - "x-requestBody": "requestbody extension 1" - }, - "responses": { - "default": { - "description": "default response" - } - } - }, - "post": { - "description": "example post", - "parameters": [ - { - "description": "File Id", - "in": "query", - "name": "id", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "properties": { - "fileUpload": { - "description": "param description", - "type": "string", - "format": "binary", - "x-formData-name":"fileUpload", - "x-mimetype": "text/plain" - }, - "note": { - "description": "Description of file contents", - "type": "integer", - "x-formData-name": "note" - }, - "fileUpload2": { - "$ref": "#/components/schemas/post_form_ref" - } - }, - "required": ["fileUpload2"], - "type": "object" - } - } - } - }, - "responses": { - "default": { - "description": "default response" - } - } - }, - "put": { - "description": "example put", - "requestBody": { - "$ref": "#/components/requestBodies/put_body" - }, - "responses": { - "default": { - "description": "default response" - } - } - }, - "x-path": "path extension 1", - "x-path2": "path extension 2" - } - }, - "security": [ - { - "default_security_0": [ - "scope0", - "scope1" - ], - "default_security_1": [] - } - ], - "servers": [ - { - "url": "https://test.example.com/v2" - } - ], - "tags": [ - { - "description": "An example tag.", - "name": "Example" - } - ], - "x-root": "root extension 1", - "x-root2": "root extension 2" + "components": { + "parameters": { + "banana": { + "in": "path", + "name": "banana", + "required": true, + "schema": { + "type": "string" + } + } + }, + "requestBodies": { + "put_body": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "required": true, + "x-originalParamName": "banana" + } + }, + "responses": { + "ForbiddenError": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Insufficient permission to perform the requested action." + } + }, + "schemas": { + "Error": { + "description": "Error response.", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "Item": { + "additionalProperties": true, + "properties": { + "foo": { + "type": "string" + }, + "quux": { + "$ref": "#/components/schemas/ItemExtension" + } + }, + "type": "object" + }, + "ItemExtension": { + "description": "It could be anything.", + "type": "boolean" + }, + "post_form_ref": { + "description": "param description", + "format": "binary", + "required": [ + "fileUpload2" + ], + "type": "string", + "x-formData-name": "fileUpload2", + "x-mimetype": "text/plain" + } + } + }, + "externalDocs": { + "description": "Example Documentation", + "url": "https://example/doc/" + }, + "info": { + "title": "MyAPI", + "version": "0.1", + "x-info": "info extension" + }, + "openapi": "3.0.3", + "paths": { + "/another/{banana}/{id}": { + "parameters": [ + { + "$ref": "#/components/parameters/banana" + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "/example": { + "delete": { + "description": "example delete", + "operationId": "example-delete", + "parameters": [ + { + "description": "Only return results that intersect the provided bounding box.", + "in": "query", + "name": "bbox", + "schema": { + "items": { + "type": "number" + }, + "maxItems": 4, + "minItems": 4, + "type": "array" + } + }, + { + "in": "query", + "name": "x", + "schema": { + "type": "string" + }, + "x-parameter": "parameter extension 1" + }, + { + "description": "The y parameter", + "in": "query", + "name": "y", + "schema": { + "default": 250, + "maximum": 10000, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array" + } + } + }, + "description": "ok" + }, + "404": { + "description": "404 response" + }, + "default": { + "description": "default response", + "x-response": "response extension 1" + } + }, + "security": [ + { + "get_security_0": [ + "scope0", + "scope1" + ], + "get_security_1": [] + } + ], + "summary": "example get", + "tags": [ + "Example" + ] + }, + "get": { + "description": "example get", + "responses": { + "403": { + "$ref": "#/components/responses/ForbiddenError" + }, + "404": { + "description": "404 response" + }, + "default": { + "description": "default response" + } + }, + "x-operation": "operation extension 1" + }, + "head": { + "description": "example head", + "responses": { + "default": { + "description": "default response" + } + } + }, + "options": { + "description": "example options", + "responses": { + "default": { + "description": "default response" + } + } + }, + "patch": { + "description": "example patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ] + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ] + } + } + }, + "x-originalParamName": "patch_body", + "x-requestBody": "requestbody extension 1" + }, + "responses": { + "default": { + "description": "default response" + } + } + }, + "post": { + "description": "example post", + "parameters": [ + { + "description": "File Id", + "in": "query", + "name": "id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "fileUpload": { + "description": "param description", + "format": "binary", + "type": "string", + "x-formData-name": "fileUpload", + "x-mimetype": "text/plain" + }, + "fileUpload2": { + "$ref": "#/components/schemas/post_form_ref" + }, + "note": { + "description": "Description of file contents", + "type": "integer", + "x-formData-name": "note" + } + }, + "required": [ + "fileUpload2" + ], + "type": "object" + } + } + } + }, + "responses": { + "default": { + "description": "default response" + } + } + }, + "put": { + "description": "example put", + "requestBody": { + "$ref": "#/components/requestBodies/put_body" + }, + "responses": { + "default": { + "description": "default response" + } + } + }, + "x-path": "path extension 1", + "x-path2": "path extension 2" + } + }, + "security": [ + { + "default_security_0": [ + "scope0", + "scope1" + ], + "default_security_1": [] + } + ], + "servers": [ + { + "url": "https://test.example.com/v2" + } + ], + "tags": [ + { + "description": "An example tag.", + "name": "Example" + } + ], + "x-root": "root extension 1", + "x-root2": "root extension 2" } ` From 4bb44a2e667c709f8c8eb0de17d8bcce67f1b53c Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 5 Jan 2021 13:41:59 +0100 Subject: [PATCH 034/260] Fix failfast flag handling (#284) --- openapi3/schema.go | 32 +++++++++++++++++++------------- openapi3/schema_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 228ce0623..4fcc864ac 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -794,8 +794,6 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf } func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) { - var oldfailfast bool - if enum := schema.Enum; len(enum) != 0 { for _, v := range enum { if value == v { @@ -818,9 +816,12 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(ref.Ref) } + var oldfailfast bool oldfailfast, settings.failfast = settings.failfast, true - if err := v.visitJSON(settings, value); err == nil { - if oldfailfast { + err := v.visitJSON(settings, value) + settings.failfast = oldfailfast + if err == nil { + if settings.failfast { return errSchema } return &SchemaError{ @@ -829,7 +830,6 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val SchemaField: "not", } } - settings.failfast = oldfailfast } if v := schema.OneOf; len(v) > 0 { @@ -839,11 +839,13 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(item.Ref) } + var oldfailfast bool oldfailfast, settings.failfast = settings.failfast, true - if err := v.visitJSON(settings, value); err == nil { + err := v.visitJSON(settings, value) + settings.failfast = oldfailfast + if err == nil { ok++ } - settings.failfast = oldfailfast } if ok != 1 { if settings.failfast { @@ -864,12 +866,14 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(item.Ref) } + var oldfailfast bool oldfailfast, settings.failfast = settings.failfast, true - if err := v.visitJSON(settings, value); err == nil { + err := v.visitJSON(settings, value) + settings.failfast = oldfailfast + if err == nil { ok = true break } - settings.failfast = oldfailfast } if !ok { if settings.failfast { @@ -888,9 +892,12 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(item.Ref) } + var oldfailfast bool oldfailfast, settings.failfast = settings.failfast, false - if err := v.visitJSON(settings, value); err != nil { - if oldfailfast { + err := v.visitJSON(settings, value) + settings.failfast = oldfailfast + if err != nil { + if settings.failfast { return errSchema } return &SchemaError{ @@ -900,7 +907,6 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Origin: err, } } - settings.failfast = oldfailfast } return } @@ -1494,7 +1500,7 @@ func (err *SchemaError) Error() string { buf.WriteByte('/') buf.WriteString(reversePath[i]) } - buf.WriteString(`":`) + buf.WriteString(`": `) } reason := err.Reason if reason == "" { diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 6650ad547..81898c888 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1192,3 +1192,30 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ }, }, } + +func TestIssue283(t *testing.T) { + const api = ` +openapi: "3.0.1" +components: + schemas: + Test: + properties: + name: + type: string + ownerName: + not: + type: boolean + type: object +` + data := map[string]interface{}{ + "name": "kin-openapi", + "ownerName": true, + } + s, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(api)) + require.NoError(t, err) + require.NotNil(t, s) + err = s.Components.Schemas["Test"].Value.VisitJSON(data) + require.NotNil(t, err) + require.NotEqual(t, errSchema, err) + require.Contains(t, err.Error(), `Error at "/ownerName": Doesn't match schema "not"`) +} From d4df86a63c4efbaf31aabce8f39555f22a8f1b30 Mon Sep 17 00:00:00 2001 From: Michael Krotscheck Date: Tue, 19 Jan 2021 09:23:32 -0800 Subject: [PATCH 035/260] Add OIDC Schema format as per spec (#287) Co-authored-by: Pierre Fenoll --- openapi3/security_scheme.go | 26 ++++++++++++++++++-------- openapi3/security_scheme_test.go | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 8dcb23086..7e8301987 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -28,13 +28,14 @@ var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) type SecurityScheme struct { ExtensionProps - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` - BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` - Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` + Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` + OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` } func NewSecurityScheme() *SecurityScheme { @@ -49,6 +50,13 @@ func NewCSRFSecurityScheme() *SecurityScheme { } } +func NewOIDCSecurityScheme(oidcUrl string) *SecurityScheme { + return &SecurityScheme{ + Type: "openIdConnect", + OpenIdConnectUrl: oidcUrl, + } +} + func NewJWTSecurityScheme() *SecurityScheme { return &SecurityScheme{ Type: "http", @@ -114,7 +122,9 @@ func (ss *SecurityScheme) Validate(c context.Context) error { case "oauth2": hasFlow = true case "openIdConnect": - return fmt.Errorf("Support for security schemes with type '%v' has not been implemented", ss.Type) + if ss.OpenIdConnectUrl == "" { + return fmt.Errorf("No OIDC URL found for openIdConnect security scheme %q", ss.Name) + } default: return fmt.Errorf("Security scheme 'type' can't be '%v'", ss.Type) } diff --git a/openapi3/security_scheme_test.go b/openapi3/security_scheme_test.go index 2a6420877..9edb17a75 100644 --- a/openapi3/security_scheme_test.go +++ b/openapi3/security_scheme_test.go @@ -198,4 +198,24 @@ var securitySchemeExamples = []securitySchemeExample{ `), valid: true, }, + { + title: "OIDC Type With URL", + raw: []byte(` +{ + "type": "openIdConnect", + "openIdConnectUrl": "https://example.com/.well-known/openid-configuration" +} +`), + valid: true, + }, + { + title: "OIDC Type Without URL", + raw: []byte(` +{ + "type": "openIdConnect", + "openIdConnectUrl": "" +} +`), + valid: false, + }, } From be070be7974bcc3661f1898837e4a91fbcb2c87f Mon Sep 17 00:00:00 2001 From: Jake Scott Date: Fri, 29 Jan 2021 02:37:44 -0500 Subject: [PATCH 036/260] Support for alternate http auth mechanisms (#291) Fixes #290 --- openapi3/security_scheme.go | 2 +- openapi3/security_scheme_test.go | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 7e8301987..8d6c7c1fb 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -115,7 +115,7 @@ func (ss *SecurityScheme) Validate(c context.Context) error { switch scheme { case "bearer": hasBearerFormat = true - case "basic": + case "basic", "negotiate", "digest": default: return fmt.Errorf("Security scheme of type 'http' has invalid 'scheme' value '%s'", scheme) } diff --git a/openapi3/security_scheme_test.go b/openapi3/security_scheme_test.go index 9edb17a75..cba0b8442 100644 --- a/openapi3/security_scheme_test.go +++ b/openapi3/security_scheme_test.go @@ -46,6 +46,26 @@ var securitySchemeExamples = []securitySchemeExample{ `), valid: true, }, + { + title: "Negotiate Authentication Sample", + raw: []byte(` +{ + "type": "http", + "scheme": "negotiate" +} +`), + valid: true, + }, + { + title: "Unknown http Authentication Sample", + raw: []byte(` +{ + "type": "http", + "scheme": "notvalid" +} +`), + valid: false, + }, { title: "API Key Sample", raw: []byte(` From aa9a5c3a388f4bcfef637aa24136eaefa709a6b7 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 29 Jan 2021 18:51:13 +0100 Subject: [PATCH 037/260] Return a more specific error when more than oneOf schemas match (#292) --- openapi3/schema.go | 9 +++++++- openapi3/schema_issue289_test.go | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 openapi3/schema_issue289_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 4fcc864ac..4968feca0 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -25,6 +25,9 @@ var ( errSchema = errors.New("Input does not match the schema") + // ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema + ErrOneOfConflict = errors.New("input matches more than one oneOf schemas") + // ErrSchemaInputNaN may be returned when validating a number ErrSchemaInputNaN = errors.New("NaN is not allowed") // ErrSchemaInputInf may be returned when validating a number @@ -851,11 +854,15 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if settings.failfast { return errSchema } - return &SchemaError{ + e := &SchemaError{ Value: value, Schema: schema, SchemaField: "oneOf", } + if ok > 1 { + e.Origin = ErrOneOfConflict + } + return e } } diff --git a/openapi3/schema_issue289_test.go b/openapi3/schema_issue289_test.go new file mode 100644 index 000000000..e4e4aad36 --- /dev/null +++ b/openapi3/schema_issue289_test.go @@ -0,0 +1,39 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue289(t *testing.T) { + spec := []byte(`components: + schemas: + Server: + properties: + address: + oneOf: + - $ref: "#/components/schemas/ip-address" + - $ref: "#/components/schemas/domain-name" + name: + type: string + type: object + domain-name: + maxLength: 10 + minLength: 5 + pattern: "((([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.)*([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.?)|\\." + type: string + ip-address: + pattern: "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$" + type: string +openapi: "3.0.1" +`) + + s, err := NewSwaggerLoader().LoadSwaggerFromData(spec) + require.NoError(t, err) + err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ + "name": "kin-openapi", + "address": "127.0.0.1", + }) + require.EqualError(t, err, ErrOneOfConflict.Error()) +} From 66309f4b3e518c98adc86fa99c2c38e268b43745 Mon Sep 17 00:00:00 2001 From: C H Date: Thu, 4 Feb 2021 07:51:40 +0100 Subject: [PATCH 038/260] fix bug on indice to compare (#295) --- openapi2/openapi2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 8cf0f0117..12cad02f2 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -178,7 +178,7 @@ func (ps Parameters) Less(i, j int) bool { return ps[i].Name < ps[j].Name } if ps[i].In != ps[j].In { - return ps[i].In < ps[i].In + return ps[i].In < ps[j].In } return ps[i].Ref < ps[j].Ref } From 33fc72130678cdcff6bcb152476c12558942768b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 23 Feb 2021 13:07:06 +0100 Subject: [PATCH 039/260] support extensions in oasv3.Server (#302) Signed-off-by: Pierre Fenoll --- openapi3/server.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openapi3/server.go b/openapi3/server.go index c6cd44353..da6323dbd 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -6,6 +6,8 @@ import ( "math" "net/url" "strings" + + "github.com/getkin/kin-openapi/jsoninfo" ) // Servers is specified by OpenAPI/Swagger standard version 3.0. @@ -37,11 +39,20 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) // Server is specified by OpenAPI/Swagger standard version 3.0. type Server struct { + ExtensionProps URL string `json:"url" yaml:"url"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` } +func (value *Server) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(value) +} + +func (value *Server) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, value) +} + func (server Server) ParameterNames() ([]string, error) { pattern := server.URL var params []string From 66fba45fb4b0f663d6a9264285608bcddd467281 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 23 Feb 2021 16:30:57 +0100 Subject: [PATCH 040/260] Add an example showing how to decode some extension props (#304) --- jsoninfo/unmarshal_test.go | 62 +++++++++++++------------- openapi2/openapi2_test.go | 3 +- openapi3/extension_test.go | 56 ++++++++++++++++------- openapi3/refs_test.go | 43 +++++++++--------- openapi3/swagger_loader_test.go | 5 +-- openapi3/testdata/testref.openapi.json | 1 + 6 files changed, 96 insertions(+), 74 deletions(-) diff --git a/jsoninfo/unmarshal_test.go b/jsoninfo/unmarshal_test.go index 77ab42bb3..c3dd957ac 100644 --- a/jsoninfo/unmarshal_test.go +++ b/jsoninfo/unmarshal_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewObjectDecoder(t *testing.T) { @@ -16,10 +16,10 @@ func TestNewObjectDecoder(t *testing.T) { `) t.Run("test new object decoder", func(t *testing.T) { decoder, err := NewObjectDecoder(data) - assert.Nil(t, err) - assert.NotNil(t, decoder) - assert.Equal(t, data, decoder.Data) - assert.Equal(t, 2, len(decoder.DecodeExtensionMap())) + require.NoError(t, err) + require.NotNil(t, decoder) + require.Equal(t, data, decoder.Data) + require.Equal(t, 2, len(decoder.DecodeExtensionMap())) }) } @@ -56,8 +56,8 @@ func TestUnmarshalStrictStruct(t *testing.T) { }, } err := UnmarshalStrictStruct(data, mockStruct) - assert.Nil(t, err) - assert.Equal(t, 1, decodeWithFnCalled) + require.NoError(t, err) + require.Equal(t, 1, decodeWithFnCalled) }) t.Run("test unmarshal with StrictStruct with err", func(t *testing.T) { @@ -72,8 +72,8 @@ func TestUnmarshalStrictStruct(t *testing.T) { }, } err := UnmarshalStrictStruct(data, mockStruct) - assert.NotNil(t, err) - assert.Equal(t, 1, decodeWithFnCalled) + require.Error(t, err) + require.Equal(t, 1, decodeWithFnCalled) }) } @@ -85,72 +85,72 @@ func TestDecodeStructFieldsAndExtensions(t *testing.T) { } `) decoder, err := NewObjectDecoder(data) - assert.Nil(t, err) - assert.NotNil(t, decoder) + require.NoError(t, err) + require.NotNil(t, decoder) t.Run("value is not pointer", func(t *testing.T) { var value interface{} - assert.Panics(t, func() { + require.Panics(t, func() { _ = decoder.DecodeStructFieldsAndExtensions(value) }, "value is not a pointer") }) t.Run("value is nil", func(t *testing.T) { var value *string = nil - assert.Panics(t, func() { + require.Panics(t, func() { _ = decoder.DecodeStructFieldsAndExtensions(value) }, "value is nil") }) t.Run("value is not struct", func(t *testing.T) { var value = "simple string" - assert.Panics(t, func() { + require.Panics(t, func() { _ = decoder.DecodeStructFieldsAndExtensions(&value) }, "value is not struct") }) t.Run("successfully decoded with all fields", func(t *testing.T) { d, err := NewObjectDecoder(data) - assert.Nil(t, err) - assert.NotNil(t, d) + require.NoError(t, err) + require.NotNil(t, d) var value = struct { Field1 string `json:"field1"` Field2 string `json:"field2"` }{} err = d.DecodeStructFieldsAndExtensions(&value) - assert.Nil(t, err) - assert.Equal(t, "field1", value.Field1) - assert.Equal(t, "field2", value.Field2) - assert.Equal(t, 0, len(d.DecodeExtensionMap())) + require.NoError(t, err) + require.Equal(t, "field1", value.Field1) + require.Equal(t, "field2", value.Field2) + require.Equal(t, 0, len(d.DecodeExtensionMap())) }) t.Run("successfully decoded with renaming field", func(t *testing.T) { d, err := NewObjectDecoder(data) - assert.Nil(t, err) - assert.NotNil(t, d) + require.NoError(t, err) + require.NotNil(t, d) var value = struct { Field1 string `json:"field1"` }{} err = d.DecodeStructFieldsAndExtensions(&value) - assert.Nil(t, err) - assert.Equal(t, "field1", value.Field1) - assert.Equal(t, 1, len(d.DecodeExtensionMap())) + require.NoError(t, err) + require.Equal(t, "field1", value.Field1) + require.Equal(t, 1, len(d.DecodeExtensionMap())) }) t.Run("un-successfully decoded due to data mismatch", func(t *testing.T) { d, err := NewObjectDecoder(data) - assert.Nil(t, err) - assert.NotNil(t, d) + require.NoError(t, err) + require.NotNil(t, d) var value = struct { Field1 int `json:"field1"` }{} err = d.DecodeStructFieldsAndExtensions(&value) - assert.NotNil(t, err) - assert.EqualError(t, err, "Error while unmarshalling property 'field1' (*int): json: cannot unmarshal string into Go value of type int") - assert.Equal(t, 0, value.Field1) - assert.Equal(t, 2, len(d.DecodeExtensionMap())) + require.Error(t, err) + require.EqualError(t, err, "Error while unmarshalling property 'field1' (*int): json: cannot unmarshal string into Go value of type int") + require.Equal(t, 0, value.Field1) + require.Equal(t, 2, len(d.DecodeExtensionMap())) }) } diff --git a/openapi2/openapi2_test.go b/openapi2/openapi2_test.go index 78194850a..1a5135d05 100644 --- a/openapi2/openapi2_test.go +++ b/openapi2/openapi2_test.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -21,5 +20,5 @@ func TestReadingSwagger(t *testing.T) { output, err := json.Marshal(swagger) require.NoError(t, err) - assert.JSONEq(t, string(input), string(output)) + require.JSONEq(t, string(input), string(output)) } diff --git a/openapi3/extension_test.go b/openapi3/extension_test.go index 3d0b233da..22ed6af8e 100644 --- a/openapi3/extension_test.go +++ b/openapi3/extension_test.go @@ -1,12 +1,36 @@ package openapi3 import ( + "encoding/json" + "fmt" "testing" "github.com/getkin/kin-openapi/jsoninfo" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func ExampleExtensionProps_DecodeWith() { + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + spec, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.json") + if err != nil { + panic(err) + } + + dec, err := jsoninfo.NewObjectDecoder(spec.Info.Extensions["x-my-extension"].(json.RawMessage)) + if err != nil { + panic(err) + } + var value struct { + Key int `json:"k"` + } + if err = spec.Info.DecodeWith(dec, &value); err != nil { + panic(err) + } + fmt.Println(value.Key) + // Output: 42 +} + func TestExtensionProps_EncodeWith(t *testing.T) { t.Run("successfully encoded", func(t *testing.T) { encoder := jsoninfo.NewObjectEncoder() @@ -22,7 +46,7 @@ func TestExtensionProps_EncodeWith(t *testing.T) { }{} err := extensionProps.EncodeWith(encoder, &value) - assert.Nil(t, err) + require.NoError(t, err) }) } @@ -35,7 +59,7 @@ func TestExtensionProps_DecodeWith(t *testing.T) { `) t.Run("successfully decode all the fields", func(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) - assert.Nil(t, err) + require.NoError(t, err) var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", @@ -49,15 +73,15 @@ func TestExtensionProps_DecodeWith(t *testing.T) { }{} err = extensionProps.DecodeWith(decoder, &value) - assert.Nil(t, err) - assert.Equal(t, 0, len(extensionProps.Extensions)) - assert.Equal(t, "value1", value.Field1) - assert.Equal(t, "value2", value.Field2) + require.NoError(t, err) + require.Equal(t, 0, len(extensionProps.Extensions)) + require.Equal(t, "value1", value.Field1) + require.Equal(t, "value2", value.Field2) }) t.Run("successfully decode some of the fields", func(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) - assert.Nil(t, err) + require.NoError(t, err) var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", @@ -70,14 +94,14 @@ func TestExtensionProps_DecodeWith(t *testing.T) { }{} err = extensionProps.DecodeWith(decoder, value) - assert.Nil(t, err) - assert.Equal(t, 1, len(extensionProps.Extensions)) - assert.Equal(t, "value1", value.Field1) + require.NoError(t, err) + require.Equal(t, 1, len(extensionProps.Extensions)) + require.Equal(t, "value1", value.Field1) }) t.Run("successfully decode none of the fields", func(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) - assert.Nil(t, err) + require.NoError(t, err) var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ @@ -92,9 +116,9 @@ func TestExtensionProps_DecodeWith(t *testing.T) { }{} err = extensionProps.DecodeWith(decoder, &value) - assert.Nil(t, err) - assert.Equal(t, 2, len(extensionProps.Extensions)) - assert.Empty(t, value.Field3) - assert.Empty(t, value.Field4) + require.NoError(t, err) + require.Equal(t, 2, len(extensionProps.Extensions)) + require.Empty(t, value.Field3) + require.Empty(t, value.Field4) }) } diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index 0c8b84570..bd79560cf 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/go-openapi/jsonpointer" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -220,57 +219,57 @@ components: ptr, err := jsonpointer.New("/paths/~1pet/put/responses/200/content") require.NoError(t, err) v, kind, err := ptr.Get(root) - assert.NoError(t, err) - assert.Equal(t, reflect.TypeOf(Content{}).Kind(), kind) - assert.IsType(t, Content{}, v) + require.NoError(t, err) + require.Equal(t, reflect.TypeOf(Content{}).Kind(), kind) + require.IsType(t, Content{}, v) ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content/application~1json/schema") require.NoError(t, err) v, kind, err = ptr.Get(root) - assert.NoError(t, err) - assert.Equal(t, reflect.Ptr, kind) - assert.IsType(t, &Ref{}, v) - assert.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) + require.NoError(t, err) + require.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Ref{}, v) + require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) ptr, err = jsonpointer.New("/components/schemas/Pets/items") require.NoError(t, err) v, kind, err = ptr.Get(root) - assert.NoError(t, err) - assert.Equal(t, reflect.Ptr, kind) + require.NoError(t, err) + require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Ref{}, v) - assert.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) + require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) ptr, err = jsonpointer.New("/components/schemas/Error/properties/code") require.NoError(t, err) v, kind, err = ptr.Get(root) - assert.NoError(t, err) - assert.Equal(t, reflect.Ptr, kind) + require.NoError(t, err) + require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Schema{}, v) - assert.Equal(t, "integer", v.(*Schema).Type) + require.Equal(t, "integer", v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/0") require.NoError(t, err) v, kind, err = ptr.Get(root) - assert.NoError(t, err) - assert.Equal(t, reflect.Ptr, kind) + require.NoError(t, err) + require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Schema{}, v) - assert.Equal(t, "string", v.(*Schema).Type) + require.Equal(t, "string", v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/1") require.NoError(t, err) v, kind, err = ptr.Get(root) - assert.NoError(t, err) - assert.Equal(t, reflect.Ptr, kind) + require.NoError(t, err) + require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Schema{}, v) - assert.Equal(t, "integer", v.(*Schema).Type) + require.Equal(t, "integer", v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/5") require.NoError(t, err) _, _, err = ptr.Get(root) - assert.Error(t, err) + require.Error(t, err) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/-1") require.NoError(t, err) _, _, err = ptr.Get(root) - assert.Error(t, err) + require.Error(t, err) } diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go index 692ebd2b8..40ae7196a 100644 --- a/openapi3/swagger_loader_test.go +++ b/openapi3/swagger_loader_test.go @@ -67,14 +67,13 @@ paths: } func ExampleSwaggerLoader() { - source := `{"info":{"description":"An API"}}` + const source = `{"info":{"description":"An API"}}` swagger, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(source)) if err != nil { panic(err) } fmt.Print(swagger.Info.Description) - // Output: - // An API + // Output: An API } func TestResolveSchemaRef(t *testing.T) { diff --git a/openapi3/testdata/testref.openapi.json b/openapi3/testdata/testref.openapi.json index 5beb61e34..42b651afb 100644 --- a/openapi3/testdata/testref.openapi.json +++ b/openapi3/testdata/testref.openapi.json @@ -2,6 +2,7 @@ "openapi": "3.0.0", "info": { "title": "", + "x-my-extension": {"k": 42}, "version": "1" }, "paths": {}, From b2761ee838a4d9b27bfb84adca6b0abbbbddf62b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 23 Feb 2021 16:54:17 +0100 Subject: [PATCH 041/260] =?UTF-8?q?clarify=20defaults=20around=20openapi3f?= =?UTF-8?q?ilter.Options=20and=20openapi3filter.Aut=E2=80=A6=20(#305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pierre Fenoll --- openapi3filter/options.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openapi3filter/options.go b/openapi3filter/options.go index 60e5475f1..b0fb39df3 100644 --- a/openapi3filter/options.go +++ b/openapi3filter/options.go @@ -1,15 +1,18 @@ package openapi3filter -import ( - "context" -) - +// DefaultOptions do not set an AuthenticationFunc. +// A spec with security schemes defined will not pass validation +// unless an AuthenticationFunc is defined. var DefaultOptions = &Options{} +// Options used by ValidateRequest and ValidateResponse type Options struct { ExcludeRequestBody bool ExcludeResponseBody bool IncludeResponseStatus bool - MultiError bool - AuthenticationFunc func(c context.Context, input *AuthenticationInput) error + + MultiError bool + + // See NoopAuthenticationFunc + AuthenticationFunc AuthenticationFunc } From 6d6f1ef76aaa51df4dc9dece4b5a346d184cc6d5 Mon Sep 17 00:00:00 2001 From: Sergi Castro Date: Tue, 23 Feb 2021 22:29:14 +0100 Subject: [PATCH 042/260] Add extensions in missing resources (#306) --- openapi2/openapi2.go | 9 +++++++++ openapi3/server.go | 17 +++++++++++++---- openapi3/tag.go | 11 +++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 12cad02f2..2a3006cfd 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -237,11 +237,20 @@ func (response *Response) UnmarshalJSON(data []byte) error { } type Header struct { + openapi3.ExtensionProps Ref string `json:"$ref,omitempty"` Description string `json:"description,omitempty"` Type string `json:"type,omitempty"` } +func (header *Header) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(header) +} + +func (header *Header) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, header) +} + type SecurityRequirements []map[string][]string type SecurityScheme struct { diff --git a/openapi3/server.go b/openapi3/server.go index da6323dbd..eeeae0668 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -45,12 +45,12 @@ type Server struct { Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` } -func (value *Server) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(value) +func (server *Server) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(server) } -func (value *Server) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +func (server *Server) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, server) } func (server Server) ParameterNames() ([]string, error) { @@ -138,11 +138,20 @@ func (server *Server) Validate(c context.Context) (err error) { // ServerVariable is specified by OpenAPI/Swagger standard version 3.0. type ServerVariable struct { + ExtensionProps Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` } +func (serverVariable *ServerVariable) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(serverVariable) +} + +func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, serverVariable) +} + func (serverVariable *ServerVariable) Validate(c context.Context) error { switch serverVariable.Default.(type) { case float64, string: diff --git a/openapi3/tag.go b/openapi3/tag.go index d5de72d59..210b69248 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -1,5 +1,7 @@ package openapi3 +import "github.com/getkin/kin-openapi/jsoninfo" + // Tags is specified by OpenAPI/Swagger 3.0 standard. type Tags []*Tag @@ -14,7 +16,16 @@ func (tags Tags) Get(name string) *Tag { // Tag is specified by OpenAPI/Swagger 3.0 standard. type Tag struct { + ExtensionProps Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } + +func (t *Tag) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(t) +} + +func (t *Tag) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, t) +} From bdba5d10c8e88a4d3e24310c918a6fc52c0ac8dc Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 25 Feb 2021 18:00:26 +0100 Subject: [PATCH 043/260] Enlarge support for JSON Path in $ref resolution (#307) --- openapi3/discriminator_test.go | 2 +- openapi3/swagger_loader.go | 293 ++++++++++-------- openapi3/swagger_loader_test.go | 14 +- .../refInRef/messages/definitions.json | 7 + .../testdata/refInRef/messages/request.json | 11 + .../testdata/refInRef/messages/response.json | 9 + openapi3/testdata/refInRef/openapi.json | 34 ++ openapi3/testdata/testref.openapi.json | 2 +- openapi3/testdata/testref.openapi.yml | 3 +- 9 files changed, 246 insertions(+), 129 deletions(-) create mode 100644 openapi3/testdata/refInRef/messages/definitions.json create mode 100644 openapi3/testdata/refInRef/messages/request.json create mode 100644 openapi3/testdata/refInRef/messages/response.json create mode 100644 openapi3/testdata/refInRef/openapi.json diff --git a/openapi3/discriminator_test.go b/openapi3/discriminator_test.go index 602b4fd68..e3ed22c27 100644 --- a/openapi3/discriminator_test.go +++ b/openapi3/discriminator_test.go @@ -38,5 +38,5 @@ var jsonSpecWithDiscriminator = []byte(` func TestParsingDiscriminator(t *testing.T) { loader, err := NewSwaggerLoader().LoadSwaggerFromData(jsonSpecWithDiscriminator) require.NoError(t, err) - require.Equal(t, 2, len(loader.Components.Schemas["MyResponseType"].Value.OneOf)) + require.Equal(t, 2, len(loader.Components.Schemas["MyResponseType"].Value.Discriminator.Mapping)) } diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 44d98ab7a..1005dfc75 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -20,22 +20,33 @@ func foundUnresolvedRef(ref string) error { return fmt.Errorf("found unresolved ref: %q", ref) } -func failedToResolveRefFragment(value string) error { - return fmt.Errorf("failed to resolve fragment in URI: %q", value) -} - -func failedToResolveRefFragmentPart(value string, what string) error { +func failedToResolveRefFragmentPart(value, what string) error { return fmt.Errorf("failed to resolve %q in fragment in URI: %q", what, value) } +// SwaggerLoader helps deserialize a Swagger object type SwaggerLoader struct { - IsExternalRefsAllowed bool - Context context.Context + // IsExternalRefsAllowed enables visiting other files + IsExternalRefsAllowed bool + + // LoadSwaggerFromURIFunc allows overriding the file/URL reading func LoadSwaggerFromURIFunc func(loader *SwaggerLoader, url *url.URL) (*Swagger, error) - visited map[interface{}]struct{} - visitedFiles map[string]struct{} + + Context context.Context + + visitedFiles map[string]struct{} + + visitedHeader map[*Header]struct{} + visitedParameter map[*Parameter]struct{} + visitedRequestBody map[*RequestBody]struct{} + visitedResponse map[*Response]struct{} + visitedSchema map[*Schema]struct{} + visitedSecurityScheme map[*SecurityScheme]struct{} + visitedExample map[*Example]struct{} + visitedLink map[*Link]struct{} } +// NewSwaggerLoader returns an empty SwaggerLoader func NewSwaggerLoader() *SwaggerLoader { return &SwaggerLoader{} } @@ -44,6 +55,7 @@ func (swaggerLoader *SwaggerLoader) reset() { swaggerLoader.visitedFiles = make(map[string]struct{}) } +// LoadSwaggerFromURI loads a spec from a remote URL func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swagger, error) { swaggerLoader.reset() return swaggerLoader.loadSwaggerFromURIInternal(location) @@ -116,6 +128,7 @@ func readURL(location *url.URL) ([]byte, error) { return data, nil } +// LoadSwaggerFromFile loads a spec from a local file path func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) { swaggerLoader.reset() return swaggerLoader.loadSwaggerFromFileInternal(path) @@ -135,6 +148,7 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*S return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, pathAsURL) } +// LoadSwaggerFromData loads a spec from a byte array func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, error) { swaggerLoader.reset() return swaggerLoader.loadSwaggerFromDataInternal(data) @@ -163,9 +177,17 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []b return swagger, swaggerLoader.ResolveRefsIn(swagger, path) } +// ResolveRefsIn expands references if for instance spec was just unmarshalled func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.URL) (err error) { - if swaggerLoader.visited == nil { - swaggerLoader.visited = make(map[interface{}]struct{}) + if swaggerLoader.visitedHeader == nil { + swaggerLoader.visitedHeader = make(map[*Header]struct{}) + swaggerLoader.visitedParameter = make(map[*Parameter]struct{}) + swaggerLoader.visitedRequestBody = make(map[*RequestBody]struct{}) + swaggerLoader.visitedResponse = make(map[*Response]struct{}) + swaggerLoader.visitedSchema = make(map[*Schema]struct{}) + swaggerLoader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) + swaggerLoader.visitedExample = make(map[*Example]struct{}) + swaggerLoader.visitedLink = make(map[*Link]struct{}) } if swaggerLoader.visitedFiles == nil { swaggerLoader.reset() @@ -253,38 +275,66 @@ func isSingleRefElement(ref string) bool { return !strings.Contains(ref, "#") } -func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref string, path *url.URL) ( - cursor interface{}, +func (swaggerLoader *SwaggerLoader) resolveComponent( + swagger *Swagger, + ref string, + path *url.URL, + resolved interface{}, +) ( componentPath *url.URL, err error, ) { if swagger, ref, componentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, path); err != nil { - return nil, nil, err + return nil, err } parsedURL, err := url.Parse(ref) if err != nil { - return nil, nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) + return nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) } fragment := parsedURL.Fragment if !strings.HasPrefix(fragment, "/") { - err := fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) - return nil, nil, err + return nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) } + var cursor interface{} cursor = swagger for _, pathPart := range strings.Split(fragment[1:], "/") { pathPart = unescapeRefString(pathPart) if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil { - return nil, nil, fmt.Errorf("failed to resolve %q in fragment in URI: %q: %v", ref, pathPart, err.Error()) + e := failedToResolveRefFragmentPart(ref, pathPart) + return nil, fmt.Errorf("%s: %s", e.Error(), err.Error()) } if cursor == nil { - return nil, nil, failedToResolveRefFragmentPart(ref, pathPart) + return nil, failedToResolveRefFragmentPart(ref, pathPart) } } - return cursor, componentPath, nil + switch { + case reflect.TypeOf(cursor) == reflect.TypeOf(resolved): + reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem()) + return componentPath, nil + + case reflect.TypeOf(cursor) == reflect.TypeOf(map[string]interface{}{}): + codec := func(got, expect interface{}) error { + enc, err := json.Marshal(got) + if err != nil { + return err + } + if err = json.Unmarshal(enc, expect); err != nil { + return err + } + return nil + } + if err := codec(cursor, resolved); err != nil { + return nil, fmt.Errorf("bad data in %q", ref) + } + return componentPath, nil + + default: + return nil, fmt.Errorf("bad data in %q", ref) + } } func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, error) { @@ -302,13 +352,15 @@ func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, e return nil, err } index := int(i) - if index >= val.Len() { + if 0 > index || index >= val.Len() { return nil, errors.New("slice index out of bounds") } return val.Index(index).Interface(), nil case reflect.Struct: + hasFields := false for i := 0; i < val.NumField(); i++ { + hasFields = true field := val.Type().Field(i) tagValue := field.Tag.Get("yaml") yamlKey := strings.Split(tagValue, ",")[0] @@ -316,12 +368,23 @@ func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, e return val.Field(i).Interface(), nil } } - // if cursor if a "ref wrapper" struct (e.g. RequestBodyRef), try digging into its Value field - _, ok := val.Type().FieldByName("Value") - if ok { - return drillIntoSwaggerField(val.FieldByName("Value").Interface(), fieldName) // recurse into .Value + // if cursor is a "ref wrapper" struct (e.g. RequestBodyRef), + if _, ok := val.Type().FieldByName("Value"); ok { + // try digging into its Value field + return drillIntoSwaggerField(val.FieldByName("Value").Interface(), fieldName) + } + if hasFields { + if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "ExtensionProps" { + extensions := val.Field(0).Interface().(ExtensionProps).Extensions + if enc, ok := extensions[fieldName]; ok { + var dec interface{} + if err := json.Unmarshal(enc.(json.RawMessage), &dec); err != nil { + return nil, err + } + return dec, nil + } + } } - // give up return nil, fmt.Errorf("struct field %q not found", fieldName) default: @@ -356,12 +419,13 @@ func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref stri return swagger, ref, componentPath, nil } -func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, path *url.URL) error { - visited := swaggerLoader.visited - if _, isVisited := visited[component]; isVisited { - return nil +func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, documentPath *url.URL) error { + if component != nil && component.Value != nil { + if _, ok := swaggerLoader.visitedHeader[component.Value]; ok { + return nil + } + swaggerLoader.visitedHeader[component.Value] = struct{}{} } - visited[component] = struct{}{} const prefix = "#/components/headers/" if component == nil { @@ -370,21 +434,18 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var header Header - if err := swaggerLoader.loadSingleElementFromURI(ref, path, &header); err != nil { + if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { return err } component.Value = &header } else { - untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path) + var resolved HeaderRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) if err != nil { return err } - resolved, ok := untypedResolved.(*HeaderRef) - if !ok { - return failedToResolveRefFragment(ref) - } - if err := swaggerLoader.resolveHeaderRef(swagger, resolved, componentPath); err != nil { + if err := swaggerLoader.resolveHeaderRef(swagger, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -395,7 +456,7 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component return nil } if schema := value.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, path); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err } } @@ -403,11 +464,12 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component } func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) error { - visited := swaggerLoader.visited - if _, isVisited := visited[component]; isVisited { - return nil + if component != nil && component.Value != nil { + if _, ok := swaggerLoader.visitedParameter[component.Value]; ok { + return nil + } + swaggerLoader.visitedParameter[component.Value] = struct{}{} } - visited[component] = struct{}{} const prefix = "#/components/parameters/" if component == nil { @@ -422,15 +484,12 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon } component.Value = ¶m } else { - untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath) + var resolved ParameterRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) if err != nil { return err } - resolved, ok := untypedResolved.(*ParameterRef) - if !ok { - return failedToResolveRefFragment(ref) - } - if err := swaggerLoader.resolveParameterRef(swagger, resolved, componentPath); err != nil { + if err := swaggerLoader.resolveParameterRef(swagger, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -464,12 +523,13 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon return nil } -func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, path *url.URL) error { - visited := swaggerLoader.visited - if _, isVisited := visited[component]; isVisited { - return nil +func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, documentPath *url.URL) error { + if component != nil && component.Value != nil { + if _, ok := swaggerLoader.visitedRequestBody[component.Value]; ok { + return nil + } + swaggerLoader.visitedRequestBody[component.Value] = struct{}{} } - visited[component] = struct{}{} const prefix = "#/components/requestBodies/" if component == nil { @@ -478,21 +538,18 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var requestBody RequestBody - if err := swaggerLoader.loadSingleElementFromURI(ref, path, &requestBody); err != nil { + if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { return err } component.Value = &requestBody } else { - untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path) + var resolved RequestBodyRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) if err != nil { return err } - resolved, ok := untypedResolved.(*RequestBodyRef) - if !ok { - return failedToResolveRefFragment(ref) - } - if err = swaggerLoader.resolveRequestBodyRef(swagger, resolved, componentPath); err != nil { + if err = swaggerLoader.resolveRequestBodyRef(swagger, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -504,13 +561,13 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp } for _, contentType := range value.Content { for name, example := range contentType.Examples { - if err := swaggerLoader.resolveExampleRef(swagger, example, path); err != nil { + if err := swaggerLoader.resolveExampleRef(swagger, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, path); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err } } @@ -519,11 +576,12 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp } func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) error { - visited := swaggerLoader.visited - if _, isVisited := visited[component]; isVisited { - return nil + if component != nil && component.Value != nil { + if _, ok := swaggerLoader.visitedResponse[component.Value]; ok { + return nil + } + swaggerLoader.visitedResponse[component.Value] = struct{}{} } - visited[component] = struct{}{} const prefix = "#/components/responses/" if component == nil { @@ -531,24 +589,19 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone } ref := component.Ref if ref != "" { - if isSingleRefElement(ref) { var resp Response if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { return err } - component.Value = &resp } else { - untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath) + var resolved ResponseRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) if err != nil { return err } - resolved, ok := untypedResolved.(*ResponseRef) - if !ok { - return failedToResolveRefFragment(ref) - } - if err := swaggerLoader.resolveResponseRef(swagger, resolved, componentPath); err != nil { + if err := swaggerLoader.resolveResponseRef(swagger, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -594,11 +647,12 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone } func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) error { - visited := swaggerLoader.visited - if _, isVisited := visited[component]; isVisited { - return nil + if component != nil && component.Value != nil { + if _, ok := swaggerLoader.visitedSchema[component.Value]; ok { + return nil + } + swaggerLoader.visitedSchema[component.Value] = struct{}{} } - visited[component] = struct{}{} const prefix = "#/components/schemas/" if component == nil { @@ -613,16 +667,12 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component } component.Value = &schema } else { - untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath) + var resolved SchemaRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) if err != nil { return err } - - resolved, ok := untypedResolved.(*SchemaRef) - if !ok { - return failedToResolveRefFragment(ref) - } - if err := swaggerLoader.resolveSchemaRef(swagger, resolved, componentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -679,12 +729,13 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component return nil } -func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, path *url.URL) error { - visited := swaggerLoader.visited - if _, isVisited := visited[component]; isVisited { - return nil +func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, documentPath *url.URL) error { + if component != nil && component.Value != nil { + if _, ok := swaggerLoader.visitedSecurityScheme[component.Value]; ok { + return nil + } + swaggerLoader.visitedSecurityScheme[component.Value] = struct{}{} } - visited[component] = struct{}{} const prefix = "#/components/securitySchemes/" if component == nil { @@ -693,21 +744,18 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var scheme SecurityScheme - if err := swaggerLoader.loadSingleElementFromURI(ref, path, &scheme); err != nil { + if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { return err } component.Value = &scheme } else { - untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path) + var resolved SecuritySchemeRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) if err != nil { return err } - resolved, ok := untypedResolved.(*SecuritySchemeRef) - if !ok { - return failedToResolveRefFragment(ref) - } - if err := swaggerLoader.resolveSecuritySchemeRef(swagger, resolved, componentPath); err != nil { + if err := swaggerLoader.resolveSecuritySchemeRef(swagger, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -716,12 +764,13 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c return nil } -func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, path *url.URL) error { - visited := swaggerLoader.visited - if _, isVisited := visited[component]; isVisited { - return nil +func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, documentPath *url.URL) error { + if component != nil && component.Value != nil { + if _, ok := swaggerLoader.visitedExample[component.Value]; ok { + return nil + } + swaggerLoader.visitedExample[component.Value] = struct{}{} } - visited[component] = struct{}{} const prefix = "#/components/examples/" if component == nil { @@ -730,21 +779,18 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var example Example - if err := swaggerLoader.loadSingleElementFromURI(ref, path, &example); err != nil { + if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { return err } component.Value = &example } else { - untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path) + var resolved ExampleRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) if err != nil { return err } - resolved, ok := untypedResolved.(*ExampleRef) - if !ok { - return failedToResolveRefFragment(ref) - } - if err := swaggerLoader.resolveExampleRef(swagger, resolved, componentPath); err != nil { + if err := swaggerLoader.resolveExampleRef(swagger, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -753,12 +799,13 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen return nil } -func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, path *url.URL) error { - visited := swaggerLoader.visited - if _, isVisited := visited[component]; isVisited { - return nil +func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) error { + if component != nil && component.Value != nil { + if _, ok := swaggerLoader.visitedLink[component.Value]; ok { + return nil + } + swaggerLoader.visitedLink[component.Value] = struct{}{} } - visited[component] = struct{}{} const prefix = "#/components/links/" if component == nil { @@ -767,21 +814,18 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var link Link - if err := swaggerLoader.loadSingleElementFromURI(ref, path, &link); err != nil { + if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { return err } component.Value = &link } else { - untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path) + var resolved LinkRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) if err != nil { return err } - resolved, ok := untypedResolved.(*LinkRef) - if !ok { - return failedToResolveRefFragment(ref) - } - if err := swaggerLoader.resolveLinkRef(swagger, resolved, componentPath); err != nil { + if err := swaggerLoader.resolveLinkRef(swagger, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -791,16 +835,15 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * } func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) { - visited := swaggerLoader.visitedFiles key := "_" if documentPath != nil { key = documentPath.EscapedPath() } key += entrypoint - if _, isVisited := visited[key]; isVisited { + if _, ok := swaggerLoader.visitedFiles[key]; ok { return nil } - visited[key] = struct{}{} + swaggerLoader.visitedFiles[key] = struct{}{} const prefix = "#/paths/" if pathItem == nil { diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go index 40ae7196a..578932039 100644 --- a/openapi3/swagger_loader_test.go +++ b/openapi3/swagger_loader_test.go @@ -201,7 +201,7 @@ func TestLoadErrorOnRefMisuse(t *testing.T) { openapi: '3.0.0' servers: [{url: /}] info: - title: '' + title: Some API version: '1' components: schemas: @@ -211,6 +211,7 @@ paths: put: description: '' requestBody: + # Uses a schema ref instead of a requestBody ref. $ref: '#/components/schemas/Thing' responses: '201': @@ -314,6 +315,17 @@ func TestLoadFromRemoteURL(t *testing.T) { require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type) } +func TestLoadWithReferenceInReference(t *testing.T) { + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadSwaggerFromFile("testdata/refInRef/openapi.json") + require.NoError(t, err) + 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) +} + func TestLoadFileWithExternalSchemaRef(t *testing.T) { loader := NewSwaggerLoader() loader.IsExternalRefsAllowed = true diff --git a/openapi3/testdata/refInRef/messages/definitions.json b/openapi3/testdata/refInRef/messages/definitions.json new file mode 100644 index 000000000..78b942836 --- /dev/null +++ b/openapi3/testdata/refInRef/messages/definitions.json @@ -0,0 +1,7 @@ +{ + "definitions": { + "External": { + "type": "string" + } + } +} diff --git a/openapi3/testdata/refInRef/messages/request.json b/openapi3/testdata/refInRef/messages/request.json new file mode 100644 index 000000000..10ff329bc --- /dev/null +++ b/openapi3/testdata/refInRef/messages/request.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "required": [ + "definition_reference" + ], + "properties": { + "definition_reference": { + "$ref": "definitions.json#/definitions/External" + } + } +} diff --git a/openapi3/testdata/refInRef/messages/response.json b/openapi3/testdata/refInRef/messages/response.json new file mode 100644 index 000000000..b636f528b --- /dev/null +++ b/openapi3/testdata/refInRef/messages/response.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + } + } +} diff --git a/openapi3/testdata/refInRef/openapi.json b/openapi3/testdata/refInRef/openapi.json new file mode 100644 index 000000000..0e9a5b1be --- /dev/null +++ b/openapi3/testdata/refInRef/openapi.json @@ -0,0 +1,34 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Reference in reference example", + "version": "1.0.0" + }, + "paths": { + "/api/test/ref/in/ref": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "messages/request.json" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "messages/response.json" + } + } + } + } + } + } + } + } +} diff --git a/openapi3/testdata/testref.openapi.json b/openapi3/testdata/testref.openapi.json index 42b651afb..f25ffbd3a 100644 --- a/openapi3/testdata/testref.openapi.json +++ b/openapi3/testdata/testref.openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "title": "", + "title": "OAI Specification w/ refs in JSON", "x-my-extension": {"k": 42}, "version": "1" }, diff --git a/openapi3/testdata/testref.openapi.yml b/openapi3/testdata/testref.openapi.yml index fe755936c..eace2456a 100644 --- a/openapi3/testdata/testref.openapi.yml +++ b/openapi3/testdata/testref.openapi.yml @@ -2,9 +2,10 @@ openapi: 3.0.0 info: title: 'OAI Specification w/ refs in YAML' + # x-my-extension: {k: 42}, version: '1' paths: {} components: schemas: AnotherTestSchema: - "$ref": components.openapi.yml#/components/schemas/CustomTestSchema + $ref: 'components.openapi.yml#/components/schemas/CustomTestSchema' From 89012b7cfdaa0da485d7e198a2300b7edb77e24a Mon Sep 17 00:00:00 2001 From: hottestseason Date: Tue, 2 Mar 2021 06:04:50 +0900 Subject: [PATCH 044/260] Prevent infinite loop while loading openapi spec with recursive references (#310) --- openapi3/swagger_loader.go | 17 +++++++++++++++-- openapi3/swagger_loader_recursive_ref_test.go | 17 +++++++++++++++++ .../testdata/recursiveRef/components/Bar.yml | 2 ++ .../testdata/recursiveRef/components/Foo.yml | 6 ++++++ openapi3/testdata/recursiveRef/openapi.yml | 13 +++++++++++++ openapi3/testdata/recursiveRef/paths/foo.yml | 11 +++++++++++ 6 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 openapi3/swagger_loader_recursive_ref_test.go create mode 100644 openapi3/testdata/recursiveRef/components/Bar.yml create mode 100644 openapi3/testdata/recursiveRef/components/Foo.yml create mode 100644 openapi3/testdata/recursiveRef/openapi.yml create mode 100644 openapi3/testdata/recursiveRef/paths/foo.yml diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 1005dfc75..ed2b05225 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -34,7 +34,8 @@ type SwaggerLoader struct { Context context.Context - visitedFiles map[string]struct{} + visitedFiles map[string]struct{} + visitedSwaggers map[string]*Swagger visitedHeader map[*Header]struct{} visitedParameter map[*Parameter]struct{} @@ -53,6 +54,7 @@ func NewSwaggerLoader() *SwaggerLoader { func (swaggerLoader *SwaggerLoader) reset() { swaggerLoader.visitedFiles = make(map[string]struct{}) + swaggerLoader.visitedSwaggers = make(map[string]*Swagger) } // LoadSwaggerFromURI loads a spec from a remote URL @@ -170,11 +172,22 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, pat } func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []byte, path *url.URL) (*Swagger, error) { + visited, ok := swaggerLoader.visitedSwaggers[path.String()] + if ok { + return visited, nil + } + swagger := &Swagger{} + swaggerLoader.visitedSwaggers[path.String()] = swagger + if err := yaml.Unmarshal(data, swagger); err != nil { return nil, err } - return swagger, swaggerLoader.ResolveRefsIn(swagger, path) + if err := swaggerLoader.ResolveRefsIn(swagger, path); err != nil { + return nil, err + } + + return swagger, nil } // ResolveRefsIn expands references if for instance spec was just unmarshalled diff --git a/openapi3/swagger_loader_recursive_ref_test.go b/openapi3/swagger_loader_recursive_ref_test.go new file mode 100644 index 000000000..45842d8f1 --- /dev/null +++ b/openapi3/swagger_loader_recursive_ref_test.go @@ -0,0 +1,17 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoaderSupportsRecursiveReference(t *testing.T) { + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadSwaggerFromFile("testdata/recursiveRef/openapi.yml") + 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["foo"].Value.Properties["bar"].Value.Items.Value.Example) +} diff --git a/openapi3/testdata/recursiveRef/components/Bar.yml b/openapi3/testdata/recursiveRef/components/Bar.yml new file mode 100644 index 000000000..cc59fc27b --- /dev/null +++ b/openapi3/testdata/recursiveRef/components/Bar.yml @@ -0,0 +1,2 @@ +type: string +example: bar diff --git a/openapi3/testdata/recursiveRef/components/Foo.yml b/openapi3/testdata/recursiveRef/components/Foo.yml new file mode 100644 index 000000000..0c0899277 --- /dev/null +++ b/openapi3/testdata/recursiveRef/components/Foo.yml @@ -0,0 +1,6 @@ +type: object +properties: + bar: + type: array + items: + $ref: ../openapi.yml#/components/schemas/Bar diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml new file mode 100644 index 000000000..5dfcfbf7c --- /dev/null +++ b/openapi3/testdata/recursiveRef/openapi.yml @@ -0,0 +1,13 @@ +openapi: "3.0.3" +info: + title: Recursive refs example + version: "1.0" +paths: + /foo: + $ref: ./paths/foo.yml +components: + schemas: + Foo: + $ref: ./components/Foo.yml + Bar: + $ref: ./components/Bar.yml diff --git a/openapi3/testdata/recursiveRef/paths/foo.yml b/openapi3/testdata/recursiveRef/paths/foo.yml new file mode 100644 index 000000000..dd6c15d0f --- /dev/null +++ b/openapi3/testdata/recursiveRef/paths/foo.yml @@ -0,0 +1,11 @@ +get: + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + foo: + $ref: ../openapi.yml#/components/schemas/Foo From 4c01aaedaca1dc836f955591c5fb59d53ba68c16 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 2 Mar 2021 17:17:28 +0100 Subject: [PATCH 045/260] nitpicks (#313) --- .github/workflows/go.yml | 12 ++++++++---- go.sum | 5 +++-- openapi2conv/openapi2_conv.go | 6 +++--- openapi3/swagger_loader.go | 22 ++++++++++++++++++---- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f8f3669fc..41b196f7d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -25,14 +25,18 @@ jobs: go-version: 1.x - run: go version - - run: go mod download && go mod verify - - run: go test ./... + - run: go mod download && go mod tidy && go mod verify + - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + shell: bash + - run: go vet ./... + - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + shell: bash + - run: go fmt ./... - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] shell: bash + - run: go test ./... - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] shell: bash - - run: go get -u -a -v ./... && go mod tidy && go mod verify - - run: git --no-pager diff diff --git a/go.sum b/go.sum index 60f84360b..f1e462c68 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -8,8 +7,10 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= @@ -20,8 +21,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 03a880279..f3f8c5c34 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -782,9 +782,9 @@ func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) open if requirements == nil { return nil } - result := make([]map[string][]string, len(requirements)) - for i, item := range requirements { - result[i] = item + result := make([]map[string][]string, 0, len(requirements)) + for _, item := range requirements { + result = append(result, item) } return result } diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index ed2b05225..6755b2917 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -37,14 +37,14 @@ type SwaggerLoader struct { visitedFiles map[string]struct{} visitedSwaggers map[string]*Swagger + visitedExample map[*Example]struct{} visitedHeader map[*Header]struct{} + visitedLink map[*Link]struct{} visitedParameter map[*Parameter]struct{} visitedRequestBody map[*RequestBody]struct{} visitedResponse map[*Response]struct{} visitedSchema map[*Schema]struct{} visitedSecurityScheme map[*SecurityScheme]struct{} - visitedExample map[*Example]struct{} - visitedLink map[*Link]struct{} } // NewSwaggerLoader returns an empty SwaggerLoader @@ -192,15 +192,29 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []b // ResolveRefsIn expands references if for instance spec was just unmarshalled func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.URL) (err error) { + if swaggerLoader.visitedExample == nil { + swaggerLoader.visitedExample = make(map[*Example]struct{}) + } if swaggerLoader.visitedHeader == nil { swaggerLoader.visitedHeader = make(map[*Header]struct{}) + } + if swaggerLoader.visitedLink == nil { + swaggerLoader.visitedLink = make(map[*Link]struct{}) + } + if swaggerLoader.visitedParameter == nil { swaggerLoader.visitedParameter = make(map[*Parameter]struct{}) + } + if swaggerLoader.visitedRequestBody == nil { swaggerLoader.visitedRequestBody = make(map[*RequestBody]struct{}) + } + if swaggerLoader.visitedResponse == nil { swaggerLoader.visitedResponse = make(map[*Response]struct{}) + } + if swaggerLoader.visitedSchema == nil { swaggerLoader.visitedSchema = make(map[*Schema]struct{}) + } + if swaggerLoader.visitedSecurityScheme == nil { swaggerLoader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) - swaggerLoader.visitedExample = make(map[*Example]struct{}) - swaggerLoader.visitedLink = make(map[*Link]struct{}) } if swaggerLoader.visitedFiles == nil { swaggerLoader.reset() From 49752fcc7c6b1cffce672445821bf8a5baf80702 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 2 Mar 2021 18:33:02 +0100 Subject: [PATCH 046/260] mention alternatives in README.md (#315) Signed-off-by: Pierre Fenoll --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bc4a733d7..979c08164 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,13 @@ Here's some projects that depend on _kin-openapi_: * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Goa is a framework for building micro-services and APIs in Go using a unique design-first approach." * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request) -## Alternative projects - * [go-openapi](https://github.com/go-openapi) - * Supports OpenAPI version 2. - * See [this list](https://github.com/OAI/OpenAPI-Specification/blob/master/IMPLEMENTATIONS.md). +## Alternatives +* [go-swagger](https://github.com/go-swagger/go-swagger) stated [*OpenAPIv3 won't be supported*](https://github.com/go-swagger/go-swagger/issues/1122#issuecomment-575968499) +* [swaggo](https://github.com/swaggo/swag) has an [open issue on OpenAPIv3](https://github.com/swaggo/swag/issues/386) +* [go-openapi](https://github.com/go-openapi)'s [spec3](https://github.com/go-openapi/spec3) + * an iteration on [spec](https://github.com/go-openapi/spec) (for OpenAPIv2) + * see [README](https://github.com/go-openapi/spec3/tree/3fab9faa9094e06ebd19ded7ea96d156c2283dca#oai-object-model---) for the missing parts +* See [https://github.com/OAI](https://github.com/OAI)'s [great tooling list](https://github.com/OAI/OpenAPI-Specification/blob/master/IMPLEMENTATIONS.md) # Structure * _openapi2_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi2)) From ea43ca73b122b9516273ed106c9d58792ce52488 Mon Sep 17 00:00:00 2001 From: hottestseason Date: Wed, 3 Mar 2021 23:33:26 +0900 Subject: [PATCH 047/260] Bypass any file/URL reading by ReadFromURIFunc (#316) --- openapi3/swagger_loader.go | 18 +++++++++++---- .../swagger_loader_read_from_uri_func_test.go | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 openapi3/swagger_loader_read_from_uri_func_test.go diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 6755b2917..db0eb44be 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -29,9 +29,12 @@ type SwaggerLoader struct { // IsExternalRefsAllowed enables visiting other files IsExternalRefsAllowed bool - // LoadSwaggerFromURIFunc allows overriding the file/URL reading func + // LoadSwaggerFromURIFunc allows overriding the swagger file/URL reading func LoadSwaggerFromURIFunc func(loader *SwaggerLoader, url *url.URL) (*Swagger, error) + // ReadFromURIFunc allows overriding the any file/URL reading func + ReadFromURIFunc func(loader *SwaggerLoader, url *url.URL) ([]byte, error) + Context context.Context visitedFiles map[string]struct{} @@ -68,7 +71,7 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL if f != nil { return f(swaggerLoader, location) } - data, err := readURL(location) + data, err := swaggerLoader.readURL(location) if err != nil { return nil, err } @@ -96,7 +99,7 @@ func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPat return fmt.Errorf("could not resolve path: %v", err) } - data, err := readURL(resolvedPath) + data, err := swaggerLoader.readURL(resolvedPath) if err != nil { return err } @@ -107,7 +110,12 @@ func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPat return nil } -func readURL(location *url.URL) ([]byte, error) { +func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { + f := swaggerLoader.ReadFromURIFunc + if f != nil { + return f(swaggerLoader, location) + } + if location.Scheme != "" && location.Host != "" { resp, err := http.Get(location.String()) if err != nil { @@ -143,7 +151,7 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*S x, err := f(swaggerLoader, pathAsURL) return x, err } - data, err := ioutil.ReadFile(path) + data, err := swaggerLoader.readURL(pathAsURL) if err != nil { return nil, err } diff --git a/openapi3/swagger_loader_read_from_uri_func_test.go b/openapi3/swagger_loader_read_from_uri_func_test.go new file mode 100644 index 000000000..3ac0ed74f --- /dev/null +++ b/openapi3/swagger_loader_read_from_uri_func_test.go @@ -0,0 +1,23 @@ +package openapi3 + +import ( + "io/ioutil" + "net/url" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoaderReadFromURIFunc(t *testing.T) { + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *SwaggerLoader, url *url.URL) ([]byte, error) { + return ioutil.ReadFile(filepath.Join("testdata", url.Path)) + } + doc, err := loader.LoadSwaggerFromFile("recursiveRef/openapi.yml") + 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["foo"].Value.Properties["bar"].Value.Items.Value.Example) +} From f598766c375cdcb0cbbfde6c9366936510b6e19c Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 3 Mar 2021 18:38:23 +0100 Subject: [PATCH 048/260] Drop `sl.LoadSwaggerFromURIFunc` (#317) --- README.md | 5 + openapi3/swagger_loader.go | 121 ++++++++---------- .../swagger_loader_read_from_uri_func_test.go | 50 ++++++++ openapi3/swagger_loader_test.go | 64 --------- 4 files changed, 108 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 979c08164..c1761a6ab 100644 --- a/README.md +++ b/README.md @@ -197,3 +197,8 @@ func arrayUniqueItemsChecker(items []interface{}) bool { // Check the uniqueness of the input slice(array in JSON) } ``` + +## Sub-v0 breaking API changes + +### v0.47.0 +Field `(*openapi3.SwaggerLoader).LoadSwaggerFromURIFunc` of type `func(*openapi3.SwaggerLoader, *url.URL) (*openapi3.Swagger, error)` was removed after the addition of the field `(*openapi3.SwaggerLoader).ReadFromURIFunc` of type `func(*openapi3.SwaggerLoader, *url.URL) ([]byte, error)`. diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index db0eb44be..3bb902dff 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -29,16 +29,14 @@ type SwaggerLoader struct { // IsExternalRefsAllowed enables visiting other files IsExternalRefsAllowed bool - // LoadSwaggerFromURIFunc allows overriding the swagger file/URL reading func - LoadSwaggerFromURIFunc func(loader *SwaggerLoader, url *url.URL) (*Swagger, error) - // ReadFromURIFunc allows overriding the any file/URL reading func ReadFromURIFunc func(loader *SwaggerLoader, url *url.URL) ([]byte, error) Context context.Context - visitedFiles map[string]struct{} - visitedSwaggers map[string]*Swagger + visitedPathItemRefs map[string]struct{} + + visitedDocuments map[string]*Swagger visitedExample map[*Example]struct{} visitedHeader map[*Header]struct{} @@ -55,22 +53,17 @@ func NewSwaggerLoader() *SwaggerLoader { return &SwaggerLoader{} } -func (swaggerLoader *SwaggerLoader) reset() { - swaggerLoader.visitedFiles = make(map[string]struct{}) - swaggerLoader.visitedSwaggers = make(map[string]*Swagger) +func (swaggerLoader *SwaggerLoader) resetVisitedPathItemRefs() { + swaggerLoader.visitedPathItemRefs = make(map[string]struct{}) } // LoadSwaggerFromURI loads a spec from a remote URL func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swagger, error) { - swaggerLoader.reset() + swaggerLoader.resetVisitedPathItemRefs() return swaggerLoader.loadSwaggerFromURIInternal(location) } func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL) (*Swagger, error) { - f := swaggerLoader.LoadSwaggerFromURIFunc - if f != nil { - return f(swaggerLoader, location) - } data, err := swaggerLoader.readURL(location) if err != nil { return nil, err @@ -111,8 +104,7 @@ func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPat } func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { - f := swaggerLoader.ReadFromURIFunc - if f != nil { + if f := swaggerLoader.ReadFromURIFunc; f != nil { return f(swaggerLoader, location) } @@ -121,36 +113,23 @@ func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { if err != nil { return nil, err } - data, err := ioutil.ReadAll(resp.Body) defer resp.Body.Close() - if err != nil { - return nil, err - } - return data, nil + return ioutil.ReadAll(resp.Body) } if location.Scheme != "" || location.Host != "" || location.RawQuery != "" { return nil, fmt.Errorf("unsupported URI: %q", location.String()) } - data, err := ioutil.ReadFile(location.Path) - if err != nil { - return nil, err - } - return data, nil + return ioutil.ReadFile(location.Path) } // LoadSwaggerFromFile loads a spec from a local file path func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) { - swaggerLoader.reset() + swaggerLoader.resetVisitedPathItemRefs() return swaggerLoader.loadSwaggerFromFileInternal(path) } func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*Swagger, error) { - f := swaggerLoader.LoadSwaggerFromURIFunc pathAsURL := &url.URL{Path: path} - if f != nil { - x, err := f(swaggerLoader, pathAsURL) - return x, err - } data, err := swaggerLoader.readURL(pathAsURL) if err != nil { return nil, err @@ -160,33 +139,39 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*S // LoadSwaggerFromData loads a spec from a byte array func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, error) { - swaggerLoader.reset() + swaggerLoader.resetVisitedPathItemRefs() return swaggerLoader.loadSwaggerFromDataInternal(data) } func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataInternal(data []byte) (*Swagger, error) { - swagger := &Swagger{} - if err := yaml.Unmarshal(data, swagger); err != nil { + doc := &Swagger{} + if err := yaml.Unmarshal(data, doc); err != nil { + return nil, err + } + if err := swaggerLoader.ResolveRefsIn(doc, nil); err != nil { return nil, err } - return swagger, swaggerLoader.ResolveRefsIn(swagger, nil) + return doc, nil } // LoadSwaggerFromDataWithPath takes the OpenApi spec data in bytes and a path where the resolver can find referred // elements and returns a *Swagger with all resolved data or an error if unable to load data or resolve refs. func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, path *url.URL) (*Swagger, error) { - swaggerLoader.reset() + swaggerLoader.resetVisitedPathItemRefs() return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, path) } func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []byte, path *url.URL) (*Swagger, error) { - visited, ok := swaggerLoader.visitedSwaggers[path.String()] - if ok { - return visited, nil + if swaggerLoader.visitedDocuments == nil { + swaggerLoader.visitedDocuments = make(map[string]*Swagger) + } + uri := path.String() + if doc, ok := swaggerLoader.visitedDocuments[uri]; ok { + return doc, nil } swagger := &Swagger{} - swaggerLoader.visitedSwaggers[path.String()] = swagger + swaggerLoader.visitedDocuments[uri] = swagger if err := yaml.Unmarshal(data, swagger); err != nil { return nil, err @@ -200,32 +185,8 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []b // ResolveRefsIn expands references if for instance spec was just unmarshalled func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.URL) (err error) { - if swaggerLoader.visitedExample == nil { - swaggerLoader.visitedExample = make(map[*Example]struct{}) - } - if swaggerLoader.visitedHeader == nil { - swaggerLoader.visitedHeader = make(map[*Header]struct{}) - } - if swaggerLoader.visitedLink == nil { - swaggerLoader.visitedLink = make(map[*Link]struct{}) - } - if swaggerLoader.visitedParameter == nil { - swaggerLoader.visitedParameter = make(map[*Parameter]struct{}) - } - if swaggerLoader.visitedRequestBody == nil { - swaggerLoader.visitedRequestBody = make(map[*RequestBody]struct{}) - } - if swaggerLoader.visitedResponse == nil { - swaggerLoader.visitedResponse = make(map[*Response]struct{}) - } - if swaggerLoader.visitedSchema == nil { - swaggerLoader.visitedSchema = make(map[*Schema]struct{}) - } - if swaggerLoader.visitedSecurityScheme == nil { - swaggerLoader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) - } - if swaggerLoader.visitedFiles == nil { - swaggerLoader.reset() + if swaggerLoader.visitedPathItemRefs == nil { + swaggerLoader.resetVisitedPathItemRefs() } // Visit all components @@ -456,6 +417,9 @@ func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref stri func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, documentPath *url.URL) error { if component != nil && component.Value != nil { + if swaggerLoader.visitedHeader == nil { + swaggerLoader.visitedHeader = make(map[*Header]struct{}) + } if _, ok := swaggerLoader.visitedHeader[component.Value]; ok { return nil } @@ -500,6 +464,9 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) error { if component != nil && component.Value != nil { + if swaggerLoader.visitedParameter == nil { + swaggerLoader.visitedParameter = make(map[*Parameter]struct{}) + } if _, ok := swaggerLoader.visitedParameter[component.Value]; ok { return nil } @@ -560,6 +527,9 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, documentPath *url.URL) error { if component != nil && component.Value != nil { + if swaggerLoader.visitedRequestBody == nil { + swaggerLoader.visitedRequestBody = make(map[*RequestBody]struct{}) + } if _, ok := swaggerLoader.visitedRequestBody[component.Value]; ok { return nil } @@ -612,6 +582,9 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) error { if component != nil && component.Value != nil { + if swaggerLoader.visitedResponse == nil { + swaggerLoader.visitedResponse = make(map[*Response]struct{}) + } if _, ok := swaggerLoader.visitedResponse[component.Value]; ok { return nil } @@ -683,6 +656,9 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) error { if component != nil && component.Value != nil { + if swaggerLoader.visitedSchema == nil { + swaggerLoader.visitedSchema = make(map[*Schema]struct{}) + } if _, ok := swaggerLoader.visitedSchema[component.Value]; ok { return nil } @@ -766,6 +742,9 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, documentPath *url.URL) error { if component != nil && component.Value != nil { + if swaggerLoader.visitedSecurityScheme == nil { + swaggerLoader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) + } if _, ok := swaggerLoader.visitedSecurityScheme[component.Value]; ok { return nil } @@ -801,6 +780,9 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, documentPath *url.URL) error { if component != nil && component.Value != nil { + if swaggerLoader.visitedExample == nil { + swaggerLoader.visitedExample = make(map[*Example]struct{}) + } if _, ok := swaggerLoader.visitedExample[component.Value]; ok { return nil } @@ -836,6 +818,9 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) error { if component != nil && component.Value != nil { + if swaggerLoader.visitedLink == nil { + swaggerLoader.visitedLink = make(map[*Link]struct{}) + } if _, ok := swaggerLoader.visitedLink[component.Value]; ok { return nil } @@ -875,10 +860,10 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo key = documentPath.EscapedPath() } key += entrypoint - if _, ok := swaggerLoader.visitedFiles[key]; ok { + if _, ok := swaggerLoader.visitedPathItemRefs[key]; ok { return nil } - swaggerLoader.visitedFiles[key] = struct{}{} + swaggerLoader.visitedPathItemRefs[key] = struct{}{} const prefix = "#/paths/" if pathItem == nil { diff --git a/openapi3/swagger_loader_read_from_uri_func_test.go b/openapi3/swagger_loader_read_from_uri_func_test.go index 3ac0ed74f..b15767855 100644 --- a/openapi3/swagger_loader_read_from_uri_func_test.go +++ b/openapi3/swagger_loader_read_from_uri_func_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "fmt" "io/ioutil" "net/url" "path/filepath" @@ -21,3 +22,52 @@ func TestLoaderReadFromURIFunc(t *testing.T) { 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["foo"].Value.Properties["bar"].Value.Items.Value.Example) } + +type multipleSourceSwaggerLoaderExample struct { + Sources map[string][]byte +} + +func (l *multipleSourceSwaggerLoaderExample) LoadSwaggerFromURI( + loader *SwaggerLoader, + location *url.URL, +) ([]byte, error) { + source := l.resolveSourceFromURI(location) + if source == nil { + return nil, fmt.Errorf("Unsupported URI: %q", location.String()) + } + return source, nil +} + +func (l *multipleSourceSwaggerLoaderExample) resolveSourceFromURI(location fmt.Stringer) []byte { + return l.Sources[location.String()] +} + +func TestResolveSchemaExternalRef(t *testing.T) { + rootLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "spec.json"} + externalLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "external.json"} + rootSpec := []byte(fmt.Sprintf( + `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Root":{"allOf":[{"$ref":"%s#/components/schemas/External"}]}}}}`, + externalLocation.String(), + )) + externalSpec := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"External Spec"},"paths":{},"components":{"schemas":{"External":{"type":"string"}}}}`) + multipleSourceLoader := &multipleSourceSwaggerLoaderExample{ + Sources: map[string][]byte{ + rootLocation.String(): rootSpec, + externalLocation.String(): externalSpec, + }, + } + loader := &SwaggerLoader{ + IsExternalRefsAllowed: true, + ReadFromURIFunc: multipleSourceLoader.LoadSwaggerFromURI, + } + + doc, err := loader.LoadSwaggerFromURI(rootLocation) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + refRootVisited := doc.Components.Schemas["Root"].Value.AllOf[0] + require.Equal(t, fmt.Sprintf("%s#/components/schemas/External", externalLocation.String()), refRootVisited.Ref) + require.NotNil(t, refRootVisited.Value) +} diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go index 578932039..898ae9fe4 100644 --- a/openapi3/swagger_loader_test.go +++ b/openapi3/swagger_loader_test.go @@ -132,70 +132,6 @@ paths: require.Equal(t, example.Value.Value.(map[string]interface{})["error"].(bool), false) } -type sourceExample struct { - Location *url.URL - Spec []byte -} - -type multipleSourceSwaggerLoaderExample struct { - Sources []*sourceExample -} - -func (l *multipleSourceSwaggerLoaderExample) LoadSwaggerFromURI( - loader *SwaggerLoader, - location *url.URL, -) (*Swagger, error) { - source := l.resolveSourceFromURI(location) - if source == nil { - return nil, fmt.Errorf("Unsupported URI: '%s'", location.String()) - } - return loader.LoadSwaggerFromData(source.Spec) -} - -func (l *multipleSourceSwaggerLoaderExample) resolveSourceFromURI(location fmt.Stringer) *sourceExample { - locationString := location.String() - for _, v := range l.Sources { - if v.Location.String() == locationString { - return v - } - } - return nil -} - -func TestResolveSchemaExternalRef(t *testing.T) { - rootLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "spec.json"} - externalLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "external.json"} - rootSpec := []byte(fmt.Sprintf( - `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Root":{"allOf":[{"$ref":"%s#/components/schemas/External"}]}}}}`, - externalLocation.String(), - )) - externalSpec := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"External Spec"},"paths":{},"components":{"schemas":{"External":{"type":"string"}}}}`) - multipleSourceLoader := &multipleSourceSwaggerLoaderExample{ - Sources: []*sourceExample{ - { - Location: rootLocation, - Spec: rootSpec, - }, - { - Location: externalLocation, - Spec: externalSpec, - }, - }, - } - loader := &SwaggerLoader{ - IsExternalRefsAllowed: true, - LoadSwaggerFromURIFunc: multipleSourceLoader.LoadSwaggerFromURI, - } - doc, err := loader.LoadSwaggerFromURI(rootLocation) - require.NoError(t, err) - err = doc.Validate(loader.Context) - - require.NoError(t, err) - refRootVisited := doc.Components.Schemas["Root"].Value.AllOf[0] - require.Equal(t, fmt.Sprintf("%s#/components/schemas/External", externalLocation.String()), refRootVisited.Ref) - require.NotNil(t, refRootVisited.Value) -} - func TestLoadErrorOnRefMisuse(t *testing.T) { spec := []byte(` openapi: '3.0.0' From 96aeb23612d5a53d3ca9a8f3a31783629d6864a1 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 4 Mar 2021 09:48:24 +0100 Subject: [PATCH 049/260] Add an openapi3gen example + options (#320) --- jsoninfo/field_info.go | 14 ++-- openapi3gen/openapi3gen.go | 114 +++++++++++++++++++++++++------- openapi3gen/openapi3gen_test.go | 104 +++++------------------------ openapi3gen/simple_test.go | 109 ++++++++++++++++++++++++++++++ 4 files changed, 221 insertions(+), 120 deletions(-) create mode 100644 openapi3gen/simple_test.go diff --git a/jsoninfo/field_info.go b/jsoninfo/field_info.go index d949a79d3..d2ad505bd 100644 --- a/jsoninfo/field_info.go +++ b/jsoninfo/field_info.go @@ -63,7 +63,7 @@ iteration: // Read our custom "multijson" tag that // allows multiple fields with the same name. - if v := f.Tag.Get("multijson"); len(v) > 0 { + if v := f.Tag.Get("multijson"); v != "" { field.MultipleFields = true jsonTag = v } @@ -74,11 +74,11 @@ iteration: } // Parse the tag - if len(jsonTag) > 0 { + if jsonTag != "" { field.HasJSONTag = true for i, part := range strings.Split(jsonTag, ",") { if i == 0 { - if len(part) > 0 { + if part != "" { field.JSONName = part } } else { @@ -92,12 +92,8 @@ iteration: } } - if _, ok := field.Type.MethodByName("MarshalJSON"); ok { - field.TypeIsMarshaller = true - } - if _, ok := field.Type.MethodByName("UnmarshalJSON"); ok { - field.TypeIsUnmarshaller = true - } + _, field.TypeIsMarshaller = field.Type.MethodByName("MarshalJSON") + _, field.TypeIsUnmarshaller = field.Type.MethodByName("UnmarshalJSON") // Field is done fields = append(fields, field) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 4a80405ba..4cf022e52 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -1,8 +1,9 @@ -// Package openapi3gen generates OpenAPI 3 schemas for Go types. +// Package openapi3gen generates OpenAPIv3 JSON schemas from Go types. package openapi3gen import ( "encoding/json" + "math" "reflect" "strings" "time" @@ -18,8 +19,22 @@ func (err *CycleError) Error() string { return "Detected JSON cycle" } -func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) { - g := NewGenerator() +// Option allows tweaking SchemaRef generation +type Option func(*generatorOpt) + +type generatorOpt struct { + useAllExportedFields bool +} + +// UseAllExportedFields changes the default behavior of only +// generating schemas for struct fields with a JSON tag. +func UseAllExportedFields() Option { + return func(x *generatorOpt) { x.useAllExportedFields = true } +} + +// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef. +func NewSchemaRefForValue(value interface{}, opts ...Option) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) { + g := NewGenerator(opts...) ref, err := g.GenerateSchemaRef(reflect.TypeOf(value)) for ref := range g.SchemaRefs { ref.Ref = "" @@ -28,6 +43,8 @@ func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3 } type Generator struct { + opts generatorOpt + Types map[reflect.Type]*openapi3.SchemaRef // SchemaRefs contains all references and their counts. @@ -36,20 +53,25 @@ type Generator struct { SchemaRefs map[*openapi3.SchemaRef]int } -func NewGenerator() *Generator { +func NewGenerator(opts ...Option) *Generator { + gOpt := &generatorOpt{} + for _, f := range opts { + f(gOpt) + } return &Generator{ Types: make(map[reflect.Type]*openapi3.SchemaRef), SchemaRefs: make(map[*openapi3.SchemaRef]int), + opts: *gOpt, } } func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) { + //check generatorOpt consistency here return g.generateSchemaRefFor(nil, t) } func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) { - ref := g.Types[t] - if ref != nil { + if ref := g.Types[t]; ref != nil { g.SchemaRefs[ref]++ return ref, nil } @@ -62,7 +84,6 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect } func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) { - // Get TypeInfo typeInfo := jsoninfo.GetTypeInfo(t) for _, parent := range parents { if parent == typeInfo { @@ -70,19 +91,15 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } } - // Doesn't exist. - // Create the schema. if cap(parents) == 0 { parents = make([]*jsoninfo.TypeInfo, 0, 4) } parents = append(parents, typeInfo) - // Ignore pointers for t.Kind() == reflect.Ptr { t = t.Elem() } - // Create instance if strings.HasSuffix(t.Name(), "Ref") { _, a := t.FieldByName("Ref") v, b := t.FieldByName("Value") @@ -104,23 +121,54 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } } - // Allocate schema schema := &openapi3.Schema{} switch t.Kind() { case reflect.Func, reflect.Chan: - return nil, nil + return nil, nil // ignore + case reflect.Bool: schema.Type = "boolean" - case reflect.Int, - reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + case reflect.Int: + schema.Type = "integer" + case reflect.Int8: + schema.Type = "integer" + schema.Min = &minInt8 + schema.Max = &maxInt8 + case reflect.Int16: + schema.Type = "integer" + schema.Min = &minInt16 + schema.Max = &maxInt16 + case reflect.Int32: + schema.Type = "integer" + schema.Format = "int32" + case reflect.Int64: schema.Type = "integer" schema.Format = "int64" + case reflect.Uint8: + schema.Type = "integer" + schema.Min = &zeroInt + schema.Max = &maxUint8 + case reflect.Uint16: + schema.Type = "integer" + schema.Min = &zeroInt + schema.Max = &maxUint16 + case reflect.Uint32: + schema.Type = "integer" + schema.Min = &zeroInt + schema.Max = &maxUint32 + case reflect.Uint64: + schema.Type = "integer" + schema.Min = &zeroInt + schema.Max = &maxUint64 - case reflect.Float32, reflect.Float64: + case reflect.Float32: schema.Type = "number" + schema.Format = "float" + case reflect.Float64: + schema.Type = "number" + schema.Format = "double" case reflect.String: schema.Type = "string" @@ -128,9 +176,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec case reflect.Slice: if t.Elem().Kind() == reflect.Uint8 { if t == rawMessageType { - return &openapi3.SchemaRef{ - Value: schema, - }, nil + return &openapi3.SchemaRef{Value: schema}, nil } schema.Type = "string" schema.Format = "byte" @@ -163,17 +209,26 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Format = "date-time" } else { for _, fieldInfo := range typeInfo.Fields { - // Only fields with JSON tag are considered - if !fieldInfo.HasJSONTag { + // Only fields with JSON tag are considered (by default) + if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields { continue } - ref, err := g.generateSchemaRefFor(parents, fieldInfo.Type) + // If asked, try to use yaml tag + name, fType := fieldInfo.JSONName, fieldInfo.Type + if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields { + ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) + if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { + name, fType = tag, ff.Type + } + } + + ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { return nil, err } if ref != nil { g.SchemaRefs[ref]++ - schema.WithPropertyRef(fieldInfo.JSONName, ref) + schema.WithPropertyRef(name, ref) } } @@ -183,6 +238,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } } } + return openapi3.NewSchemaRef(t.Name(), schema), nil } @@ -192,4 +248,14 @@ var RefSchemaRef = openapi3.NewSchemaRef("Ref", var ( timeType = reflect.TypeOf(time.Time{}) rawMessageType = reflect.TypeOf(json.RawMessage{}) + + zeroInt = float64(0) + maxInt8 = float64(math.MaxInt8) + minInt8 = float64(math.MinInt8) + maxInt16 = float64(math.MaxInt16) + minInt16 = float64(math.MinInt16) + maxUint8 = float64(math.MaxUint8) + maxUint16 = float64(math.MaxUint16) + maxUint32 = float64(math.MaxUint32) + maxUint64 = float64(math.MaxUint64) ) diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 2a58433cb..1422017a5 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -1,10 +1,9 @@ package openapi3gen import ( - "encoding/json" "testing" - "time" + "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -16,96 +15,27 @@ type CyclicType1 struct { } func TestCyclic(t *testing.T) { - schema, refsMap, err := NewSchemaRefForValue(&CyclicType0{}) + schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{}) require.IsType(t, &CycleError{}, err) - require.Nil(t, schema) + require.Nil(t, schemaRef) require.Empty(t, refsMap) } -func TestSimple(t *testing.T) { - type ExampleChild string - type Example struct { - Bool bool `json:"bool"` - Int int `json:"int"` - Int64 int64 `json:"int64"` - Float64 float64 `json:"float64"` - String string `json:"string"` - Bytes []byte `json:"bytes"` - JSON json.RawMessage `json:"json"` - Time time.Time `json:"time"` - Slice []*ExampleChild `json:"slice"` - Map map[string]*ExampleChild `json:"map"` - Struct struct { - X string `json:"x"` - } `json:"struct"` - EmptyStruct struct { - X string - } `json:"structWithoutFields"` - Ptr *ExampleChild `json:"ptr"` +func TestExportedNonTagged(t *testing.T) { + type Bla struct { + A string + Another string `json:"another"` + yetAnother string + EvenAYaml string `yaml:"even_a_yaml"` } - schema, refsMap, err := NewSchemaRefForValue(&Example{}) + schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields()) require.NoError(t, err) - require.Len(t, refsMap, 14) - data, err := json.Marshal(schema) - require.NoError(t, err) - require.JSONEq(t, expectedSimple, string(data)) -} - -const expectedSimple = ` -{ - "type": "object", - "properties": { - "bool": { - "type": "boolean" - }, - "int": { - "type": "integer", - "format": "int64" - }, - "int64": { - "type": "integer", - "format": "int64" - }, - "float64": { - "type": "number" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "string": { - "type": "string" - }, - "bytes": { - "type": "string", - "format": "byte" - }, - "json": {}, - "slice": { - "type": "array", - "items": { - "type": "string" - } - }, - "map": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "struct": { - "type": "object", - "properties": { - "x": { - "type": "string" - } - } - }, - "structWithoutFields": {}, - "ptr": { - "type": "string" - } - } + require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ + Type: "object", + Properties: map[string]*openapi3.SchemaRef{ + "A": {Value: &openapi3.Schema{Type: "string"}}, + "another": {Value: &openapi3.Schema{Type: "string"}}, + "even_a_yaml": {Value: &openapi3.Schema{Type: "string"}}, + }}}, schemaRef) } -` diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go new file mode 100644 index 000000000..d997e23b2 --- /dev/null +++ b/openapi3gen/simple_test.go @@ -0,0 +1,109 @@ +package openapi3gen_test + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/getkin/kin-openapi/openapi3gen" +) + +type ( + SomeStruct struct { + Bool bool `json:"bool"` + Int int `json:"int"` + Int64 int64 `json:"int64"` + Float64 float64 `json:"float64"` + String string `json:"string"` + Bytes []byte `json:"bytes"` + JSON json.RawMessage `json:"json"` + Time time.Time `json:"time"` + Slice []SomeOtherType `json:"slice"` + Map map[string]*SomeOtherType `json:"map"` + + Struct struct { + X string `json:"x"` + } `json:"struct"` + + EmptyStruct struct { + Y string + } `json:"structWithoutFields"` + + Ptr *SomeOtherType `json:"ptr"` + } + + SomeOtherType string +) + +func Example() { + schemaRef, refsMap, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}) + if err != nil { + panic(err) + } + + if len(refsMap) != 15 { + panic(fmt.Sprintf("unintended len(refsMap) = %d", len(refsMap))) + } + + data, err := json.MarshalIndent(schemaRef, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("%s\n", data) + // Output: + // { + // "properties": { + // "bool": { + // "type": "boolean" + // }, + // "bytes": { + // "format": "byte", + // "type": "string" + // }, + // "float64": { + // "format": "double", + // "type": "number" + // }, + // "int": { + // "type": "integer" + // }, + // "int64": { + // "format": "int64", + // "type": "integer" + // }, + // "json": {}, + // "map": { + // "additionalProperties": { + // "type": "string" + // }, + // "type": "object" + // }, + // "ptr": { + // "type": "string" + // }, + // "slice": { + // "items": { + // "type": "string" + // }, + // "type": "array" + // }, + // "string": { + // "type": "string" + // }, + // "struct": { + // "properties": { + // "x": { + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "structWithoutFields": {}, + // "time": { + // "format": "date-time", + // "type": "string" + // } + // }, + // "type": "object" + // } +} From 1286d066039886ab13d9fca0947db4d1d5994343 Mon Sep 17 00:00:00 2001 From: Riccardo Manfrin Date: Mon, 8 Mar 2021 20:59:49 +0100 Subject: [PATCH 050/260] Adds oneOf/discriminator/mapping management (#321) --- openapi3/schema.go | 14 ++- .../validation_discriminator_test.go | 98 +++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 openapi3filter/validation_discriminator_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 4968feca0..bed51c159 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -847,7 +847,19 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val err := v.visitJSON(settings, value) settings.failfast = oldfailfast if err == nil { - ok++ + if schema.Discriminator != nil { + pn := schema.Discriminator.PropertyName + if valuemap, okcheck := value.(map[string]interface{}); okcheck { + if discriminatorVal, okcheck := valuemap[pn]; okcheck == true { + mapref, okcheck := schema.Discriminator.Mapping[discriminatorVal.(string)] + if okcheck && mapref == item.Ref { + ok++ + } + } + } + } else { + ok++ + } } } if ok != 1 { diff --git a/openapi3filter/validation_discriminator_test.go b/openapi3filter/validation_discriminator_test.go new file mode 100644 index 000000000..51d804f79 --- /dev/null +++ b/openapi3filter/validation_discriminator_test.go @@ -0,0 +1,98 @@ +package openapi3filter + +import ( + "bytes" + "context" + "net/http" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +var yaJsonSpecWithDiscriminator = []byte(` +openapi: 3.0.0 +info: + version: 0.2.0 + title: yaAPI + +paths: + + /blob: + put: + operationId: SetObj + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/blob' + responses: + '200': + description: Ok + +components: + schemas: + blob: + oneOf: + - $ref: '#/components/schemas/objA' + - $ref: '#/components/schemas/objB' + discriminator: + propertyName: discr + mapping: + objA: '#/components/schemas/objA' + objB: '#/components/schemas/objB' + genericObj: + type: object + required: + - discr + properties: + discr: + type: string + enum: + - objA + - objB + discriminator: + propertyName: discr + mapping: + objA: '#/components/schemas/objA' + objB: '#/components/schemas/objB' + objA: + allOf: + - $ref: '#/components/schemas/genericObj' + - type: object + properties: + base64: + type: string + + objB: + allOf: + - $ref: '#/components/schemas/genericObj' + - type: object + properties: + value: + type: integer +`) + +func forgeRequest(body string) *http.Request { + iobody := bytes.NewReader([]byte(body)) + req, _ := http.NewRequest("PUT", "/blob", iobody) + req.Header.Add("Content-Type", "application/json") + return req +} + +func TestValidationWithDiscriminatorSelection(t *testing.T) { + openapi, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(yaJsonSpecWithDiscriminator) + require.NoError(t, err) + router := NewRouter().WithSwagger(openapi) + req := forgeRequest(`{"discr": "objA", "base64": "S25vY2sgS25vY2ssIE5lbyAuLi4="}`) + route, pathParams, _ := router.FindRoute(req.Method, req.URL) + requestValidationInput := &RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + } + ctx := context.Background() + err = ValidateRequest(ctx, requestValidationInput) + require.NoError(t, err) +} From 0ef6b183952b2f6d489901057c819ba400f3c0a1 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 10 Mar 2021 16:42:41 +0100 Subject: [PATCH 051/260] reproduce incorrect discriminator handling with ValidateRequest (#323) --- openapi3filter/validate_request_test.go | 108 ++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 openapi3filter/validate_request_test.go diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go new file mode 100644 index 000000000..ba320cab3 --- /dev/null +++ b/openapi3filter/validate_request_test.go @@ -0,0 +1,108 @@ +package openapi3filter_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" +) + +const spec = ` +openapi: 3.0.0 +info: + title: My API + version: 0.0.1 +paths: + /: + post: + responses: + default: + description: '' + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + discriminator: + propertyName: pet_type + +components: + schemas: + Pet: + type: object + required: [pet_type] + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + breed: + type: string + enum: [Dingo, Husky, Retriever, Shepherd] + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + hunts: + type: boolean + age: + type: integer +` + +func Example() { + loader := openapi3.NewSwaggerLoader() + doc, err := loader.LoadSwaggerFromData([]byte(spec)) + if err != nil { + panic(err) + } + if err := doc.Validate(loader.Context); err != nil { + panic(err) + } + + router := openapi3filter.NewRouter().WithSwagger(doc) + + p, err := json.Marshal(map[string]interface{}{ + "pet_type": "Cat", + "breed": "Dingo", + "bark": true, + }) + if err != nil { + panic(err) + } + + req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(p)) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/json") + + route, pathParams, err := router.FindRoute(req.Method, req.URL) + if err != nil { + panic(err) + } + + requestValidationInput := &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + } + if err = openapi3filter.ValidateRequest(loader.Context, requestValidationInput); err == nil { + fmt.Println("Valid") + } else { + fmt.Println("NOT valid") + } + // Output: NOT valid +} From d28d2bbd4e2f54c237fd5f32b06d0a3bd5467d31 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 18 Mar 2021 18:09:34 +0100 Subject: [PATCH 052/260] prepare for #210 (#325) --- .github/workflows/go.yml | 55 ++++++-- README.md | 14 +- jsoninfo/marshal.go | 6 +- jsoninfo/unmarshal.go | 12 +- jsoninfo/unmarshal_test.go | 2 +- jsoninfo/unsupported_properties_error.go | 11 +- openapi2/openapi2.go | 4 +- openapi2conv/openapi2_conv.go | 4 +- openapi3/discriminator_test.go | 24 +++- openapi3/encoding.go | 2 +- openapi3/info.go | 6 +- openapi3/link.go | 2 +- openapi3/operation.go | 2 +- openapi3/operation_test.go | 2 +- openapi3/parameter.go | 2 +- openapi3/path_item.go | 4 +- openapi3/refs_test.go | 2 +- openapi3/schema.go | 48 +++---- openapi3/schema_formats.go | 2 +- openapi3/schema_test.go | 2 +- openapi3/security_scheme.go | 30 ++-- openapi3/server.go | 8 +- openapi3/server_test.go | 2 +- openapi3/swagger.go | 6 +- openapi3/swagger_loader.go | 21 ++- .../swagger_loader_read_from_uri_func_test.go | 2 +- openapi3/swagger_loader_relative_refs_test.go | 8 +- openapi3/swagger_test.go | 8 +- openapi3/unique_items_checker_test.go | 2 +- openapi3filter/authentication_input.go | 13 +- openapi3filter/errors.go | 31 ++--- openapi3filter/options.go | 10 +- openapi3filter/req_resp_decoder.go | 83 +++++------ openapi3filter/req_resp_decoder_test.go | 32 ++--- openapi3filter/router.go | 21 ++- openapi3filter/router_test.go | 131 ++++++++++-------- openapi3filter/validate_readonly_test.go | 8 +- openapi3filter/validate_request.go | 30 ++-- openapi3filter/validate_request_test.go | 30 +++- openapi3filter/validate_response.go | 12 +- .../validation_discriminator_test.go | 4 +- openapi3filter/validation_error_encoder.go | 116 +++++++--------- openapi3filter/validation_error_test.go | 102 +++++++------- openapi3filter/validation_test.go | 27 ++-- openapi3gen/openapi3gen.go | 4 +- pathpattern/node.go | 4 +- pathpattern/node_test.go | 4 +- 47 files changed, 491 insertions(+), 464 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 41b196f7d..d82af1e6b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,29 +14,62 @@ jobs: matrix: # Locked at https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on os: - - ubuntu-18.04 + - ubuntu-20.04 - windows-2019 + - macos-10.15 runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash steps: - - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: go-version: 1.x - - run: go version + + - id: go-cache-paths + run: | + echo "::set-output name=go-build::$(go env GOCACHE)" + echo "::set-output name=go-mod::$(go env GOMODCACHE)" + + - name: Go Build Cache + uses: actions/cache@v2 + with: + path: ${{ steps.go-cache-paths.outputs.go-build }} + key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} + + - name: Go Mod Cache + uses: actions/cache@v2 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} + + + - uses: actions/checkout@v2 - run: go mod download && go mod tidy && go mod verify - - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - shell: bash + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - run: go vet ./... - - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - shell: bash + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - run: go fmt ./... - - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - shell: bash + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - run: go test ./... - - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - shell: bash + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + + + - if: runner.os == 'Linux' + name: Errors must not be capitalized https://github.com/golang/go/wiki/CodeReviewComments#error-strings + run: | + ! git grep -E '(fmt|errors)[^(]+\(.[A-Z]' + + - if: runner.os == 'Linux' + name: Did you mean %q + run: | + ! git grep -E "'[%].'" diff --git a/README.md b/README.md index c1761a6ab..8d9484dcc 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Here's some projects that depend on _kin-openapi_: # Some recipes ## Loading OpenAPI document -Use `SwaggerLoader`, which resolves all JSON references: +Use `SwaggerLoader`, which resolves all references: ```go swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("swagger.json") ``` @@ -108,9 +108,7 @@ func main() { responseValidationInput := &openapi3filter.ResponseValidationInput{ RequestValidationInput: requestValidationInput, Status: respStatus, - Header: http.Header{ - "Content-Type": []string{respContentType}, - }, + Header: http.Header{"Content-Type": []string{respContentType}}, } if respBody != nil { data, _ := json.Marshal(respBody) @@ -160,7 +158,7 @@ func xmlBodyDecoder(body []byte) (interface{}, error) { } ``` -## Custom function for check uniqueness of JSON array +## Custom function to check uniqueness of array items By defaut, the library check unique items by below predefined function @@ -169,8 +167,6 @@ func isSliceOfUniqueItems(xs []interface{}) bool { s := len(xs) m := make(map[string]struct{}, s) for _, x := range xs { - // The input slice is coverted from a JSON string, there shall - // have no error when covert it back. key, _ := json.Marshal(&x) m[string(key)] = struct{}{} } @@ -180,7 +176,7 @@ func isSliceOfUniqueItems(xs []interface{}) bool { In the predefined function using `json.Marshal` to generate a string can be used as a map key which is to support check the uniqueness of an array -when the array items are JSON objects or JSON arraies. You can register +when the array items are objects or arrays. You can register you own function according to your input data to get better performance: ```go @@ -194,7 +190,7 @@ func main() { } func arrayUniqueItemsChecker(items []interface{}) bool { - // Check the uniqueness of the input slice(array in JSON) + // Check the uniqueness of the input slice } ``` diff --git a/jsoninfo/marshal.go b/jsoninfo/marshal.go index 93de99a56..2a98d68fb 100644 --- a/jsoninfo/marshal.go +++ b/jsoninfo/marshal.go @@ -59,11 +59,11 @@ func (encoder *ObjectEncoder) EncodeStructFieldsAndExtensions(value interface{}) // Follow "encoding/json" semantics if reflection.Kind() != reflect.Ptr { // Panic because this is a clear programming error - panic(fmt.Errorf("Value %s is not a pointer", reflection.Type().String())) + panic(fmt.Errorf("value %s is not a pointer", reflection.Type().String())) } if reflection.IsNil() { // Panic because this is a clear programming error - panic(fmt.Errorf("Value %s is nil", reflection.Type().String())) + panic(fmt.Errorf("value %s is nil", reflection.Type().String())) } // Take the element @@ -146,7 +146,7 @@ iteration: continue iteration } default: - panic(fmt.Errorf("Field '%s' has unsupported type %s", field.JSONName, field.Type.String())) + panic(fmt.Errorf("field %q has unsupported type %s", field.JSONName, field.Type.String())) } // No special treament is needed diff --git a/jsoninfo/unmarshal.go b/jsoninfo/unmarshal.go index 329718758..ce3c337a3 100644 --- a/jsoninfo/unmarshal.go +++ b/jsoninfo/unmarshal.go @@ -25,7 +25,7 @@ type ObjectDecoder struct { func NewObjectDecoder(data []byte) (*ObjectDecoder, error) { var remainingFields map[string]json.RawMessage if err := json.Unmarshal(data, &remainingFields); err != nil { - return nil, fmt.Errorf("Failed to unmarshal extension properties: %v\nInput: %s", err, data) + return nil, fmt.Errorf("failed to unmarshal extension properties: %v (%s)", err, data) } return &ObjectDecoder{ Data: data, @@ -41,10 +41,10 @@ func (decoder *ObjectDecoder) DecodeExtensionMap() map[string]json.RawMessage { func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) error { reflection := reflect.ValueOf(value) if reflection.Kind() != reflect.Ptr { - panic(fmt.Errorf("Value %T is not a pointer", value)) + panic(fmt.Errorf("value %T is not a pointer", value)) } if reflection.IsNil() { - panic(fmt.Errorf("Value %T is nil", value)) + panic(fmt.Errorf("value %T is nil", value)) } reflection = reflection.Elem() for (reflection.Kind() == reflect.Interface || reflection.Kind() == reflect.Ptr) && !reflection.IsNil() { @@ -52,7 +52,7 @@ func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) } reflectionType := reflection.Type() if reflectionType.Kind() != reflect.Struct { - panic(fmt.Errorf("Value %T is not a struct", value)) + panic(fmt.Errorf("value %T is not a struct", value)) } typeInfo := GetTypeInfo(reflectionType) @@ -87,7 +87,7 @@ func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) continue } } - return fmt.Errorf("Error while unmarshalling property '%s' (%s): %v", + return fmt.Errorf("failed to unmarshal property %q (%s): %v", field.JSONName, fieldValue.Type().String(), err) } if !isPtr { @@ -109,7 +109,7 @@ func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) continue } } - return fmt.Errorf("Error while unmarshalling property '%s' (%s): %v", + return fmt.Errorf("failed to unmarshal property %q (%s): %v", field.JSONName, fieldPtr.Type().String(), err) } diff --git a/jsoninfo/unmarshal_test.go b/jsoninfo/unmarshal_test.go index c3dd957ac..dd25f04b6 100644 --- a/jsoninfo/unmarshal_test.go +++ b/jsoninfo/unmarshal_test.go @@ -149,7 +149,7 @@ func TestDecodeStructFieldsAndExtensions(t *testing.T) { }{} err = d.DecodeStructFieldsAndExtensions(&value) require.Error(t, err) - require.EqualError(t, err, "Error while unmarshalling property 'field1' (*int): json: cannot unmarshal string into Go value of type int") + require.EqualError(t, err, `failed to unmarshal property "field1" (*int): json: cannot unmarshal string into Go value of type int`) require.Equal(t, 0, value.Field1) require.Equal(t, 2, len(d.DecodeExtensionMap())) }) diff --git a/jsoninfo/unsupported_properties_error.go b/jsoninfo/unsupported_properties_error.go index 258efef28..f69aafdc3 100644 --- a/jsoninfo/unsupported_properties_error.go +++ b/jsoninfo/unsupported_properties_error.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "sort" - "strings" ) // UnsupportedPropertiesError is a helper for extensions that want to refuse @@ -27,7 +26,7 @@ func (err *UnsupportedPropertiesError) Error() string { m := err.UnsupportedProperties typeInfo := GetTypeInfoForValue(err.Value) if m == nil || typeInfo == nil { - return "Invalid UnsupportedPropertiesError" + return fmt.Sprintf("invalid %T", *err) } keys := make([]string, 0, len(m)) for k := range m { @@ -36,10 +35,8 @@ func (err *UnsupportedPropertiesError) Error() string { sort.Strings(keys) supported := typeInfo.FieldNames() if len(supported) == 0 { - return fmt.Sprintf("Type '%T' doesn't take any properties. Unsupported properties: '%s'\n", - err.Value, strings.Join(keys, "', '")) + return fmt.Sprintf("type \"%T\" doesn't take any properties. Unsupported properties: %+v", + err.Value, keys) } - return fmt.Sprintf("Unsupported properties: '%s'\nSupported properties are: '%s'", - strings.Join(keys, "', '"), - strings.Join(supported, "', '")) + return fmt.Sprintf("unsupported properties: %+v (supported properties are: %+v)", keys, supported) } diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 2a3006cfd..8797c573a 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -120,7 +120,7 @@ func (pathItem *PathItem) GetOperation(method string) *Operation { case http.MethodPut: return pathItem.Put default: - panic(fmt.Errorf("Unsupported HTTP method '%s'", method)) + panic(fmt.Errorf("unsupported HTTP method %q", method)) } } @@ -141,7 +141,7 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { case http.MethodPut: pathItem.Put = operation default: - panic(fmt.Errorf("Unsupported HTTP method '%s'", method)) + panic(fmt.Errorf("unsupported HTTP method %q", method)) } } diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index f3f8c5c34..2622f76dc 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -519,7 +519,7 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu case "password": flows.Password = flow default: - return nil, fmt.Errorf("Unsupported flow '%s'", securityScheme.Flow) + return nil, fmt.Errorf("unsupported flow %q", securityScheme.Flow) } } return &openapi3.SecuritySchemeRef{ @@ -1077,7 +1077,7 @@ func FromV3SecurityScheme(swagger *openapi3.Swagger, ref *openapi3.SecuritySchem } } default: - return nil, fmt.Errorf("Unsupported security scheme type '%s'", securityScheme.Type) + return nil, fmt.Errorf("unsupported security scheme type %q", securityScheme.Type) } return result, nil } diff --git a/openapi3/discriminator_test.go b/openapi3/discriminator_test.go index e3ed22c27..c85548122 100644 --- a/openapi3/discriminator_test.go +++ b/openapi3/discriminator_test.go @@ -6,9 +6,19 @@ import ( "github.com/stretchr/testify/require" ) -var jsonSpecWithDiscriminator = []byte(` +func TestParsingDiscriminator(t *testing.T) { + const spec = ` { "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "title", + "description": "desc", + "contact": { + "email": "email" + } + }, + "paths": {}, "components": { "schemas": { "MyResponseType": { @@ -33,10 +43,14 @@ var jsonSpecWithDiscriminator = []byte(` } } } -`) +` -func TestParsingDiscriminator(t *testing.T) { - loader, err := NewSwaggerLoader().LoadSwaggerFromData(jsonSpecWithDiscriminator) + loader := NewSwaggerLoader() + doc, err := loader.LoadSwaggerFromData([]byte(spec)) require.NoError(t, err) - require.Equal(t, 2, len(loader.Components.Schemas["MyResponseType"].Value.Discriminator.Mapping)) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + require.Equal(t, 2, len(doc.Components.Schemas["MyResponseType"].Value.Discriminator.Mapping)) } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 16b7a2694..4bf657f70 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -86,7 +86,7 @@ func (encoding *Encoding) Validate(c context.Context) error { sm.Style == SerializationDeepObject && sm.Explode: // it is a valid default: - return fmt.Errorf("Serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode) + return fmt.Errorf("serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode) } return nil diff --git a/openapi3/info.go b/openapi3/info.go index 25e675e66..386ae861c 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -40,11 +40,11 @@ func (value *Info) Validate(c context.Context) error { } if value.Version == "" { - return errors.New("value of version must be a non-empty JSON string") + return errors.New("value of version must be a non-empty string") } if value.Title == "" { - return errors.New("value of title must be a non-empty JSON string") + return errors.New("value of title must be a non-empty string") } return nil @@ -87,7 +87,7 @@ func (value *License) UnmarshalJSON(data []byte) error { func (value *License) Validate(c context.Context) error { if value.Name == "" { - return errors.New("value of license name must be a non-empty JSON string") + return errors.New("value of license name must be a non-empty string") } return nil } diff --git a/openapi3/link.go b/openapi3/link.go index 0fe1a1c74..722c166d2 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -49,7 +49,7 @@ func (value *Link) Validate(c context.Context) error { return errors.New("missing operationId or operationRef on link") } if value.OperationID != "" && value.OperationRef != "" { - return fmt.Errorf("operationId '%s' and operationRef '%s' are mutually exclusive", value.OperationID, value.OperationRef) + return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", value.OperationID, value.OperationRef) } return nil } diff --git a/openapi3/operation.go b/openapi3/operation.go index 08b43127b..f7ff93fe5 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -136,7 +136,7 @@ func (operation *Operation) Validate(c context.Context) error { return err } } else { - return errors.New("value of responses must be a JSON object") + return errors.New("value of responses must be an object") } return nil } diff --git a/openapi3/operation_test.go b/openapi3/operation_test.go index fe59c6031..50684a3ae 100644 --- a/openapi3/operation_test.go +++ b/openapi3/operation_test.go @@ -53,7 +53,7 @@ func TestOperationValidation(t *testing.T) { { "when no Responses object is provided", operationWithoutResponses(), - errors.New("value of responses must be a JSON object"), + errors.New("value of responses must be an object"), }, { "when a Responses object is provided", diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 8603bd7bc..76e5f7f1d 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -38,7 +38,7 @@ func (p Parameters) JSONLookup(token string) (interface{}, error) { } if index < 0 || index >= len(p) { - return nil, fmt.Errorf("index out of bounds array[0,%d] index '%d'", len(p), index) + return nil, fmt.Errorf("index %d out of bounds of array of length %d", index, len(p)) } ref := p[index] diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 2ed578aa0..f7cf1d989 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -87,7 +87,7 @@ func (pathItem *PathItem) GetOperation(method string) *Operation { case http.MethodTrace: return pathItem.Trace default: - panic(fmt.Errorf("Unsupported HTTP method '%s'", method)) + panic(fmt.Errorf("unsupported HTTP method %q", method)) } } @@ -112,7 +112,7 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { case http.MethodTrace: pathItem.Trace = operation default: - panic(fmt.Errorf("Unsupported HTTP method '%s'", method)) + panic(fmt.Errorf("unsupported HTTP method %q", method)) } } diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index bd79560cf..7de298ca4 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -109,7 +109,7 @@ components: ` _, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) - require.EqualError(t, err, `invalid response: value MUST be a JSON object`) + require.EqualError(t, err, `invalid response: value MUST be an object`) } func TestIssue247(t *testing.T) { diff --git a/openapi3/schema.go b/openapi3/schema.go index bed51c159..c39023e53 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -23,15 +23,15 @@ var ( //SchemaFormatValidationDisabled disables validation of schema type formats. SchemaFormatValidationDisabled = false - errSchema = errors.New("Input does not match the schema") + errSchema = errors.New("input does not match the schema") // ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema ErrOneOfConflict = errors.New("input matches more than one oneOf schemas") // ErrSchemaInputNaN may be returned when validating a number - ErrSchemaInputNaN = errors.New("NaN is not allowed") + ErrSchemaInputNaN = errors.New("floating point NaN is not allowed") // ErrSchemaInputInf may be returned when validating a number - ErrSchemaInputInf = errors.New("Inf is not allowed") + ErrSchemaInputInf = errors.New("floating point Inf is not allowed") ) // Float64Ptr is a helper for defining OpenAPI schemas. @@ -591,7 +591,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { stack = append(stack, schema) if schema.ReadOnly && schema.WriteOnly { - return errors.New("A property MUST NOT be marked as both readOnly and writeOnly being true") + return errors.New("a property MUST NOT be marked as both readOnly and writeOnly being true") } for _, item := range schema.OneOf { @@ -678,11 +678,11 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { } case "array": if schema.Items == nil { - return errors.New("When schema type is 'array', schema 'items' must be non-null") + return errors.New("when schema type is 'array', schema 'items' must be non-null") } case "object": default: - return fmt.Errorf("Unsupported 'type' value '%s'", schemaType) + return fmt.Errorf("unsupported 'type' value %q", schemaType) } if ref := schema.Items; ref != nil { @@ -791,7 +791,7 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf Value: value, Schema: schema, SchemaField: "type", - Reason: fmt.Sprintf("Not a JSON value: %T", value), + Reason: fmt.Sprintf("unhandled value of type %T", value), } } } @@ -810,7 +810,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Value: value, Schema: schema, SchemaField: "enum", - Reason: "JSON value is not one of the allowed values", + Reason: "value is not one of the allowed values", } } @@ -994,7 +994,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "exclusiveMinimum", - Reason: fmt.Sprintf("Number must be more than %g", *schema.Min), + Reason: fmt.Sprintf("number must be more than %g", *schema.Min), } if !settings.multiError { return err @@ -1011,7 +1011,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "exclusiveMaximum", - Reason: fmt.Sprintf("Number must be less than %g", *schema.Max), + Reason: fmt.Sprintf("number must be less than %g", *schema.Max), } if !settings.multiError { return err @@ -1028,7 +1028,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "minimum", - Reason: fmt.Sprintf("Number must be at least %g", *v), + Reason: fmt.Sprintf("number must be at least %g", *v), } if !settings.multiError { return err @@ -1045,7 +1045,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "maximum", - Reason: fmt.Sprintf("Number must be most %g", *v), + Reason: fmt.Sprintf("number must be most %g", *v), } if !settings.multiError { return err @@ -1113,7 +1113,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "minLength", - Reason: fmt.Sprintf("Minimum string length is %d", minLength), + Reason: fmt.Sprintf("minimum string length is %d", minLength), } if !settings.multiError { return err @@ -1128,7 +1128,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "maxLength", - Reason: fmt.Sprintf("Maximum string length is %d", *maxLength), + Reason: fmt.Sprintf("maximum string length is %d", *maxLength), } if !settings.multiError { return err @@ -1158,7 +1158,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "pattern", - Reason: fmt.Sprintf("JSON string doesn't match the regular expression %q", schema.Pattern), + Reason: fmt.Sprintf("string doesn't match the regular expression %q", schema.Pattern), } if !settings.multiError { return err @@ -1173,7 +1173,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value switch { case f.regexp != nil && f.callback == nil: if cp := f.regexp; !cp.MatchString(value) { - formatErr = fmt.Sprintf("JSON string doesn't match the format %q (regular expression %q)", format, cp.String()) + formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression %q)", format, cp.String()) } case f.regexp == nil && f.callback != nil: if err := f.callback(value); err != nil { @@ -1228,7 +1228,7 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ Value: value, Schema: schema, SchemaField: "minItems", - Reason: fmt.Sprintf("Minimum number of items is %d", v), + Reason: fmt.Sprintf("minimum number of items is %d", v), } if !settings.multiError { return err @@ -1245,7 +1245,7 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ Value: value, Schema: schema, SchemaField: "maxItems", - Reason: fmt.Sprintf("Maximum number of items is %d", *v), + Reason: fmt.Sprintf("maximum number of items is %d", *v), } if !settings.multiError { return err @@ -1265,7 +1265,7 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ Value: value, Schema: schema, SchemaField: "uniqueItems", - Reason: fmt.Sprintf("Duplicate items found"), + Reason: fmt.Sprintf("duplicate items found"), } if !settings.multiError { return err @@ -1326,7 +1326,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "minProperties", - Reason: fmt.Sprintf("There must be at least %d properties", v), + Reason: fmt.Sprintf("there must be at least %d properties", v), } if !settings.multiError { return err @@ -1343,7 +1343,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "maxProperties", - Reason: fmt.Sprintf("There must be at most %d properties", *v), + Reason: fmt.Sprintf("there must be at most %d properties", *v), } if !settings.multiError { return err @@ -1408,7 +1408,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "properties", - Reason: fmt.Sprintf("Property '%s' is unsupported", k), + Reason: fmt.Sprintf("property %q is unsupported", k), } if !settings.multiError { return err @@ -1432,7 +1432,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "required", - Reason: fmt.Sprintf("Property '%s' is missing", k), + Reason: fmt.Sprintf("property %q is missing", k), }, k) if !settings.multiError { return err @@ -1572,5 +1572,5 @@ func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) { } func unsupportedFormat(format string) error { - return fmt.Errorf("Unsupported 'format' value '%s'", format) + return fmt.Errorf("unsupported 'format' value %q", format) } diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index ab2992bc9..1eb41509e 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -26,7 +26,7 @@ var SchemaStringFormats = make(map[string]Format, 8) func DefineStringFormat(name string, pattern string) { re, err := regexp.Compile(pattern) if err != nil { - err := fmt.Errorf("Format '%v' has invalid pattern '%v': %v", name, pattern, err) + err := fmt.Errorf("format %q has invalid pattern %q: %v", name, pattern, err) panic(err) } SchemaStringFormats[name] = Format{regexp: re} diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 81898c888..91747d84b 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1082,7 +1082,7 @@ func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*t break } } - require.True(t, found, fmt.Sprintf("Missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))) + require.True(t, found, fmt.Sprintf("missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))) } } } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 8d6c7c1fb..d1f665cd9 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -117,16 +117,16 @@ func (ss *SecurityScheme) Validate(c context.Context) error { hasBearerFormat = true case "basic", "negotiate", "digest": default: - return fmt.Errorf("Security scheme of type 'http' has invalid 'scheme' value '%s'", scheme) + return fmt.Errorf("security scheme of type 'http' has invalid 'scheme' value %q", scheme) } case "oauth2": hasFlow = true case "openIdConnect": if ss.OpenIdConnectUrl == "" { - return fmt.Errorf("No OIDC URL found for openIdConnect security scheme %q", ss.Name) + return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", ss.Name) } default: - return fmt.Errorf("Security scheme 'type' can't be '%v'", ss.Type) + return fmt.Errorf("security scheme 'type' can't be %q", ss.Type) } // Validate "in" and "name" @@ -134,34 +134,34 @@ func (ss *SecurityScheme) Validate(c context.Context) error { switch ss.In { case "query", "header", "cookie": default: - return fmt.Errorf("Security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not '%s'", ss.In) + return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", ss.In) } if ss.Name == "" { - return errors.New("Security scheme of type 'apiKey' should have 'name'") + return errors.New("security scheme of type 'apiKey' should have 'name'") } } else if len(ss.In) > 0 { - return fmt.Errorf("Security scheme of type '%s' can't have 'in'", ss.Type) + return fmt.Errorf("security scheme of type %q can't have 'in'", ss.Type) } else if len(ss.Name) > 0 { - return errors.New("Security scheme of type 'apiKey' can't have 'name'") + return errors.New("security scheme of type 'apiKey' can't have 'name'") } // Validate "format" // "bearerFormat" is an arbitrary string so we only check if the scheme supports it if !hasBearerFormat && len(ss.BearerFormat) > 0 { - return fmt.Errorf("Security scheme of type '%v' can't have 'bearerFormat'", ss.Type) + return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", ss.Type) } // Validate "flow" if hasFlow { flow := ss.Flows if flow == nil { - return fmt.Errorf("Security scheme of type '%v' should have 'flows'", ss.Type) + return fmt.Errorf("security scheme of type %q should have 'flows'", ss.Type) } if err := flow.Validate(c); err != nil { - return fmt.Errorf("Security scheme 'flow' is invalid: %v", err) + return fmt.Errorf("security scheme 'flow' is invalid: %v", err) } } else if ss.Flows != nil { - return fmt.Errorf("Security scheme of type '%s' can't have 'flows'", ss.Type) + return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type) } return nil } @@ -204,7 +204,7 @@ func (flows *OAuthFlows) Validate(c context.Context) error { if v := flows.AuthorizationCode; v != nil { return v.Validate(c, oAuthFlowAuthorizationCode) } - return errors.New("No OAuth flow is defined") + return errors.New("no OAuth flow is defined") } type OAuthFlow struct { @@ -226,16 +226,16 @@ func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { func (flow *OAuthFlow) Validate(c context.Context, typ oAuthFlowType) error { if typ == oAuthFlowAuthorizationCode || typ == oAuthFlowTypeImplicit { if v := flow.AuthorizationURL; v == "" { - return errors.New("An OAuth flow is missing 'authorizationUrl in authorizationCode or implicit '") + return errors.New("an OAuth flow is missing 'authorizationUrl in authorizationCode or implicit '") } } if typ != oAuthFlowTypeImplicit { if v := flow.TokenURL; v == "" { - return errors.New("An OAuth flow is missing 'tokenUrl in not implicit'") + return errors.New("an OAuth flow is missing 'tokenUrl in not implicit'") } } if v := flow.Scopes; v == nil { - return errors.New("An OAuth flow is missing 'scopes'") + return errors.New("an OAuth flow is missing 'scopes'") } return nil } diff --git a/openapi3/server.go b/openapi3/server.go index eeeae0668..56dd1d8ed 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -64,7 +64,7 @@ func (server Server) ParameterNames() ([]string, error) { pattern = pattern[i+1:] i = strings.IndexByte(pattern, '}') if i < 0 { - return nil, errors.New("Missing '}'") + return nil, errors.New("missing '}'") } params = append(params, strings.TrimSpace(pattern[:i])) pattern = pattern[i+1:] @@ -126,7 +126,7 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) { func (server *Server) Validate(c context.Context) (err error) { if server.URL == "" { - return errors.New("value of url must be a non-empty JSON string") + return errors.New("value of url must be a non-empty string") } for _, v := range server.Variables { if err = v.Validate(c); err != nil { @@ -156,13 +156,13 @@ func (serverVariable *ServerVariable) Validate(c context.Context) error { switch serverVariable.Default.(type) { case float64, string: default: - return errors.New("value of default must be either JSON number or JSON string") + return errors.New("value of default must be either a number or a string") } for _, item := range serverVariable.Enum { switch item.(type) { case float64, string: default: - return errors.New("All 'enum' items must be either a number or a string") + return errors.New("all 'enum' items must be either a number or a string") } } return nil diff --git a/openapi3/server_test.go b/openapi3/server_test.go index 550eacbd9..0ace0345f 100644 --- a/openapi3/server_test.go +++ b/openapi3/server_test.go @@ -69,7 +69,7 @@ func TestServerValidation(t *testing.T) { { "when no URL is provided", invalidServer(), - errors.New("value of url must be a non-empty JSON string"), + errors.New("value of url must be a non-empty string"), }, { "when a URL is provided", diff --git a/openapi3/swagger.go b/openapi3/swagger.go index 06be8a343..64f76c232 100644 --- a/openapi3/swagger.go +++ b/openapi3/swagger.go @@ -48,7 +48,7 @@ func (swagger *Swagger) AddServer(server *Server) { func (swagger *Swagger) Validate(c context.Context) error { if swagger.OpenAPI == "" { - return errors.New("value of openapi must be a non-empty JSON string") + return errors.New("value of openapi must be a non-empty string") } // NOTE: only mention info/components/paths/... key in this func's errors. @@ -67,7 +67,7 @@ func (swagger *Swagger) Validate(c context.Context) error { return wrap(err) } } else { - return wrap(errors.New("must be a JSON object")) + return wrap(errors.New("must be an object")) } } @@ -78,7 +78,7 @@ func (swagger *Swagger) Validate(c context.Context) error { return wrap(err) } } else { - return wrap(errors.New("must be a JSON object")) + return wrap(errors.New("must be an object")) } } diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 3bb902dff..8ae8d0406 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -71,8 +71,7 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location) } -// loadSingleElementFromURI read the data from ref and unmarshal to JSON to the -// passed element. +// loadSingleElementFromURI reads the data from ref and unmarshals to the passed element. func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) error { if !swaggerLoader.IsExternalRefsAllowed { return fmt.Errorf("encountered non-allowed external reference: %q", ref) @@ -428,7 +427,7 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component const prefix = "#/components/headers/" if component == nil { - return errors.New("invalid header: value MUST be a JSON object") + return errors.New("invalid header: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { @@ -475,7 +474,7 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon const prefix = "#/components/parameters/" if component == nil { - return errors.New("invalid parameter: value MUST be a JSON object") + return errors.New("invalid parameter: value MUST be an object") } ref := component.Ref if ref != "" { @@ -538,7 +537,7 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp const prefix = "#/components/requestBodies/" if component == nil { - return errors.New("invalid requestBody: value MUST be a JSON object") + return errors.New("invalid requestBody: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { @@ -593,7 +592,7 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone const prefix = "#/components/responses/" if component == nil { - return errors.New("invalid response: value MUST be a JSON object") + return errors.New("invalid response: value MUST be an object") } ref := component.Ref if ref != "" { @@ -667,7 +666,7 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component const prefix = "#/components/schemas/" if component == nil { - return errors.New("invalid schema: value MUST be a JSON object") + return errors.New("invalid schema: value MUST be an object") } ref := component.Ref if ref != "" { @@ -753,7 +752,7 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c const prefix = "#/components/securitySchemes/" if component == nil { - return errors.New("invalid securityScheme: value MUST be a JSON object") + return errors.New("invalid securityScheme: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { @@ -791,7 +790,7 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen const prefix = "#/components/examples/" if component == nil { - return errors.New("invalid example: value MUST be a JSON object") + return errors.New("invalid example: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { @@ -829,7 +828,7 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * const prefix = "#/components/links/" if component == nil { - return errors.New("invalid link: value MUST be a JSON object") + return errors.New("invalid link: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { @@ -867,7 +866,7 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo const prefix = "#/paths/" if pathItem == nil { - return errors.New("invalid path item: value MUST be a JSON object") + return errors.New("invalid path item: value MUST be an object") } ref := pathItem.Ref if ref != "" { diff --git a/openapi3/swagger_loader_read_from_uri_func_test.go b/openapi3/swagger_loader_read_from_uri_func_test.go index b15767855..d63a3be3c 100644 --- a/openapi3/swagger_loader_read_from_uri_func_test.go +++ b/openapi3/swagger_loader_read_from_uri_func_test.go @@ -33,7 +33,7 @@ func (l *multipleSourceSwaggerLoaderExample) LoadSwaggerFromURI( ) ([]byte, error) { source := l.resolveSourceFromURI(location) if source == nil { - return nil, fmt.Errorf("Unsupported URI: %q", location.String()) + return nil, fmt.Errorf("unsupported URI: %q", location.String()) } return source, nil } diff --git a/openapi3/swagger_loader_relative_refs_test.go b/openapi3/swagger_loader_relative_refs_test.go index 071938908..8f074680a 100644 --- a/openapi3/swagger_loader_relative_refs_test.go +++ b/openapi3/swagger_loader_relative_refs_test.go @@ -196,7 +196,7 @@ var refTestDataEntriesResponseError = []refTestDataEntryWithErrorMessage{ func TestLoadFromDataWithExternalRef(t *testing.T) { for _, td := range refTestDataEntries { - t.Logf("testcase '%s'", td.name) + t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json")) loader := NewSwaggerLoader() @@ -209,7 +209,7 @@ func TestLoadFromDataWithExternalRef(t *testing.T) { func TestLoadFromDataWithExternalRefResponseError(t *testing.T) { for _, td := range refTestDataEntriesResponseError { - t.Logf("testcase '%s'", td.name) + t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json")) loader := NewSwaggerLoader() @@ -222,7 +222,7 @@ func TestLoadFromDataWithExternalRefResponseError(t *testing.T) { func TestLoadFromDataWithExternalNestedRef(t *testing.T) { for _, td := range refTestDataEntries { - t.Logf("testcase '%s'", td.name) + t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "nesteddir/nestedcomponents.openapi.json")) loader := NewSwaggerLoader() @@ -801,7 +801,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ func TestLoadSpecWithRelativeDocumentRefs(t *testing.T) { for _, td := range relativeDocRefsTestDataEntries { - t.Logf("testcase '%s'", td.name) + t.Logf("testcase %q", td.name) spec := []byte(td.contentTemplate) loader := NewSwaggerLoader() diff --git a/openapi3/swagger_test.go b/openapi3/swagger_test.go index 0117cea6e..ded2500b2 100644 --- a/openapi3/swagger_test.go +++ b/openapi3/swagger_test.go @@ -393,10 +393,10 @@ components: tests := map[string]string{ spec: "", - strings.Replace(spec, `openapi: 3.0.2`, ``, 1): "value of openapi must be a non-empty JSON string", - strings.Replace(spec, `openapi: 3.0.2`, `openapi: ''`, 1): "value of openapi must be a non-empty JSON string", - strings.Replace(spec, info, ``, 1): "invalid info: must be a JSON object", - strings.Replace(spec, paths, ``, 1): "invalid paths: must be a JSON object", + strings.Replace(spec, `openapi: 3.0.2`, ``, 1): "value of openapi must be a non-empty string", + strings.Replace(spec, `openapi: 3.0.2`, `openapi: ''`, 1): "value of openapi must be a non-empty string", + strings.Replace(spec, info, ``, 1): "invalid info: must be an object", + strings.Replace(spec, paths, ``, 1): "invalid paths: must be an object", } for spec, expectedErr := range tests { diff --git a/openapi3/unique_items_checker_test.go b/openapi3/unique_items_checker_test.go index c2dc6f381..85147c67a 100644 --- a/openapi3/unique_items_checker_test.go +++ b/openapi3/unique_items_checker_test.go @@ -32,5 +32,5 @@ func TestRegisterArrayUniqueItemsChecker(t *testing.T) { err = schema.VisitJSON(val) require.Error(t, err) - require.True(t, strings.HasPrefix(err.Error(), "Duplicate items found")) + require.True(t, strings.HasPrefix(err.Error(), "duplicate items found")) } diff --git a/openapi3filter/authentication_input.go b/openapi3filter/authentication_input.go index bae7c43d3..a53484b99 100644 --- a/openapi3filter/authentication_input.go +++ b/openapi3filter/authentication_input.go @@ -2,7 +2,6 @@ package openapi3filter import ( "fmt" - "strings" "github.com/getkin/kin-openapi/openapi3" ) @@ -16,19 +15,15 @@ type AuthenticationInput struct { func (input *AuthenticationInput) NewError(err error) error { if err == nil { - scopes := input.Scopes - if len(scopes) == 0 { - err = fmt.Errorf("Security requirement '%s' failed", - input.SecuritySchemeName) + if len(input.Scopes) == 0 { + err = fmt.Errorf("security requirement %q failed", input.SecuritySchemeName) } else { - err = fmt.Errorf("Security requirement '%s' (scopes: '%s') failed", - input.SecuritySchemeName, - strings.Join(input.Scopes, "', '")) + err = fmt.Errorf("security requirement %q (scopes: %+v) failed", input.SecuritySchemeName, input.Scopes) } } return &RequestError{ Input: input.RequestValidationInput, - Reason: "Authorization failed", + Reason: "authorization failed", Err: err, } } diff --git a/openapi3filter/errors.go b/openapi3filter/errors.go index 9b46ebda6..f4c9f0b79 100644 --- a/openapi3filter/errors.go +++ b/openapi3filter/errors.go @@ -1,19 +1,11 @@ package openapi3filter import ( - "errors" "fmt" - "net/http" "github.com/getkin/kin-openapi/openapi3" ) -var ( - errRouteMissingSwagger = errors.New("Route is missing OpenAPI specification") - errRouteMissingOperation = errors.New("Route is missing OpenAPI operation") - ErrAuthenticationServiceMissing = errors.New("Request validator doesn't have an authentication service defined") -) - type RouteError struct { Route Route Reason string @@ -23,23 +15,17 @@ func (err *RouteError) Error() string { return err.Reason } +var _ error = &RequestError{} + +// RequestError is returned by ValidateRequest when request does not match OpenAPI spec type RequestError struct { Input *RequestValidationInput Parameter *openapi3.Parameter RequestBody *openapi3.RequestBody - Status int Reason string Err error } -func (err *RequestError) HTTPStatus() int { - status := err.Status - if status == 0 { - status = http.StatusBadRequest - } - return status -} - func (err *RequestError) Error() string { reason := err.Reason if e := err.Err; e != nil { @@ -50,14 +36,17 @@ func (err *RequestError) Error() string { } } if v := err.Parameter; v != nil { - return fmt.Sprintf("Parameter '%s' in %s has an error: %s", v.Name, v.In, reason) + return fmt.Sprintf("parameter %q in %s has an error: %s", v.Name, v.In, reason) } else if v := err.RequestBody; v != nil { - return fmt.Sprintf("Request body has an error: %s", reason) + return fmt.Sprintf("request body has an error: %s", reason) } else { return reason } } +var _ error = &ResponseError{} + +// ResponseError is returned by ValidateResponse when response does not match OpenAPI spec type ResponseError struct { Input *ResponseValidationInput Reason string @@ -76,6 +65,10 @@ func (err *ResponseError) Error() string { return reason } +var _ error = &SecurityRequirementsError{} + +// SecurityRequirementsError is returned by ValidateSecurityRequirements +// when no requirement is met. type SecurityRequirementsError struct { SecurityRequirements openapi3.SecurityRequirements Errors []error diff --git a/openapi3filter/options.go b/openapi3filter/options.go index b0fb39df3..1622339e2 100644 --- a/openapi3filter/options.go +++ b/openapi3filter/options.go @@ -7,8 +7,14 @@ var DefaultOptions = &Options{} // Options used by ValidateRequest and ValidateResponse type Options struct { - ExcludeRequestBody bool - ExcludeResponseBody bool + // Set ExcludeRequestBody so ValidateRequest skips request body validation + ExcludeRequestBody bool + + // Set ExcludeResponseBody so ValidateResponse skips response body validation + ExcludeResponseBody bool + + // Set IncludeResponseStatus so ValidateResponse fails on response + // status not defined in OpenAPI spec IncludeResponseStatus bool MultiError bool diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 163bc69b2..d19266440 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -130,13 +130,13 @@ func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationI found = true } default: - err = fmt.Errorf("unsupported parameter's 'in': %s", param.In) + err = fmt.Errorf("unsupported parameter.in: %q", param.In) return } if !found { if param.Required { - err = fmt.Errorf("parameter '%s' is required, but missing", param.Name) + err = fmt.Errorf("parameter %q is required, but missing", param.Name) } return } @@ -154,32 +154,32 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) outValue interface{}, outSchema *openapi3.Schema, err error) { // Only query parameters can have multiple values. if len(values) > 1 && param.In != openapi3.ParameterInQuery { - err = fmt.Errorf("%s parameter '%s' can't have multiple values", param.In, param.Name) + err = fmt.Errorf("%s parameter %q cannot have multiple values", param.In, param.Name) return } content := param.Content if content == nil { - err = fmt.Errorf("parameter '%s' expected to have content", param.Name) + err = fmt.Errorf("parameter %q expected to have content", param.Name) return } // We only know how to decode a parameter if it has one content, application/json if len(content) != 1 { - err = fmt.Errorf("multiple content types for parameter '%s'", param.Name) + err = fmt.Errorf("multiple content types for parameter %q", param.Name) return } mt := content.Get("application/json") if mt == nil { - err = fmt.Errorf("parameter '%s' has no json content schema", param.Name) + err = fmt.Errorf("parameter %q has no content schema", param.Name) return } outSchema = mt.Schema.Value if len(values) == 1 { if err = json.Unmarshal([]byte(values[0]), &outValue); err != nil { - err = fmt.Errorf("error unmarshaling parameter '%s' as json", param.Name) + err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return } } else { @@ -187,7 +187,7 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) for _, v := range values { var item interface{} if err = json.Unmarshal([]byte(v), &item); err != nil { - err = fmt.Errorf("error unmarshaling parameter '%s' as json", param.Name) + err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return } outArray = append(outArray, item) @@ -257,12 +257,10 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho return value, nil } } - if required == true { + if required { return nil, fmt.Errorf("decoding anyOf for parameter %q failed", param) - } else { - return nil, nil } - + return nil, nil } if len(schema.Value.OneOf) > 0 { @@ -280,12 +278,12 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho } else if isMatched > 1 { return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched) } - if required == true { + if required { return nil, fmt.Errorf("decoding oneOf failed: %q is required", param) - } else { - return nil, nil } + return nil, nil } + if schema.Value.Not != nil { // TODO(decode not): handle decoding "not" JSON Schema return nil, errors.New("not implemented: decoding 'not'") @@ -306,6 +304,7 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho } return decodeFn(param, sm, schema) } + return nil, nil } @@ -331,7 +330,7 @@ func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializat // HTTP request does not contains a value of the target path parameter. return nil, nil } - raw, ok := d.pathParams[paramKey(param, sm)] + raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. return nil, nil @@ -368,7 +367,7 @@ func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationM // HTTP request does not contains a value of the target path parameter. return nil, nil } - raw, ok := d.pathParams[paramKey(param, sm)] + raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. return nil, nil @@ -383,25 +382,25 @@ func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationM func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { var prefix, propsDelim, valueDelim string switch { - case sm.Style == "simple" && sm.Explode == false: + case sm.Style == "simple" && !sm.Explode: propsDelim = "," valueDelim = "," - case sm.Style == "simple" && sm.Explode == true: + case sm.Style == "simple" && sm.Explode: propsDelim = "," valueDelim = "=" - case sm.Style == "label" && sm.Explode == false: + case sm.Style == "label" && !sm.Explode: prefix = "." propsDelim = "," valueDelim = "," - case sm.Style == "label" && sm.Explode == true: + case sm.Style == "label" && sm.Explode: prefix = "." propsDelim = "." valueDelim = "=" - case sm.Style == "matrix" && sm.Explode == false: + case sm.Style == "matrix" && !sm.Explode: prefix = ";" + param + "=" propsDelim = "," valueDelim = "," - case sm.Style == "matrix" && sm.Explode == true: + case sm.Style == "matrix" && sm.Explode: prefix = ";" propsDelim = ";" valueDelim = "=" @@ -413,7 +412,7 @@ func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.Serialization // HTTP request does not contains a value of the target path parameter. return nil, nil } - raw, ok := d.pathParams[paramKey(param, sm)] + raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. return nil, nil @@ -429,18 +428,6 @@ func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.Serialization return makeObject(props, schema) } -// paramKey returns a key to get a raw value of a path parameter. -func paramKey(param string, sm *openapi3.SerializationMethod) string { - switch sm.Style { - case "label": - return "." + param - case "matrix": - return ";" + param - default: - return param - } -} - // cutPrefix validates that a raw value of a path parameter has the specified prefix, // and returns a raw value without the prefix. func cutPrefix(raw, prefix string) (string, error) { @@ -737,8 +724,8 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err } // parsePrimitive returns a value that is created by parsing a source string to a primitive type -// that is specified by a JSON schema. The function returns nil when the source string is empty. -// The function panics when a JSON schema has a non primitive type. +// that is specified by a schema. The function returns nil when the source string is empty. +// The function panics when a schema has a non primitive type. func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) { if raw == "" { return nil, nil @@ -804,16 +791,20 @@ func UnregisterBodyDecoder(contentType string) { delete(bodyDecoders, contentType) } +var headerCT = http.CanonicalHeaderKey("Content-Type") + +const prefixUnsupportedCT = "unsupported content type" + // decodeBody returns a decoded body. // The function returns ParseError when a body is invalid. func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { - contentType := header.Get(http.CanonicalHeaderKey("Content-Type")) + contentType := header.Get(headerCT) mediaType := parseMediaType(contentType) decoder, ok := bodyDecoders[mediaType] if !ok { return nil, &ParseError{ Kind: KindUnsupportedFormat, - Reason: fmt.Sprintf("unsupported content type %q", mediaType), + Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType), } } value, err := decoder(body, header, schema, encFn) @@ -849,20 +840,20 @@ func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema } func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { - // Validate JSON schema of request body. + // Validate schema of request body. // By the OpenAPI 3 specification request body's schema must have type "object". // Properties of the schema describes individual parts of request body. if schema.Value.Type != "object" { - return nil, errors.New("unsupported JSON schema of request body") + return nil, errors.New("unsupported schema of request body") } for propName, propSchema := range schema.Value.Properties { switch propSchema.Value.Type { case "object": - return nil, fmt.Errorf("unsupported JSON schema of request body's property %q", propName) + return nil, fmt.Errorf("unsupported schema of request body's property %q", propName) case "array": items := propSchema.Value.Items.Value if items.Type != "string" && items.Type != "integer" && items.Type != "number" && items.Type != "boolean" { - return nil, fmt.Errorf("unsupported JSON schema of request body's property %q", propName) + return nil, fmt.Errorf("unsupported schema of request body's property %q", propName) } } } @@ -901,12 +892,12 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { if schema.Value.Type != "object" { - return nil, errors.New("unsupported JSON schema of request body") + return nil, errors.New("unsupported schema of request body") } // Parse form. values := make(map[string][]interface{}) - contentType := header.Get(http.CanonicalHeaderKey("Content-Type")) + contentType := header.Get(headerCT) _, params, err := mime.ParseMediaType(contentType) if err != nil { return nil, err diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 1cf7be33e..04ecd1693 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -2,6 +2,7 @@ package openapi3filter import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -903,20 +904,9 @@ func TestDecodeParameter(t *testing.T) { req.AddCookie(&http.Cookie{Name: v[0], Value: v[1]}) } - var path string + path := "/test" if tc.path != "" { - switch tc.param.Style { - case "label": - path = "." + tc.param.Name - case "matrix": - path = ";" + tc.param.Name - default: - path = tc.param.Name - } - if tc.param.Explode != nil && *tc.param.Explode { - path += "*" - } - path = "/{" + path + "}" + path += "/{" + tc.param.Name + "}" } info := &openapi3.Info{ @@ -929,12 +919,14 @@ func TestDecodeParameter(t *testing.T) { Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, Responses: openapi3.NewResponses(), } - spec.AddOperation("/test"+path, http.MethodGet, op) + spec.AddOperation(path, http.MethodGet, op) + err = spec.Validate(context.Background()) + require.NoError(t, err) router := NewRouter() - require.NoError(t, router.AddSwagger(spec), "failed to create a router") + require.NoError(t, router.AddSwagger(spec)) route, pathParams, err := router.FindRoute(req.Method, req.URL) - require.NoError(t, err, "failed to find a route") + require.NoError(t, err) input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route} got, err := decodeStyledParameter(tc.param, input) @@ -1015,7 +1007,7 @@ func TestDecodeBody(t *testing.T) { wantErr error }{ { - name: "unsupported content type", + name: prefixUnsupportedCT, mime: "application/xml", wantErr: &ParseError{Kind: KindUnsupportedFormat}, }, @@ -1134,7 +1126,7 @@ func TestDecodeBody(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { h := make(http.Header) - h.Set(http.CanonicalHeaderKey("Content-Type"), tc.mime) + h.Set(headerCT, tc.mime) var schemaRef *openapi3.SchemaRef if tc.schema != nil { schemaRef = tc.schema.NewRef() @@ -1180,7 +1172,7 @@ func newTestMultipartForm(parts []*testFormPart) (io.Reader, string, error) { } h := make(textproto.MIMEHeader) - h.Set("Content-Type", p.contentType) + h.Set(headerCT, p.contentType) h.Set("Content-Disposition", disp) pw, err := w.CreatePart(h) if err != nil { @@ -1214,7 +1206,7 @@ func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { wantErr = &ParseError{Kind: KindUnsupportedFormat} ) h := make(http.Header) - h.Set(http.CanonicalHeaderKey("Content-Type"), contentType) + h.Set(headerCT, contentType) RegisterBodyDecoder(contentType, decoder) got, err := decodeBody(body, h, schema, encFn) diff --git a/openapi3filter/router.go b/openapi3filter/router.go index 3904750eb..3c7690d58 100644 --- a/openapi3filter/router.go +++ b/openapi3filter/router.go @@ -12,6 +12,13 @@ import ( "github.com/getkin/kin-openapi/pathpattern" ) +// ErrPathNotFound is returned when no route match is found +var ErrPathNotFound = errors.New("no matching operation was found") + +// ErrMethodNotAllowed is returned when no method of the matched route matches +var ErrMethodNotAllowed = errors.New("method not allowed") + +// Route describes the operation an http.Request can match type Route struct { Swagger *openapi3.Swagger Server *openapi3.Server @@ -27,6 +34,7 @@ type Route struct { // Routers maps a HTTP request to a Router. type Routers []*Router +// FindRoute extracts the route and parameters of an http.Request func (routers Routers) FindRoute(method string, url *url.URL) (*Router, *Route, map[string]string, error) { for _, router := range routers { // Skip routers that have DO NOT have servers @@ -97,7 +105,7 @@ func (router *Router) AddSwaggerFromFile(path string) error { // AddSwagger adds all operations in the OpenAPI specification. func (router *Router) AddSwagger(swagger *openapi3.Swagger) error { if err := swagger.Validate(context.TODO()); err != nil { - return fmt.Errorf("Validating Swagger failed: %v", err) + return fmt.Errorf("validating OpenAPI failed: %v", err) } router.swagger = swagger root := router.node() @@ -122,12 +130,12 @@ func (router *Router) AddSwagger(swagger *openapi3.Swagger) error { func (router *Router) AddRoute(route *Route) error { method := route.Method if method == "" { - return errors.New("Route is missing method") + return errors.New("route is missing method") } method = strings.ToUpper(method) path := route.Path if path == "" { - return errors.New("Route is missing path") + return errors.New("route is missing path") } return router.node().Add(method+" "+path, router, nil) } @@ -141,6 +149,7 @@ func (router *Router) node() *pathpattern.Node { return root } +// FindRoute extracts the route and parameters of an http.Request func (router *Router) FindRoute(method string, url *url.URL) (*Route, map[string]string, error) { swagger := router.swagger @@ -159,7 +168,7 @@ func (router *Router) FindRoute(method string, url *url.URL) (*Route, map[string Route: Route{ Swagger: swagger, }, - Reason: "Does not match any server", + Reason: ErrPathNotFound.Error(), } } pathParams = make(map[string]string, 8) @@ -185,7 +194,7 @@ func (router *Router) FindRoute(method string, url *url.URL) (*Route, map[string Swagger: swagger, Server: server, }, - Reason: "Path was not found", + Reason: ErrPathNotFound.Error(), } } @@ -196,7 +205,7 @@ func (router *Router) FindRoute(method string, url *url.URL) (*Route, map[string Swagger: swagger, Server: server, }, - Reason: "Path doesn't support the HTTP method", + Reason: ErrMethodNotAllowed.Error(), } } diff --git a/openapi3filter/router_test.go b/openapi3filter/router_test.go index 5a7241145..6e8f7fd72 100644 --- a/openapi3filter/router_test.go +++ b/openapi3filter/router_test.go @@ -1,23 +1,16 @@ -package openapi3filter_test +package openapi3filter import ( + "context" "net/http" "sort" "testing" "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" "github.com/stretchr/testify/require" ) func TestRouter(t *testing.T) { - var ( - pathNotFound = "Path was not found" - methodNotAllowed = "Path doesn't support the HTTP method" - doesNotMatchAnyServer = "Does not match any server" - ) - - // Build swagger helloCONNECT := &openapi3.Operation{Responses: openapi3.NewResponses()} helloDELETE := &openapi3.Operation{Responses: openapi3.NewResponses()} helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} @@ -28,6 +21,7 @@ func TestRouter(t *testing.T) { 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()} swagger := &openapi3.Swagger{ OpenAPI: "3.0.0", @@ -50,6 +44,7 @@ func TestRouter(t *testing.T) { "/onlyGET": &openapi3.PathItem{ Get: helloGET, }, + //TODO: use "/params/{x}/{y}/{z:.*}": &openapi3.PathItem{ when reworking https://github.com/getkin/kin-openapi/pull/210 "/params/{x}/{y}/{z*}": &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ @@ -58,48 +53,48 @@ func TestRouter(t *testing.T) { &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")}, }, }, + "/books/{bookid}": &openapi3.PathItem{ + Get: paramsGET, + Parameters: openapi3.Parameters{ + &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, + }, + }, + "/books/{bookid2}.json": &openapi3.PathItem{ + Post: booksPOST, + Parameters: openapi3.Parameters{ + &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")}, + }, + }, "/partial": &openapi3.PathItem{ Get: partialGET, }, }, } - // Build router - router := openapi3filter.NewRouter().WithSwagger(swagger) - - // Declare a helper function - expect := func(method string, uri string, operation *openapi3.Operation, params map[string]string) { + expect := func(r *Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { req, err := http.NewRequest(method, uri, nil) require.NoError(t, err) - route, pathParams, err := router.FindRoute(req.Method, req.URL) + route, pathParams, err := r.FindRoute(req.Method, req.URL) if err != nil { if operation == nil { - if err.Error() == doesNotMatchAnyServer { - return - } - pathItem := swagger.Paths[uri] if pathItem == nil { - if err.Error() != pathNotFound { - t.Fatalf("'%s %s': should have returned '%s', but it returned an error: %v", - method, uri, pathNotFound, err) + if err.Error() != ErrPathNotFound.Error() { + t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, ErrPathNotFound, err) } return } if pathItem.GetOperation(method) == nil { - if err.Error() != methodNotAllowed { - t.Fatalf("'%s %s': should have returned '%s', but it returned an error: %v", - method, uri, methodNotAllowed, err) + if err.Error() != ErrMethodNotAllowed.Error() { + t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, ErrMethodNotAllowed, err) } } } else { - t.Fatalf("'%s %s': should have returned an operation, but it returned an error: %v", - method, uri, err) + t.Fatalf("'%s %s': should have returned an operation, but it returned an error: %v", method, uri, err) } } if operation == nil && err == nil { - t.Fatalf("'%s %s': should have returned an error, but didn't", - method, uri) + t.Fatalf("'%s %s': should have failed, but returned\nroute = %+v\npathParams = %+v", method, uri, route, pathParams) } if route == nil { return @@ -108,10 +103,9 @@ func TestRouter(t *testing.T) { t.Fatalf("'%s %s': Returned wrong operation (%v)", method, uri, route.Operation) } - if params == nil { + if len(params) == 0 { if len(pathParams) != 0 { - t.Fatalf("'%s %s': should return no path arguments, but found some", - method, uri) + t.Fatalf("'%s %s': should return no path arguments, but found %+v", method, uri, pathParams) } } else { names := make([]string, 0, len(params)) @@ -123,54 +117,71 @@ func TestRouter(t *testing.T) { expected := params[name] actual, exists := pathParams[name] if !exists { - t.Fatalf("'%s %s': path parameter '%s' should be '%s', but it's not defined.", - method, uri, name, expected) + t.Fatalf("'%s %s': path parameter %q should be %q, but it's not defined.", method, uri, name, expected) } if actual != expected { - t.Fatalf("'%s %s': path parameter '%s' should be '%s', but it's '%s'", - method, uri, name, expected, actual) + t.Fatalf("'%s %s': path parameter %q should be %q, but it's %q", method, uri, name, expected, actual) } } } } - expect(http.MethodGet, "/not_existing", nil, nil) - expect(http.MethodDelete, "/hello", helloDELETE, nil) - expect(http.MethodGet, "/hello", helloGET, nil) - expect(http.MethodHead, "/hello", helloHEAD, nil) - expect(http.MethodPatch, "/hello", helloPATCH, nil) - expect(http.MethodPost, "/hello", helloPOST, nil) - expect(http.MethodPut, "/hello", helloPUT, nil) - expect(http.MethodGet, "/params/a/b/c/d", paramsGET, map[string]string{ + + err := swagger.Validate(context.Background()) + require.NoError(t, err) + r := NewRouter().WithSwagger(swagger) + + expect(r, http.MethodGet, "/not_existing", nil, nil) + expect(r, http.MethodDelete, "/hello", helloDELETE, nil) + expect(r, http.MethodGet, "/hello", helloGET, nil) + expect(r, http.MethodHead, "/hello", helloHEAD, nil) + expect(r, http.MethodPatch, "/hello", helloPATCH, nil) + expect(r, http.MethodPost, "/hello", helloPOST, nil) + expect(r, http.MethodPut, "/hello", helloPUT, nil) + expect(r, http.MethodGet, "/params/a/b/", paramsGET, map[string]string{ + "x": "a", + "y": "b", + "z": "", + }) + expect(r, http.MethodGet, "/params/a/b/c%2Fd", paramsGET, map[string]string{ "x": "a", "y": "b", "z": "c/d", }) - expect(http.MethodPost, "/partial", nil, nil) - swagger.Servers = append(swagger.Servers, &openapi3.Server{ - URL: "https://www.example.com/api/v1/", - }, &openapi3.Server{ - URL: "https://{d0}.{d1}.com/api/v1/", + expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{ + "bookid": "War.and.Peace", }) - expect(http.MethodGet, "/hello", nil, nil) - expect(http.MethodGet, "/api/v1/hello", nil, nil) - expect(http.MethodGet, "www.example.com/api/v1/hello", nil, nil) - expect(http.MethodGet, "https:///api/v1/hello", nil, nil) - expect(http.MethodGet, "https://www.example.com/hello", nil, nil) - expect(http.MethodGet, "https://www.example.com/api/v1/hello", helloGET, map[string]string{}) - expect(http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{ + // TODO: fix https://github.com/getkin/kin-openapi/issues/129 + // expect(r, http.MethodPost, "/books/War.and.Peace.json", booksPOST, map[string]string{ + // "bookid2": "War.and.Peace", + // }) + expect(r, http.MethodPost, "/partial", nil, nil) + + swagger.Servers = []*openapi3.Server{ + {URL: "https://www.example.com/api/v1"}, + {URL: "https://{d0}.{d1}.com/api/v1/"}, + } + err = swagger.Validate(context.Background()) + require.NoError(t, err) + r = NewRouter().WithSwagger(swagger) + expect(r, http.MethodGet, "/hello", nil, nil) + expect(r, http.MethodGet, "/api/v1/hello", nil, nil) + expect(r, http.MethodGet, "www.example.com/api/v1/hello", nil, nil) + expect(r, http.MethodGet, "https:///api/v1/hello", nil, nil) + expect(r, http.MethodGet, "https://www.example.com/hello", nil, nil) + expect(r, http.MethodGet, "https://www.example.com/api/v1/hello", helloGET, nil) + expect(r, http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{ "d0": "domain0", "d1": "domain1", }) { uri := "https://www.example.com/api/v1/onlyGET" - expect(http.MethodGet, uri, helloGET, nil) + expect(r, http.MethodGet, uri, helloGET, nil) req, err := http.NewRequest(http.MethodDelete, uri, nil) require.NoError(t, err) require.NotNil(t, req) - route, pathParams, err := router.FindRoute(req.Method, req.URL) - require.Error(t, err) - require.Equal(t, err.(*openapi3filter.RouteError).Reason, "Path doesn't support the HTTP method") + route, pathParams, err := r.FindRoute(req.Method, req.URL) + require.EqualError(t, err, ErrMethodNotAllowed.Error()) require.Nil(t, route) require.Nil(t, pathParams) } diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go index fac9bf524..6812c6971 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -65,16 +65,18 @@ func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { } sl := openapi3.NewSwaggerLoader() - l, err := sl.LoadSwaggerFromData([]byte(spec)) + doc, err := sl.LoadSwaggerFromData([]byte(spec)) require.NoError(t, err) - router := NewRouter().WithSwagger(l) + err = doc.Validate(sl.Context) + require.NoError(t, err) + router := NewRouter().WithSwagger(doc) b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) require.NoError(t, err) httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b)) require.NoError(t, err) - httpReq.Header.Add("Content-Type", "application/json") + httpReq.Header.Add(headerCT, "application/json") route, pathParams, err := router.FindRoute(httpReq.Method, httpReq.URL) require.NoError(t, err) diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 0af54c299..f1e7bf977 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -12,8 +12,12 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -// ErrInvalidRequired is an error that happens when a required value of a parameter or request's body is not defined. -var ErrInvalidRequired = errors.New("must have a value") +// ErrAuthenticationServiceMissing is returned when no authentication service +// is defined for the request validator +var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc") + +// ErrInvalidRequired is returned when a required value of a parameter or request body is not defined. +var ErrInvalidRequired = errors.New("value is required but missing") // ValidateRequest is used to validate the given input according to previous // loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a @@ -32,13 +36,7 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { options = DefaultOptions } route := input.Route - if route == nil { - return errors.New("invalid route") - } operation := route.Operation - if operation == nil { - return errRouteMissingOperation - } operationParameters := operation.Parameters pathItemParameters := route.PathItem.Parameters @@ -87,9 +85,6 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { security := operation.Security // If there aren't any security requirements for the operation if security == nil { - if route.Swagger == nil { - return errRouteMissingSwagger - } // Use the global security requirements. security = &route.Swagger.Security } @@ -145,7 +140,7 @@ func ValidateParameter(c context.Context, input *RequestValidationInput, paramet // Validate a parameter's value. if value == nil { if parameter.Required { - return &RequestError{Input: input, Parameter: parameter, Reason: "must have a value", Err: ErrInvalidRequired} + return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired} } return nil } @@ -165,6 +160,8 @@ func ValidateParameter(c context.Context, input *RequestValidationInput, paramet return nil } +const prefixInvalidCT = "header Content-Type has unexpected value" + // ValidateRequestBody validates data of a request's body. // // The function returns RequestError with ErrInvalidRequired cause when a value is required but not defined. @@ -208,13 +205,13 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque return nil } - inputMIME := req.Header.Get("Content-Type") + inputMIME := req.Header.Get(headerCT) contentType := requestBody.Content.Get(inputMIME) if contentType == nil { return &RequestError{ Input: input, RequestBody: requestBody, - Reason: fmt.Sprintf("header 'Content-Type' has unexpected value: %q", inputMIME), + Reason: fmt.Sprintf("%s %q", prefixInvalidCT, inputMIME), } } @@ -279,9 +276,6 @@ func ValidateSecurityRequirements(c context.Context, input *RequestValidationInp // validateSecurityRequirement validates a single OpenAPI 3 security requirement func validateSecurityRequirement(c context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error { swagger := input.Route.Swagger - if swagger == nil { - return errRouteMissingSwagger - } securitySchemes := swagger.Components.SecuritySchemes // Ensure deterministic order @@ -312,7 +306,7 @@ func validateSecurityRequirement(c context.Context, input *RequestValidationInpu if securityScheme == nil { return &RequestError{ Input: input, - Err: fmt.Errorf("Security scheme '%s' is not declared", name), + Err: fmt.Errorf("security scheme %q is not declared", name), } } scopes := securityRequirement[name] diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index ba320cab3..e18d0f455 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -99,10 +99,30 @@ func Example() { PathParams: pathParams, Route: route, } - if err = openapi3filter.ValidateRequest(loader.Context, requestValidationInput); err == nil { - fmt.Println("Valid") - } else { - fmt.Println("NOT valid") + if err := openapi3filter.ValidateRequest(loader.Context, requestValidationInput); err != nil { + fmt.Println(err) } - // Output: NOT valid + // Output: + // request body has an error: doesn't match the schema: Doesn't match schema "oneOf" + // Schema: + // { + // "discriminator": { + // "propertyName": "pet_type" + // }, + // "oneOf": [ + // { + // "$ref": "#/components/schemas/Cat" + // }, + // { + // "$ref": "#/components/schemas/Dog" + // } + // ] + // } + // + // Value: + // { + // "bark": true, + // "breed": "Dingo", + // "pet_type": "Cat" + // } } diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index f203802a4..9575c4c1e 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -24,13 +24,6 @@ func ValidateResponse(c context.Context, input *ResponseValidationInput) error { return nil } status := input.Status - if status < 100 { - return &ResponseError{ - Input: input, - Reason: "illegal status code", - Err: fmt.Errorf("Status %d", status), - } - } // These status codes will never be validated. // TODO: The list is probably missing some. @@ -61,7 +54,6 @@ func ValidateResponse(c context.Context, input *ResponseValidationInput) error { if !options.IncludeResponseStatus { return nil } - return &ResponseError{Input: input, Reason: "status is not supported"} } response := responseRef.Value @@ -80,12 +72,12 @@ func ValidateResponse(c context.Context, input *ResponseValidationInput) error { return nil } - inputMIME := input.Header.Get("Content-Type") + inputMIME := input.Header.Get(headerCT) contentType := content.Get(inputMIME) if contentType == nil { return &ResponseError{ Input: input, - Reason: fmt.Sprintf("input header 'Content-Type' has unexpected value: %q", inputMIME), + Reason: fmt.Sprintf("input header Content-Type has unexpected value: %q", inputMIME), } } diff --git a/openapi3filter/validation_discriminator_test.go b/openapi3filter/validation_discriminator_test.go index 51d804f79..792944719 100644 --- a/openapi3filter/validation_discriminator_test.go +++ b/openapi3filter/validation_discriminator_test.go @@ -64,7 +64,7 @@ components: properties: base64: type: string - + objB: allOf: - $ref: '#/components/schemas/genericObj' @@ -77,7 +77,7 @@ components: func forgeRequest(body string) *http.Request { iobody := bytes.NewReader([]byte(body)) req, _ := http.NewRequest("PUT", "/blob", iobody) - req.Header.Add("Content-Type", "application/json") + req.Header.Add(headerCT, "application/json") return req } diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 34e4af94d..7c7ba5d6d 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "regexp" "strings" "github.com/getkin/kin-openapi/openapi3" @@ -17,10 +16,8 @@ type ValidationErrorEncoder struct { // Encode implements the ErrorEncoder interface for encoding ValidationErrors func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) { - var cErr *ValidationError - if e, ok := err.(*RouteError); ok { - cErr = convertRouteError(e) + cErr := convertRouteError(e) enc.Encoder(ctx, cErr, w) return } @@ -31,6 +28,7 @@ func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http return } + var cErr *ValidationError if e.Err == nil { cErr = convertBasicRequestError(e) } else if e.Err == ErrInvalidRequired { @@ -43,95 +41,80 @@ func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http if cErr != nil { enc.Encoder(ctx, cErr, w) - } else { - enc.Encoder(ctx, err, w) + return } + enc.Encoder(ctx, err, w) } func convertRouteError(e *RouteError) *ValidationError { - var cErr *ValidationError - switch e.Reason { - case "Path doesn't support the HTTP method": - cErr = &ValidationError{Status: http.StatusMethodNotAllowed, Title: e.Reason} - default: - cErr = &ValidationError{Status: http.StatusNotFound, Title: e.Reason} + status := http.StatusNotFound + if e.Reason == ErrMethodNotAllowed.Error() { + status = http.StatusMethodNotAllowed } - return cErr + return &ValidationError{Status: status, Title: e.Reason} } func convertBasicRequestError(e *RequestError) *ValidationError { - var cErr *ValidationError - unsupportedContentType := "header 'Content-Type' has unexpected value: " - if strings.HasPrefix(e.Reason, unsupportedContentType) { - if strings.HasSuffix(e.Reason, `: ""`) { - cErr = &ValidationError{ + if strings.HasPrefix(e.Reason, prefixInvalidCT) { + if strings.HasSuffix(e.Reason, `""`) { + return &ValidationError{ Status: http.StatusUnsupportedMediaType, - Title: "header 'Content-Type' is required", - } - } else { - cErr = &ValidationError{ - Status: http.StatusUnsupportedMediaType, - Title: "unsupported content type " + strings.TrimPrefix(e.Reason, unsupportedContentType), + Title: "header Content-Type is required", } } - } else { - cErr = &ValidationError{ - Status: http.StatusBadRequest, - Title: e.Error(), + return &ValidationError{ + Status: http.StatusUnsupportedMediaType, + Title: prefixUnsupportedCT + strings.TrimPrefix(e.Reason, prefixInvalidCT), } } - return cErr + return &ValidationError{ + Status: http.StatusBadRequest, + Title: e.Error(), + } } func convertErrInvalidRequired(e *RequestError) *ValidationError { - var cErr *ValidationError if e.Reason == ErrInvalidRequired.Error() && e.Parameter != nil { - cErr = &ValidationError{ + return &ValidationError{ Status: http.StatusBadRequest, - Title: fmt.Sprintf("Parameter '%s' in %s is required", e.Parameter.Name, e.Parameter.In), - } - } else { - cErr = &ValidationError{ - Status: http.StatusBadRequest, - Title: e.Error(), + Title: fmt.Sprintf("parameter %q in %s is required", e.Parameter.Name, e.Parameter.In), } } - return cErr + return &ValidationError{ + Status: http.StatusBadRequest, + Title: e.Error(), + } } func convertParseError(e *RequestError, innerErr *ParseError) *ValidationError { - var cErr *ValidationError // We treat path params of the wrong type like a 404 instead of a 400 if innerErr.Kind == KindInvalidFormat && e.Parameter != nil && e.Parameter.In == "path" { - cErr = &ValidationError{ + return &ValidationError{ Status: http.StatusNotFound, - Title: fmt.Sprintf("Resource not found with '%s' value: %v", e.Parameter.Name, innerErr.Value), + Title: fmt.Sprintf("resource not found with %q value: %v", e.Parameter.Name, innerErr.Value), } - } else if strings.HasPrefix(innerErr.Reason, "unsupported content type") { - cErr = &ValidationError{ + } else if strings.HasPrefix(innerErr.Reason, prefixUnsupportedCT) { + return &ValidationError{ Status: http.StatusUnsupportedMediaType, Title: innerErr.Reason, } } else if innerErr.RootCause() != nil { if rootErr, ok := innerErr.Cause.(*ParseError); ok && rootErr.Kind == KindInvalidFormat && e.Parameter.In == "query" { - cErr = &ValidationError{ + return &ValidationError{ Status: http.StatusBadRequest, - Title: fmt.Sprintf("Parameter '%s' in %s is invalid: %v is %s", + Title: fmt.Sprintf("parameter %q in %s is invalid: %v is %s", e.Parameter.Name, e.Parameter.In, rootErr.Value, rootErr.Reason), } - } else { - cErr = &ValidationError{ - Status: http.StatusBadRequest, - Title: innerErr.Reason, - } + } + return &ValidationError{ + Status: http.StatusBadRequest, + Title: innerErr.Reason, } } - return cErr + return nil } -var propertyMissingNameRE = regexp.MustCompile(`Property '(?P[^']*)' is missing`) - func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *ValidationError { cErr := &ValidationError{Title: innerErr.Reason} @@ -151,34 +134,31 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida if e.Parameter != nil { // We have a JSONPointer in the query param too so need to // make sure 'Parameter' check takes priority over 'Pointer' - cErr.Source = &ValidationErrorSource{ - Parameter: e.Parameter.Name, - } - } else if innerErr.JSONPointer() != nil { - pointer := innerErr.JSONPointer() - - cErr.Source = &ValidationErrorSource{ - Pointer: toJSONPointer(pointer), - } + cErr.Source = &ValidationErrorSource{Parameter: e.Parameter.Name} + } else if ptr := innerErr.JSONPointer(); ptr != nil { + cErr.Source = &ValidationErrorSource{Pointer: toJSONPointer(ptr)} } // Add details on allowed values for enums - if innerErr.SchemaField == "enum" && - innerErr.Reason == "JSON value is not one of the allowed values" { + if innerErr.SchemaField == "enum" { enums := make([]string, 0, len(innerErr.Schema.Enum)) for _, enum := range innerErr.Schema.Enum { enums = append(enums, fmt.Sprintf("%v", enum)) } - cErr.Detail = fmt.Sprintf("Value '%v' at %s must be one of: %s", - innerErr.Value, toJSONPointer(innerErr.JSONPointer()), strings.Join(enums, ", ")) + cErr.Detail = fmt.Sprintf("value %v at %s must be one of: %s", + innerErr.Value, + toJSONPointer(innerErr.JSONPointer()), + strings.Join(enums, ", ")) value := fmt.Sprintf("%v", innerErr.Value) if e.Parameter != nil && (e.Parameter.Explode == nil || *e.Parameter.Explode == true) && (e.Parameter.Style == "" || e.Parameter.Style == "form") && strings.Contains(value, ",") { parts := strings.Split(value, ",") - cErr.Detail = cErr.Detail + "; " + fmt.Sprintf("perhaps you intended '?%s=%s'", - e.Parameter.Name, strings.Join(parts, "&"+e.Parameter.Name+"=")) + cErr.Detail = fmt.Sprintf("%s; perhaps you intended '?%s=%s'", + cErr.Detail, + e.Parameter.Name, + strings.Join(parts, "&"+e.Parameter.Name+"=")) } } return cErr diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 0a28ab6a3..f6e5de21f 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -19,7 +19,7 @@ func newPetstoreRequest(t *testing.T, method, path string, body io.Reader) *http pathPrefix := "v2" r, err := http.NewRequest(method, fmt.Sprintf("http://%s/%s%s", host, pathPrefix, path), body) require.NoError(t, err) - r.Header.Set("Content-Type", "application/json") + r.Header.Set(headerCT, "application/json") r.Header.Set("Authorization", "Bearer magicstring") r.Host = host return r @@ -63,16 +63,16 @@ func getValidationTests(t *testing.T) []*validationTest { missingBody2 := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(``)) noContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) - noContentType.Header.Del("Content-Type") + noContentType.Header.Del(headerCT) noContentTypeNeeded := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil) - noContentTypeNeeded.Header.Del("Content-Type") + noContentTypeNeeded.Header.Del(headerCT) unknownContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) - unknownContentType.Header.Set("Content-Type", "application/xml") + unknownContentType.Header.Set(headerCT, "application/xml") unsupportedContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) - unsupportedContentType.Header.Set("Content-Type", "text/plain") + unsupportedContentType.Header.Set(headerCT, "text/plain") unsupportedHeaderValue := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) unsupportedHeaderValue.Header.Set("x-environment", "watdis") @@ -87,45 +87,45 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: badHost, }, - wantErrReason: "Does not match any server", - wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: "Does not match any server"}, + wantErrReason: ErrPathNotFound.Error(), + wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: ErrPathNotFound.Error()}, }, { name: "error - unknown path", args: validationArgs{ r: badPath, }, - wantErrReason: "Path was not found", - wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: "Path was not found"}, + wantErrReason: ErrPathNotFound.Error(), + wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: ErrPathNotFound.Error()}, }, { name: "error - unknown method", args: validationArgs{ r: badMethod, }, - wantErrReason: "Path doesn't support the HTTP method", + wantErrReason: ErrMethodNotAllowed.Error(), // TODO: By HTTP spec, this should have an Allow header with what is allowed // but kin-openapi doesn't provide us the requested method or path, so impossible to provide details wantErrResponse: &ValidationError{Status: http.StatusMethodNotAllowed, - Title: "Path doesn't support the HTTP method"}, + Title: ErrMethodNotAllowed.Error()}, }, { name: "error - missing body on POST", args: validationArgs{ r: missingBody1, }, - wantErrBody: "Request body has an error: must have a value", + wantErrBody: "request body has an error: " + ErrInvalidRequired.Error(), wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "Request body has an error: must have a value"}, + Title: "request body has an error: " + ErrInvalidRequired.Error()}, }, { name: "error - empty body on POST", args: validationArgs{ r: missingBody2, }, - wantErrBody: "Request body has an error: must have a value", + wantErrBody: "request body has an error: " + ErrInvalidRequired.Error(), wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "Request body has an error: must have a value"}, + Title: "request body has an error: " + ErrInvalidRequired.Error()}, }, // @@ -137,9 +137,9 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: noContentType, }, - wantErrReason: "header 'Content-Type' has unexpected value: \"\"", + wantErrReason: prefixInvalidCT + ` ""`, wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType, - Title: "header 'Content-Type' is required"}, + Title: "header Content-Type is required"}, }, { name: "error - unknown content-type on POST", @@ -148,18 +148,18 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrReason: "failed to decode request body", wantErrParseKind: KindUnsupportedFormat, - wantErrParseReason: "unsupported content type \"application/xml\"", + wantErrParseReason: prefixUnsupportedCT + ` "application/xml"`, wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType, - Title: "unsupported content type \"application/xml\""}, + Title: prefixUnsupportedCT + ` "application/xml"`}, }, { name: "error - unsupported content-type on POST", args: validationArgs{ r: unsupportedContentType, }, - wantErrReason: "header 'Content-Type' has unexpected value: \"text/plain\"", + wantErrReason: prefixInvalidCT + ` "text/plain"`, wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType, - Title: "unsupported content type \"text/plain\""}, + Title: prefixUnsupportedCT + ` "text/plain"`}, }, { name: "success - no content-type header required on GET", @@ -179,9 +179,9 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrReason: "must have a value", + wantErrReason: ErrInvalidRequired.Error(), wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "Parameter 'status' in query is required"}, + Title: `parameter "status" in query is required`}, }, { name: "error - wrong query string parameter type", @@ -192,11 +192,11 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrParamIn: "query", // This is a nested ParseError. The outer error is a KindOther with no details. // So we'd need to look at the inner one which is a KindInvalidFormat. So just check the error body. - wantErrBody: "Parameter 'ids' in query has an error: path 1: value notAnInt: an invalid integer: " + + wantErrBody: `parameter "ids" in query has an error: path 1: value notAnInt: an invalid integer: ` + "strconv.ParseFloat: parsing \"notAnInt\": invalid syntax", // TODO: Should we treat query params of the wrong type like a 404 instead of a 400? wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "Parameter 'ids' in query is invalid: notAnInt is an invalid integer"}, + Title: `parameter "ids" in query is invalid: notAnInt is an invalid integer`}, }, { name: "success - ignores unknown query string parameter", @@ -223,12 +223,12 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "JSON value is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaPath: "/0", wantErrSchemaValue: "available,sold", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "JSON value is not one of the allowed values", - Detail: "Value 'available,sold' at /0 must be one of: available, pending, sold; " + + Title: "value is not one of the allowed values", + Detail: "value available,sold at /0 must be one of: available, pending, sold; " + // TODO: do we really want to use this heuristic to guess // that they're using the wrong serialization? "perhaps you intended '?status=available&status=sold'", @@ -241,12 +241,12 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "JSON value is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaPath: "/1", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "JSON value is not one of the allowed values", - Detail: "Value 'watdis' at /1 must be one of: available, pending, sold", + Title: "value is not one of the allowed values", + Detail: "value watdis at /1 must be one of: available, pending, sold", Source: &ValidationErrorSource{Parameter: "status"}}, }, { @@ -257,12 +257,12 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "kind", wantErrParamIn: "query", - wantErrSchemaReason: "JSON value is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaPath: "/1", wantErrSchemaValue: "fish,with,commas", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "JSON value is not one of the allowed values", - Detail: "Value 'fish,with,commas' at /1 must be one of: dog, cat, turtle, bird,with,commas", + Title: "value is not one of the allowed values", + Detail: "value fish,with,commas at /1 must be one of: dog, cat, turtle, bird,with,commas", // No 'perhaps you intended' because its the right serialization format Source: &ValidationErrorSource{Parameter: "kind"}}, }, @@ -283,12 +283,12 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "x-environment", wantErrParamIn: "header", - wantErrSchemaReason: "JSON value is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaPath: "/", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "JSON value is not one of the allowed values", - Detail: "Value 'watdis' at / must be one of: demo, prod", + Title: "value is not one of the allowed values", + Detail: "value watdis at / must be one of: demo, prod", Source: &ValidationErrorSource{Parameter: "x-environment"}}, }, @@ -302,12 +302,12 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), }, wantErrReason: "doesn't match the schema", - wantErrSchemaReason: "JSON value is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaValue: "watdis", wantErrSchemaPath: "/status", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "JSON value is not one of the allowed values", - Detail: "Value 'watdis' at /status must be one of: available, pending, sold", + Title: "value is not one of the allowed values", + Detail: "value watdis at /status must be one of: available, pending, sold", Source: &ValidationErrorSource{Pointer: "/status"}}, }, { @@ -316,11 +316,11 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama"}`)), }, wantErrReason: "doesn't match the schema", - wantErrSchemaReason: "Property 'photoUrls' is missing", + wantErrSchemaReason: `property "photoUrls" is missing`, wantErrSchemaValue: map[string]string{"name": "Bahama"}, wantErrSchemaPath: "/photoUrls", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Property 'photoUrls' is missing", + Title: `property "photoUrls" is missing`, Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { @@ -330,11 +330,11 @@ func getValidationTests(t *testing.T) []*validationTest { bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{}}`)), }, wantErrReason: "doesn't match the schema", - wantErrSchemaReason: "Property 'name' is missing", + wantErrSchemaReason: `property "name" is missing`, wantErrSchemaValue: map[string]string{}, wantErrSchemaPath: "/category/name", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Property 'name' is missing", + Title: `property "name" is missing`, Source: &ValidationErrorSource{Pointer: "/category/name"}}, }, { @@ -344,11 +344,11 @@ func getValidationTests(t *testing.T) []*validationTest { bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{"tags": [{}]}}`)), }, wantErrReason: "doesn't match the schema", - wantErrSchemaReason: "Property 'name' is missing", + wantErrSchemaReason: `property "name" is missing`, wantErrSchemaValue: map[string]string{}, wantErrSchemaPath: "/category/tags/0/name", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Property 'name' is missing", + Title: `property "name" is missing`, Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"}}, }, { @@ -384,11 +384,11 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrReason: "doesn't match the schema", wantErrSchemaPath: "/", wantErrSchemaValue: map[string]string{"name": "Bahama"}, - wantErrSchemaOriginReason: "Property 'photoUrls' is missing", + wantErrSchemaOriginReason: `property "photoUrls" is missing`, wantErrSchemaOriginValue: map[string]string{"name": "Bahama"}, wantErrSchemaOriginPath: "/photoUrls", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Property 'photoUrls' is missing", + Title: `property "photoUrls" is missing`, Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { @@ -423,9 +423,9 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "petId", wantErrParamIn: "path", - wantErrReason: "must have a value", + wantErrReason: ErrInvalidRequired.Error(), wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "Parameter 'petId' in path is required"}, + Title: `parameter "petId" in path is required`}, }, { name: "error - wrong path param type", @@ -438,7 +438,7 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrParseValue: "NotAnInt", wantErrParseReason: "an invalid integer", wantErrResponse: &ValidationError{Status: http.StatusNotFound, - Title: "Resource not found with 'petId' value: NotAnInt"}, + Title: `resource not found with "petId" value: NotAnInt`}, }, { name: "success - normal case, with path params", diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index 4de536ef1..10d7a3843 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -103,7 +103,7 @@ func TestFilter(t *testing.T) { ).NewRef(), }, }, - // TODO(decode not): handle decoding "not" JSON Schema + // TODO(decode not): handle decoding "not" Schema // { // Value: &openapi3.Parameter{ // In: "query", @@ -154,11 +154,13 @@ func TestFilter(t *testing.T) { }, } + err := swagger.Validate(context.Background()) + require.NoError(t, err) router := NewRouter().WithSwagger(swagger) expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder ContentParameterDecoder) error { t.Logf("Request: %s %s", req.Method, req.URL) httpReq, _ := http.NewRequest(req.Method, req.URL, marshalReader(req.Body)) - httpReq.Header.Set("Content-Type", req.ContentType) + httpReq.Header.Set(headerCT, req.ContentType) // Find route route, pathParams, err := router.FindRoute(httpReq.Method, httpReq.URL) @@ -179,7 +181,7 @@ func TestFilter(t *testing.T) { RequestValidationInput: requestValidationInput, Status: resp.Status, Header: http.Header{ - "Content-Type": []string{ + headerCT: []string{ resp.ContentType, }, }, @@ -197,15 +199,12 @@ func TestFilter(t *testing.T) { return expectWithDecoder(req, resp, nil) } - var err error - var req ExampleRequest - var resp ExampleResponse - resp = ExampleResponse{ + resp := ExampleResponse{ Status: 200, } // Test paths - req = ExampleRequest{ + req := ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix", } @@ -286,7 +285,7 @@ func TestFilter(t *testing.T) { err = expect(req, resp) require.IsType(t, &RequestError{}, err) - // TODO(decode not): handle decoding "not" JSON Schema + // TODO(decode not): handle decoding "not" Schema // req = ExampleRequest{ // Method: "POST", // URL: "http://example.com/api/prefix/v/suffix?queryArgNot=abdfg", @@ -294,7 +293,7 @@ func TestFilter(t *testing.T) { // err = expect(req, resp) // require.IsType(t, &RequestError{}, err) - // TODO(decode not): handle decoding "not" JSON Schema + // TODO(decode not): handle decoding "not" Schema // req = ExampleRequest{ // Method: "POST", // URL: "http://example.com/api/prefix/v/suffix?queryArgNot=123", @@ -433,7 +432,7 @@ func TestValidateRequestBody(t *testing.T) { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/test", tc.data) if tc.mime != "" { - req.Header.Set(http.CanonicalHeaderKey("Content-Type"), tc.mime) + req.Header.Set(headerCT, tc.mime) } inp := &RequestValidationInput{Request: req} err := ValidateRequestBody(context.Background(), inp, tc.body) @@ -570,6 +569,8 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { } } + err := swagger.Validate(context.Background()) + require.NoError(t, err) // Declare the router router := NewRouter().WithSwagger(swagger) @@ -701,6 +702,8 @@ func TestAnySecurityRequirementMet(t *testing.T) { } } + err := swagger.Validate(context.Background()) + require.NoError(t, err) // Create the router router := NewRouter().WithSwagger(&swagger) @@ -801,6 +804,8 @@ func TestAllSchemesMet(t *testing.T) { } } + err := swagger.Validate(context.Background()) + require.NoError(t, err) // Create the router from the swagger router := NewRouter().WithSwagger(&swagger) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 4cf022e52..f7041334d 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -15,9 +15,7 @@ import ( // CycleError indicates that a type graph has one or more possible cycles. type CycleError struct{} -func (err *CycleError) Error() string { - return "Detected JSON cycle" -} +func (err *CycleError) Error() string { return "detected cycle" } // Option allows tweaking SchemaRef generation type Option func(*generatorOpt) diff --git a/pathpattern/node.go b/pathpattern/node.go index 43e2959d4..862199864 100644 --- a/pathpattern/node.go +++ b/pathpattern/node.go @@ -212,7 +212,7 @@ loop: // Find variable name i := strings.IndexByte(remaining, '}') if i < 0 { - return nil, fmt.Errorf("Missing '}' in: %s", path) + return nil, fmt.Errorf("missing '}' in: %s", path) } variableName := strings.TrimSpace(remaining[1:i]) remaining = remaining[i+1:] @@ -247,7 +247,7 @@ loop: if suffix.Kind == SuffixKindRegExp { regExp, err := regexp.Compile(suffix.Pattern) if err != nil { - return nil, fmt.Errorf("Invalid regular expression in: %s", path) + return nil, fmt.Errorf("invalid regular expression in: %s", path) } suffix.regExp = regExp } diff --git a/pathpattern/node_test.go b/pathpattern/node_test.go index 6d0ec9d92..f4842397c 100644 --- a/pathpattern/node_test.go +++ b/pathpattern/node_test.go @@ -36,11 +36,11 @@ func TestPatterns(t *testing.T) { } } if actually != expected { - t.Fatalf("Wrong path!\nInput: %s\nExpected: '%s'\nActually: '%s'\nTree:\n%s\n\n", uri, expected, actually, rootNode.String()) + t.Fatalf("Wrong path!\nInput: %s\nExpected: %q\nActually: %q\nTree:\n%s\n\n", uri, expected, actually, rootNode.String()) return } if !argsEqual(expectedArgs, actualArgs) { - t.Fatalf("Wrong variable values!\nInput: %s\nExpected: '%s'\nActually: '%s'\nTree:\n%s\n\n", uri, expectedArgs, actualArgs, rootNode.String()) + t.Fatalf("Wrong variable values!\nInput: %s\nExpected: %q\nActually: %q\nTree:\n%s\n\n", uri, expectedArgs, actualArgs, rootNode.String()) return } } From 3794d148eb07a8562934cd7d4d3ace10dcc37fbd Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 18 Mar 2021 18:14:53 +0100 Subject: [PATCH 053/260] go:embed loader.ReadFromURIFunc example (#319) --- go.mod | 2 +- openapi3/load_with_go_embed_test.go | 32 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 openapi3/load_with_go_embed_test.go diff --git a/go.mod b/go.mod index 8ccc13917..5856146cf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/getkin/kin-openapi -go 1.14 +go 1.16 require ( github.com/ghodss/yaml v1.0.0 diff --git a/openapi3/load_with_go_embed_test.go b/openapi3/load_with_go_embed_test.go new file mode 100644 index 000000000..67ecfea0a --- /dev/null +++ b/openapi3/load_with_go_embed_test.go @@ -0,0 +1,32 @@ +package openapi3_test + +import ( + "embed" + "fmt" + "net/url" + + "github.com/getkin/kin-openapi/openapi3" +) + +//go:embed testdata/recursiveRef/* +var fs embed.FS + +func Example() { + loader := openapi3.NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.SwaggerLoader, uri *url.URL) ([]byte, error) { + return fs.ReadFile(uri.Path) + } + + doc, err := loader.LoadSwaggerFromFile("testdata/recursiveRef/openapi.yml") + if err != nil { + panic(err) + } + + if err = doc.Validate(loader.Context); err != nil { + panic(err) + } + + fmt.Println(doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo"].Value.Properties["bar"].Value.Type) + // Output: array +} From 3690b664fe9e5aa8e7e5de77833da7c32d08b6fa Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 19 Mar 2021 17:06:33 +0100 Subject: [PATCH 054/260] Rework router (#210) --- README.md | 29 +-- go.mod | 1 + go.sum | 2 + openapi3/server.go | 15 +- openapi3/swagger_loader_test.go | 35 ++- openapi3filter/errors.go | 9 - openapi3filter/req_resp_decoder_test.go | 7 +- openapi3filter/router.go | 226 ------------------ openapi3filter/validate_readonly_test.go | 6 +- openapi3filter/validate_request_input.go | 3 +- openapi3filter/validate_request_test.go | 8 +- .../validation_discriminator_test.go | 32 +-- openapi3filter/validation_error_encoder.go | 9 +- openapi3filter/validation_error_test.go | 19 +- openapi3filter/validation_handler.go | 20 +- openapi3filter/validation_test.go | 78 +++--- routers/gorillamux/router.go | 153 ++++++++++++ .../gorillamux}/router_test.go | 50 ++-- .../legacy/pathpattern}/node.go | 0 .../legacy/pathpattern}/node_test.go | 18 +- routers/legacy/router.go | 164 +++++++++++++ routers/legacy/router_test.go | 195 +++++++++++++++ routers/types.go | 35 +++ 23 files changed, 745 insertions(+), 369 deletions(-) delete mode 100644 openapi3filter/router.go create mode 100644 routers/gorillamux/router.go rename {openapi3filter => routers/gorillamux}/router_test.go (81%) rename {pathpattern => routers/legacy/pathpattern}/node.go (100%) rename {pathpattern => routers/legacy/pathpattern}/node_test.go (79%) create mode 100644 routers/legacy/router.go create mode 100644 routers/legacy/router_test.go create mode 100644 routers/types.go diff --git a/README.md b/README.md index 8d9484dcc..c46f64616 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,9 @@ Here's some projects that depend on _kin-openapi_: * Support for OpenAPI 3 files, including serialization, deserialization, and validation. * _openapi3filter_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi3filter)) * Validates HTTP requests and responses + * Provides a [gorilla/mux](https://github.com/gorilla/mux) router for OpenAPI operations * _openapi3gen_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi3gen)) * Generates `*openapi3.Schema` values for Go types. - * _pathpattern_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/pathpattern)) - * Matches strings with OpenAPI path patterns ("/path/{parameter}") # Some recipes ## Loading OpenAPI document @@ -51,19 +50,12 @@ swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("swagger.json") ## Getting OpenAPI operation that matches request ```go -func GetOperation(httpRequest *http.Request) (*openapi3.Operation, error) { - // Load Swagger file - router := openapi3filter.NewRouter().WithSwaggerFromFile("swagger.json") - - // Find route - route, _, err := router.FindRoute("GET", req.URL) - if err != nil { - return nil, err - } - - // Get OpenAPI 3 operation - return route.Operation -} +loader := openapi3.NewSwaggerLoader() +spec, _ := loader.LoadSwaggerFromData([]byte(`...`)) +_ := spec.Validate(loader.Context) +router, _ := openapi3filter.NewRouter(spec) +route, pathParams, _ := router.FindRoute(httpRequest) +// Do something with route.Operation ``` ## Validating HTTP requests/responses @@ -81,12 +73,15 @@ import ( ) func main() { - router := openapi3filter.NewRouter().WithSwaggerFromFile("swagger.json") ctx := context.Background() + loader := &openapi3.SwaggerLoader{Context: ctx} + spec, _ := loader.LoadSwaggerFromFile("openapi3_spec.json") + _ := spec.Validate(ctx) + router, _ := openapi3filter.NewRouter(spec) httpReq, _ := http.NewRequest(http.MethodGet, "/items", nil) // Find route - route, pathParams, _ := router.FindRoute(httpReq.Method, httpReq.URL) + route, pathParams, _ := router.FindRoute(httpReq) // Validate request requestValidationInput := &openapi3filter.RequestValidationInput{ diff --git a/go.mod b/go.mod index 5856146cf..6da959250 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/ghodss/yaml v1.0.0 github.com/go-openapi/jsonpointer v0.19.5 + github.com/gorilla/mux v1.8.0 github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index f1e462c68..2b289d716 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/openapi3/server.go b/openapi3/server.go index 56dd1d8ed..682cb4d6f 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -3,6 +3,7 @@ package openapi3 import ( "context" "errors" + "fmt" "math" "net/url" "strings" @@ -128,7 +129,17 @@ func (server *Server) Validate(c context.Context) (err error) { if server.URL == "" { return errors.New("value of url must be a non-empty string") } - for _, v := range server.Variables { + opening, closing := strings.Count(server.URL, "{"), strings.Count(server.URL, "}") + if opening != closing { + return errors.New("server URL has mismatched { and }") + } + if opening != len(server.Variables) { + return errors.New("server has undeclared variables") + } + for name, v := range server.Variables { + if !strings.Contains(server.URL, fmt.Sprintf("{%s}", name)) { + return errors.New("server has undeclared variables") + } if err = v.Validate(c); err != nil { return } @@ -154,7 +165,7 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { func (serverVariable *ServerVariable) Validate(c context.Context) error { switch serverVariable.Default.(type) { - case float64, string: + case float64, string, nil: default: return errors.New("value of default must be either a number or a string") } diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go index 898ae9fe4..71c176377 100644 --- a/openapi3/swagger_loader_test.go +++ b/openapi3/swagger_loader_test.go @@ -1,11 +1,13 @@ package openapi3 import ( + "errors" "fmt" "net" "net/http" "net/http/httptest" "net/url" + "strings" "testing" "github.com/stretchr/testify/require" @@ -82,8 +84,8 @@ func TestResolveSchemaRef(t *testing.T) { doc, err := loader.LoadSwaggerFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) - require.NoError(t, err) + refAVisited := doc.Components.Schemas["A"].Value.AllOf[0] require.Equal(t, "#/components/schemas/B", refAVisited.Ref) require.NotNil(t, refAVisited.Value) @@ -267,7 +269,6 @@ func TestLoadFileWithExternalSchemaRef(t *testing.T) { loader.IsExternalRefsAllowed = true swagger, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.json") require.NoError(t, err) - require.NotNil(t, swagger.Components.Schemas["AnotherTestSchema"].Value.Type) } @@ -497,3 +498,33 @@ paths: err = doc.Validate(loader.Context) require.NoError(t, err) } + +func TestServersVariables(t *testing.T) { + const spec = ` +openapi: 3.0.1 +info: + title: My API + version: 1.0.0 +paths: {} +servers: +- @@@ +` + for value, expected := range map[string]error{ + `{url: /}`: nil, + `{url: "http://{x}.{y}.example.com"}`: errors.New("invalid servers: server has undeclared variables"), + `{url: "http://{x}.y}.example.com"}`: errors.New("invalid servers: server URL has mismatched { and }"), + `{url: "http://{x.example.com"}`: errors.New("invalid servers: server URL has mismatched { and }"), + `{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`: nil, + `{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`: nil, + `{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), + `{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), + } { + t.Run(value, func(t *testing.T) { + loader := NewSwaggerLoader() + doc, err := loader.LoadSwaggerFromData([]byte(strings.Replace(spec, "@@@", value, 1))) + require.NoError(t, err) + err = doc.Validate(loader.Context) + require.Equal(t, expected, err) + }) + } +} diff --git a/openapi3filter/errors.go b/openapi3filter/errors.go index f4c9f0b79..ec8ea053c 100644 --- a/openapi3filter/errors.go +++ b/openapi3filter/errors.go @@ -6,15 +6,6 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -type RouteError struct { - Route Route - Reason string -} - -func (err *RouteError) Error() string { - return err.Reason -} - var _ error = &RequestError{} // RequestError is returned by ValidateRequest when request does not match OpenAPI spec diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 04ecd1693..b6d603e2b 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/getkin/kin-openapi/openapi3" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) @@ -922,10 +923,10 @@ func TestDecodeParameter(t *testing.T) { spec.AddOperation(path, http.MethodGet, op) err = spec.Validate(context.Background()) require.NoError(t, err) - router := NewRouter() - require.NoError(t, router.AddSwagger(spec)) + router, err := legacyrouter.NewRouter(spec) + require.NoError(t, err) - route, pathParams, err := router.FindRoute(req.Method, req.URL) + route, pathParams, err := router.FindRoute(req) require.NoError(t, err) input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route} diff --git a/openapi3filter/router.go b/openapi3filter/router.go deleted file mode 100644 index 3c7690d58..000000000 --- a/openapi3filter/router.go +++ /dev/null @@ -1,226 +0,0 @@ -package openapi3filter - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/pathpattern" -) - -// ErrPathNotFound is returned when no route match is found -var ErrPathNotFound = errors.New("no matching operation was found") - -// ErrMethodNotAllowed is returned when no method of the matched route matches -var ErrMethodNotAllowed = errors.New("method not allowed") - -// Route describes the operation an http.Request can match -type Route struct { - Swagger *openapi3.Swagger - Server *openapi3.Server - Path string - PathItem *openapi3.PathItem - Method string - Operation *openapi3.Operation - - // For developers who want use the router for handling too - Handler http.Handler -} - -// Routers maps a HTTP request to a Router. -type Routers []*Router - -// FindRoute extracts the route and parameters of an http.Request -func (routers Routers) FindRoute(method string, url *url.URL) (*Router, *Route, map[string]string, error) { - for _, router := range routers { - // Skip routers that have DO NOT have servers - if len(router.swagger.Servers) == 0 { - continue - } - route, pathParams, err := router.FindRoute(method, url) - if err == nil { - return router, route, pathParams, nil - } - } - for _, router := range routers { - // Skip routers that DO have servers - if len(router.swagger.Servers) > 0 { - continue - } - route, pathParams, err := router.FindRoute(method, url) - if err == nil { - return router, route, pathParams, nil - } - } - return nil, nil, nil, &RouteError{ - Reason: "None of the routers matches", - } -} - -// Router maps a HTTP request to an OpenAPI operation. -type Router struct { - swagger *openapi3.Swagger - pathNode *pathpattern.Node -} - -// NewRouter creates a new router. -// -// If the given Swagger has servers, router will use them. -// All operations of the Swagger will be added to the router. -func NewRouter() *Router { - return &Router{} -} - -// WithSwaggerFromFile loads the Swagger file and adds it using WithSwagger. -// Panics on any error. -func (router *Router) WithSwaggerFromFile(path string) *Router { - if err := router.AddSwaggerFromFile(path); err != nil { - panic(err) - } - return router -} - -// WithSwagger adds all operations in the OpenAPI specification. -// Panics on any error. -func (router *Router) WithSwagger(swagger *openapi3.Swagger) *Router { - if err := router.AddSwagger(swagger); err != nil { - panic(err) - } - return router -} - -// AddSwaggerFromFile loads the Swagger file and adds it using AddSwagger. -func (router *Router) AddSwaggerFromFile(path string) error { - swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile(path) - if err != nil { - return err - } - return router.AddSwagger(swagger) -} - -// AddSwagger adds all operations in the OpenAPI specification. -func (router *Router) AddSwagger(swagger *openapi3.Swagger) error { - if err := swagger.Validate(context.TODO()); err != nil { - return fmt.Errorf("validating OpenAPI failed: %v", err) - } - router.swagger = swagger - root := router.node() - for path, pathItem := range swagger.Paths { - for method, operation := range pathItem.Operations() { - method = strings.ToUpper(method) - if err := root.Add(method+" "+path, &Route{ - Swagger: swagger, - Path: path, - PathItem: pathItem, - Method: method, - Operation: operation, - }, nil); err != nil { - return err - } - } - } - return nil -} - -// AddRoute adds a route in the router. -func (router *Router) AddRoute(route *Route) error { - method := route.Method - if method == "" { - return errors.New("route is missing method") - } - method = strings.ToUpper(method) - path := route.Path - if path == "" { - return errors.New("route is missing path") - } - return router.node().Add(method+" "+path, router, nil) -} - -func (router *Router) node() *pathpattern.Node { - root := router.pathNode - if root == nil { - root = &pathpattern.Node{} - router.pathNode = root - } - return root -} - -// FindRoute extracts the route and parameters of an http.Request -func (router *Router) FindRoute(method string, url *url.URL) (*Route, map[string]string, error) { - swagger := router.swagger - - // Get server - servers := swagger.Servers - var server *openapi3.Server - var remainingPath string - var pathParams map[string]string - if len(servers) == 0 { - remainingPath = url.Path - } else { - var paramValues []string - server, paramValues, remainingPath = servers.MatchURL(url) - if server == nil { - return nil, nil, &RouteError{ - Route: Route{ - Swagger: swagger, - }, - Reason: ErrPathNotFound.Error(), - } - } - pathParams = make(map[string]string, 8) - paramNames, _ := server.ParameterNames() - for i, value := range paramValues { - name := paramNames[i] - pathParams[name] = value - } - } - - // Get PathItem - root := router.node() - var route *Route - node, paramValues := root.Match(method + " " + remainingPath) - if node != nil { - route, _ = node.Value.(*Route) - } - if route == nil { - pathItem := swagger.Paths[remainingPath] - if pathItem == nil { - return nil, nil, &RouteError{ - Route: Route{ - Swagger: swagger, - Server: server, - }, - Reason: ErrPathNotFound.Error(), - } - } - - // Get operation - if pathItem.GetOperation(method) == nil { - return nil, nil, &RouteError{ - Route: Route{ - Swagger: swagger, - Server: server, - }, - Reason: ErrMethodNotAllowed.Error(), - } - } - - } - - if pathParams == nil { - pathParams = make(map[string]string, len(paramValues)) - } - paramKeys := node.VariableNames - for i, value := range paramValues { - key := paramKeys[i] - if strings.HasSuffix(key, "*") { - key = key[:len(key)-1] - } - pathParams[key] = value - } - return route, pathParams, nil -} diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go index 6812c6971..cefadf77e 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/getkin/kin-openapi/openapi3" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) @@ -69,7 +70,8 @@ func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { require.NoError(t, err) err = doc.Validate(sl.Context) require.NoError(t, err) - router := NewRouter().WithSwagger(doc) + router, err := legacyrouter.NewRouter(doc) + require.NoError(t, err) b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) require.NoError(t, err) @@ -78,7 +80,7 @@ func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { require.NoError(t, err) httpReq.Header.Add(headerCT, "application/json") - route, pathParams, err := router.FindRoute(httpReq.Method, httpReq.URL) + route, pathParams, err := router.FindRoute(httpReq) require.NoError(t, err) err = ValidateRequest(sl.Context, &RequestValidationInput{ diff --git a/openapi3filter/validate_request_input.go b/openapi3filter/validate_request_input.go index 44bc8579a..14c661bbb 100644 --- a/openapi3filter/validate_request_input.go +++ b/openapi3filter/validate_request_input.go @@ -5,6 +5,7 @@ import ( "net/url" "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" ) // A ContentParameterDecoder takes a parameter definition from the swagger spec, @@ -22,7 +23,7 @@ type RequestValidationInput struct { Request *http.Request PathParams map[string]string QueryParams url.Values - Route *Route + Route *routers.Route Options *Options ParamDecoder ContentParameterDecoder } diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index e18d0f455..2c1c0b5cc 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -8,6 +8,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) const spec = ` @@ -72,7 +73,10 @@ func Example() { panic(err) } - router := openapi3filter.NewRouter().WithSwagger(doc) + router, err := legacyrouter.NewRouter(doc) + if err != nil { + panic(err) + } p, err := json.Marshal(map[string]interface{}{ "pet_type": "Cat", @@ -89,7 +93,7 @@ func Example() { } req.Header.Set("Content-Type", "application/json") - route, pathParams, err := router.FindRoute(req.Method, req.URL) + route, pathParams, err := router.FindRoute(req) if err != nil { panic(err) } diff --git a/openapi3filter/validation_discriminator_test.go b/openapi3filter/validation_discriminator_test.go index 792944719..a3eb8a8c7 100644 --- a/openapi3filter/validation_discriminator_test.go +++ b/openapi3filter/validation_discriminator_test.go @@ -2,15 +2,16 @@ package openapi3filter import ( "bytes" - "context" "net/http" "testing" "github.com/getkin/kin-openapi/openapi3" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) -var yaJsonSpecWithDiscriminator = []byte(` +func TestValidationWithDiscriminatorSelection(t *testing.T) { + const spec = ` openapi: 3.0.0 info: version: 0.2.0 @@ -72,27 +73,28 @@ components: properties: value: type: integer -`) +` + + loader := openapi3.NewSwaggerLoader() + doc, err := loader.LoadSwaggerFromData([]byte(spec)) + require.NoError(t, err) + + router, err := legacyrouter.NewRouter(doc) + require.NoError(t, err) -func forgeRequest(body string) *http.Request { - iobody := bytes.NewReader([]byte(body)) - req, _ := http.NewRequest("PUT", "/blob", iobody) + body := bytes.NewReader([]byte(`{"discr": "objA", "base64": "S25vY2sgS25vY2ssIE5lbyAuLi4="}`)) + req, err := http.NewRequest("PUT", "/blob", body) + require.NoError(t, err) req.Header.Add(headerCT, "application/json") - return req -} -func TestValidationWithDiscriminatorSelection(t *testing.T) { - openapi, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(yaJsonSpecWithDiscriminator) + route, pathParams, err := router.FindRoute(req) require.NoError(t, err) - router := NewRouter().WithSwagger(openapi) - req := forgeRequest(`{"discr": "objA", "base64": "S25vY2sgS25vY2ssIE5lbyAuLi4="}`) - route, pathParams, _ := router.FindRoute(req.Method, req.URL) + requestValidationInput := &RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, } - ctx := context.Background() - err = ValidateRequest(ctx, requestValidationInput) + err = ValidateRequest(loader.Context, requestValidationInput) require.NoError(t, err) } diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 7c7ba5d6d..47f9cd9f0 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" ) // ValidationErrorEncoder wraps a base ErrorEncoder to handle ValidationErrors @@ -16,7 +17,7 @@ type ValidationErrorEncoder struct { // Encode implements the ErrorEncoder interface for encoding ValidationErrors func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) { - if e, ok := err.(*RouteError); ok { + if e, ok := err.(*routers.RouteError); ok { cErr := convertRouteError(e) enc.Encoder(ctx, cErr, w) return @@ -46,12 +47,12 @@ func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http enc.Encoder(ctx, err, w) } -func convertRouteError(e *RouteError) *ValidationError { +func convertRouteError(e *routers.RouteError) *ValidationError { status := http.StatusNotFound - if e.Reason == ErrMethodNotAllowed.Error() { + if e.Error() == routers.ErrMethodNotAllowed.Error() { status = http.StatusMethodNotAllowed } - return &ValidationError{Status: status, Title: e.Reason} + return &ValidationError{Status: status, Title: e.Error()} } func convertBasicRequestError(e *RequestError) *ValidationError { diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index f6e5de21f..d03fb7dbf 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" "github.com/stretchr/testify/require" ) @@ -87,27 +88,27 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: badHost, }, - wantErrReason: ErrPathNotFound.Error(), - wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: ErrPathNotFound.Error()}, + wantErrReason: routers.ErrPathNotFound.Error(), + wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: routers.ErrPathNotFound.Error()}, }, { name: "error - unknown path", args: validationArgs{ r: badPath, }, - wantErrReason: ErrPathNotFound.Error(), - wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: ErrPathNotFound.Error()}, + wantErrReason: routers.ErrPathNotFound.Error(), + wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: routers.ErrPathNotFound.Error()}, }, { name: "error - unknown method", args: validationArgs{ r: badMethod, }, - wantErrReason: ErrMethodNotAllowed.Error(), + wantErrReason: routers.ErrMethodNotAllowed.Error(), // TODO: By HTTP spec, this should have an Allow header with what is allowed // but kin-openapi doesn't provide us the requested method or path, so impossible to provide details wantErrResponse: &ValidationError{Status: http.StatusMethodNotAllowed, - Title: ErrMethodNotAllowed.Error()}, + Title: routers.ErrMethodNotAllowed.Error()}, }, { name: "error - missing body on POST", @@ -466,13 +467,13 @@ func TestValidationHandler_validateRequest(t *testing.T) { req.Equal(tt.wantErrBody, err.Error()) } - if e, ok := err.(*RouteError); ok { - req.Equal(tt.wantErrReason, e.Reason) + if e, ok := err.(*routers.RouteError); ok { + req.Equal(tt.wantErrReason, e.Error()) return } e, ok := err.(*RequestError) - req.True(ok, "error = %v, not a RequestError -- %#v", err, err) + req.True(ok, "not a RequestError: %T -- %#v", err, err) req.Equal(tt.wantErrReason, e.Reason) diff --git a/openapi3filter/validation_handler.go b/openapi3filter/validation_handler.go index 336187f49..111ece745 100644 --- a/openapi3filter/validation_handler.go +++ b/openapi3filter/validation_handler.go @@ -3,6 +3,10 @@ package openapi3filter import ( "context" "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) type AuthenticationFunc func(context.Context, *AuthenticationInput) error @@ -16,13 +20,19 @@ type ValidationHandler struct { AuthenticationFunc AuthenticationFunc SwaggerFile string ErrorEncoder ErrorEncoder - router *Router + router routers.Router } func (h *ValidationHandler) Load() error { - h.router = NewRouter() - - if err := h.router.AddSwaggerFromFile(h.SwaggerFile); err != nil { + loader := openapi3.NewSwaggerLoader() + doc, err := loader.LoadSwaggerFromFile(h.SwaggerFile) + if err != nil { + return err + } + if err := doc.Validate(loader.Context); err != nil { + return err + } + if h.router, err = legacyrouter.NewRouter(doc); err != nil { return err } @@ -69,7 +79,7 @@ func (h *ValidationHandler) before(w http.ResponseWriter, r *http.Request) (hand func (h *ValidationHandler) validateRequest(r *http.Request) error { // Find route - route, pathParams, err := h.router.FindRoute(r.Method, r.URL) + route, pathParams, err := h.router.FindRoute(r) if err != nil { return err } diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index 10d7a3843..9db76748a 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/getkin/kin-openapi/openapi3" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) @@ -44,7 +45,7 @@ func TestFilter(t *testing.T) { complexArgSchema.Required = []string{"name", "id"} // Declare router - swagger := &openapi3.Swagger{ + doc := &openapi3.Swagger{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -154,16 +155,18 @@ func TestFilter(t *testing.T) { }, } - err := swagger.Validate(context.Background()) + err := doc.Validate(context.Background()) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) - router := NewRouter().WithSwagger(swagger) expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder ContentParameterDecoder) error { t.Logf("Request: %s %s", req.Method, req.URL) - httpReq, _ := http.NewRequest(req.Method, req.URL, marshalReader(req.Body)) + httpReq, err := http.NewRequest(req.Method, req.URL, marshalReader(req.Body)) + require.NoError(t, err) httpReq.Header.Set(headerCT, req.ContentType) // Find route - route, pathParams, err := router.FindRoute(httpReq.Method, httpReq.URL) + route, pathParams, err := router.FindRoute(httpReq) require.NoError(t, err) // Validate request @@ -476,8 +479,7 @@ func toJSON(v interface{}) io.Reader { return bytes.NewReader(data) } -// TestOperationOrSwaggerSecurity asserts that the swagger's SecurityRequirements are used if no SecurityRequirements are provided for an operation. -func TestOperationOrSwaggerSecurity(t *testing.T) { +func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *testing.T) { // Create the security schemes securitySchemes := []ExampleSecurityScheme{ { @@ -526,8 +528,7 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { }, } - // Create the swagger - swagger := &openapi3.Swagger{ + doc := &openapi3.Swagger{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -546,12 +547,12 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { // Add the security schemes to the components for _, scheme := range securitySchemes { - swagger.Components.SecuritySchemes[scheme.Name] = &openapi3.SecuritySchemeRef{ + doc.Components.SecuritySchemes[scheme.Name] = &openapi3.SecuritySchemeRef{ Value: scheme.Scheme, } } - // Add the paths from the test cases to the swagger's paths + // Add the paths from the test cases to the spec's paths for _, tc := range tc { var securityRequirements *openapi3.SecurityRequirements = nil if tc.schemes != nil { @@ -561,7 +562,7 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { } securityRequirements = tempS } - swagger.Paths[tc.name] = &openapi3.PathItem{ + doc.Paths[tc.name] = &openapi3.PathItem{ Get: &openapi3.Operation{ Security: securityRequirements, Responses: openapi3.NewResponses(), @@ -569,10 +570,10 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { } } - err := swagger.Validate(context.Background()) + err := doc.Validate(context.Background()) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) - // Declare the router - router := NewRouter().WithSwagger(swagger) // Test each case for _, path := range tc { @@ -588,12 +589,11 @@ func TestOperationOrSwaggerSecurity(t *testing.T) { // Create the request emptyBody := bytes.NewReader(make([]byte, 0)) - pathURL, err := url.Parse(path.name) - require.NoError(t, err) - route, _, err := router.FindRoute(http.MethodGet, pathURL) + httpReq := httptest.NewRequest(http.MethodGet, path.name, emptyBody) + route, _, err := router.FindRoute(httpReq) require.NoError(t, err) req := RequestValidationInput{ - Request: httptest.NewRequest(http.MethodGet, path.name, emptyBody), + Request: httpReq, Route: route, Options: &Options{ AuthenticationFunc: func(c context.Context, input *AuthenticationInput) error { @@ -662,8 +662,7 @@ func TestAnySecurityRequirementMet(t *testing.T) { }, } - // Create the swagger - swagger := openapi3.Swagger{ + doc := openapi3.Swagger{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -675,9 +674,9 @@ func TestAnySecurityRequirementMet(t *testing.T) { }, } - // Add the security schemes to the swagger's components + // Add the security schemes to the spec's components for schemeName := range schemes { - swagger.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{ + doc.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{ Value: &openapi3.SecurityScheme{ Type: "http", Scheme: "basic", @@ -685,7 +684,7 @@ func TestAnySecurityRequirementMet(t *testing.T) { } } - // Add the paths to the swagger + // Add the paths to the spec for _, tc := range tc { // Create the security requirements from the test cases's schemes securityRequirements := openapi3.NewSecurityRequirements() @@ -694,7 +693,7 @@ func TestAnySecurityRequirementMet(t *testing.T) { } // Create the path with the security requirements - swagger.Paths[tc.name] = &openapi3.PathItem{ + doc.Paths[tc.name] = &openapi3.PathItem{ Get: &openapi3.Operation{ Security: securityRequirements, Responses: openapi3.NewResponses(), @@ -702,10 +701,10 @@ func TestAnySecurityRequirementMet(t *testing.T) { } } - err := swagger.Validate(context.Background()) + err := doc.Validate(context.Background()) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(&doc) require.NoError(t, err) - // Create the router - router := NewRouter().WithSwagger(&swagger) // Create the authentication function authFunc := makeAuthFunc(schemes) @@ -714,7 +713,8 @@ func TestAnySecurityRequirementMet(t *testing.T) { // Create the request input for the path tcURL, err := url.Parse(tc.name) require.NoError(t, err) - route, _, err := router.FindRoute(http.MethodGet, tcURL) + httpReq := httptest.NewRequest(http.MethodGet, tcURL.String(), nil) + route, _, err := router.FindRoute(httpReq) require.NoError(t, err) req := RequestValidationInput{ Route: route, @@ -759,8 +759,7 @@ func TestAllSchemesMet(t *testing.T) { }, } - // Create the swagger - swagger := openapi3.Swagger{ + doc := openapi3.Swagger{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -772,9 +771,9 @@ func TestAllSchemesMet(t *testing.T) { }, } - // Add the security schemes to the swagger's components + // Add the security schemes to the spec's components for schemeName := range schemes { - swagger.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{ + doc.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{ Value: &openapi3.SecurityScheme{ Type: "http", Scheme: "basic", @@ -782,7 +781,7 @@ func TestAllSchemesMet(t *testing.T) { } } - // Add the paths to the swagger + // Add the paths to the spec for _, tc := range tc { // Create the security requirement for the path securityRequirement := openapi3.SecurityRequirement{} @@ -794,7 +793,7 @@ func TestAllSchemesMet(t *testing.T) { } } - swagger.Paths[tc.name] = &openapi3.PathItem{ + doc.Paths[tc.name] = &openapi3.PathItem{ Get: &openapi3.Operation{ Security: &openapi3.SecurityRequirements{ securityRequirement, @@ -804,10 +803,10 @@ func TestAllSchemesMet(t *testing.T) { } } - err := swagger.Validate(context.Background()) + err := doc.Validate(context.Background()) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(&doc) require.NoError(t, err) - // Create the router from the swagger - router := NewRouter().WithSwagger(&swagger) // Create the authentication function authFunc := makeAuthFunc(schemes) @@ -816,7 +815,8 @@ func TestAllSchemesMet(t *testing.T) { // Create the request input for the path tcURL, err := url.Parse(tc.name) require.NoError(t, err) - route, _, err := router.FindRoute(http.MethodGet, tcURL) + httpReq := httptest.NewRequest(http.MethodGet, tcURL.String(), nil) + route, _, err := router.FindRoute(httpReq) require.NoError(t, err) req := RequestValidationInput{ Route: route, diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go new file mode 100644 index 000000000..7973398a7 --- /dev/null +++ b/routers/gorillamux/router.go @@ -0,0 +1,153 @@ +// Package gorillamux implements a router. +// +// It differs from the legacy router: +// * it provides somewhat granular errors: "path not found", "method not allowed". +// * it handles matching routes with extensions (e.g. /books/{id}.json) +// * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z:.*}) +package gorillamux + +import ( + "net/http" + "net/url" + "sort" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" + "github.com/gorilla/mux" +) + +// Router helps link http.Request.s and an OpenAPIv3 spec +type Router struct { + muxes []*mux.Route + routes []*routers.Route +} + +// NewRouter creates a gorilla/mux router. +// Assumes spec is .Validate()d +// TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request)) +func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { + type srv struct { + scheme, host, base string + server *openapi3.Server + } + servers := make([]srv, 0, len(doc.Servers)) + for _, server := range doc.Servers { + u, err := url.Parse(bEncode(server.URL)) + if err != nil { + return nil, err + } + path := bDecode(u.EscapedPath()) + if path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + servers = append(servers, srv{ + host: bDecode(u.Host), //u.Hostname()? + base: path, + scheme: bDecode(u.Scheme), + server: server, + }) + } + if len(servers) == 0 { + servers = append(servers, srv{}) + } + muxRouter := mux.NewRouter() /*.UseEncodedPath()?*/ + r := &Router{} + for _, path := range orderedPaths(doc.Paths) { + pathItem := doc.Paths[path] + + operations := pathItem.Operations() + methods := make([]string, 0, len(operations)) + for method := range operations { + methods = append(methods, method) + } + sort.Strings(methods) + + for _, s := range servers { + muxRoute := muxRouter.Path(s.base + path).Methods(methods...) + if scheme := s.scheme; scheme != "" { + muxRoute.Schemes(scheme) + } + if host := s.host; host != "" { + muxRoute.Host(host) + } + if err := muxRoute.GetError(); err != nil { + return nil, err + } + r.muxes = append(r.muxes, muxRoute) + r.routes = append(r.routes, &routers.Route{ + Swagger: doc, + Server: s.server, + Path: path, + PathItem: pathItem, + Method: "", + Operation: nil, + }) + } + } + return r, nil +} + +// FindRoute extracts the route and parameters of an http.Request +func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) { + for i, muxRoute := range r.muxes { + var match mux.RouteMatch + if muxRoute.Match(req, &match) { + if err := match.MatchErr; err != nil { + // What then? + } + route := r.routes[i] + route.Method = req.Method + route.Operation = route.Swagger.Paths[route.Path].GetOperation(route.Method) + return route, match.Vars, nil + } + switch match.MatchErr { + case nil: + case mux.ErrMethodMismatch: + return nil, nil, routers.ErrMethodNotAllowed + default: // What then? + } + } + return nil, nil, routers.ErrPathNotFound +} + +func orderedPaths(paths map[string]*openapi3.PathItem) []string { + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathsObject + // When matching URLs, concrete (non-templated) paths would be matched + // before their templated counterparts. + // NOTE: sorting by number of variables ASC then by lexicographical + // order seems to be a good heuristic. + vars := make(map[int][]string) + max := 0 + for path := range paths { + count := strings.Count(path, "}") + vars[count] = append(vars[count], path) + if count > max { + max = count + } + } + ordered := make([]string, 0, len(paths)) + for c := 0; c <= max; c++ { + if ps, ok := vars[c]; ok { + sort.Strings(ps) + for _, p := range ps { + ordered = append(ordered, p) + } + } + } + return ordered +} + +// Magic strings that temporarily replace "{}" so net/url.Parse() works +var blURL, brURL = strings.Repeat("-", 50), strings.Repeat("_", 50) + +func bEncode(s string) string { + s = strings.Replace(s, "{", blURL, -1) + s = strings.Replace(s, "}", brURL, -1) + return s +} +func bDecode(s string) string { + s = strings.Replace(s, blURL, "{", -1) + s = strings.Replace(s, brURL, "}", -1) + return s +} diff --git a/openapi3filter/router_test.go b/routers/gorillamux/router_test.go similarity index 81% rename from openapi3filter/router_test.go rename to routers/gorillamux/router_test.go index 6e8f7fd72..fd04d4ea9 100644 --- a/openapi3filter/router_test.go +++ b/routers/gorillamux/router_test.go @@ -1,4 +1,4 @@ -package openapi3filter +package gorillamux import ( "context" @@ -7,6 +7,7 @@ import ( "testing" "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" "github.com/stretchr/testify/require" ) @@ -23,7 +24,7 @@ func TestRouter(t *testing.T) { paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()} booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()} - swagger := &openapi3.Swagger{ + doc := &openapi3.Swagger{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -44,8 +45,7 @@ func TestRouter(t *testing.T) { "/onlyGET": &openapi3.PathItem{ Get: helloGET, }, - //TODO: use "/params/{x}/{y}/{z:.*}": &openapi3.PathItem{ when reworking https://github.com/getkin/kin-openapi/pull/210 - "/params/{x}/{y}/{z*}": &openapi3.PathItem{ + "/params/{x}/{y}/{z:.*}": &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")}, @@ -71,22 +71,22 @@ func TestRouter(t *testing.T) { }, } - expect := func(r *Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { + expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { req, err := http.NewRequest(method, uri, nil) require.NoError(t, err) - route, pathParams, err := r.FindRoute(req.Method, req.URL) + route, pathParams, err := r.FindRoute(req) if err != nil { if operation == nil { - pathItem := swagger.Paths[uri] + pathItem := doc.Paths[uri] if pathItem == nil { - if err.Error() != ErrPathNotFound.Error() { - t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, ErrPathNotFound, err) + 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) } return } if pathItem.GetOperation(method) == nil { - if err.Error() != ErrMethodNotAllowed.Error() { - t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, ErrMethodNotAllowed, err) + if err.Error() != routers.ErrMethodNotAllowed.Error() { + t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrMethodNotAllowed, err) } } } else { @@ -126,9 +126,10 @@ func TestRouter(t *testing.T) { } } - err := swagger.Validate(context.Background()) + err := doc.Validate(context.Background()) + require.NoError(t, err) + r, err := NewRouter(doc) require.NoError(t, err) - r := NewRouter().WithSwagger(swagger) expect(r, http.MethodGet, "/not_existing", nil, nil) expect(r, http.MethodDelete, "/hello", helloDELETE, nil) @@ -150,19 +151,22 @@ func TestRouter(t *testing.T) { expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{ "bookid": "War.and.Peace", }) - // TODO: fix https://github.com/getkin/kin-openapi/issues/129 - // expect(r, http.MethodPost, "/books/War.and.Peace.json", booksPOST, map[string]string{ - // "bookid2": "War.and.Peace", - // }) + expect(r, http.MethodPost, "/books/War.and.Peace.json", booksPOST, map[string]string{ + "bookid2": "War.and.Peace", + }) expect(r, http.MethodPost, "/partial", nil, nil) - swagger.Servers = []*openapi3.Server{ + doc.Servers = []*openapi3.Server{ {URL: "https://www.example.com/api/v1"}, - {URL: "https://{d0}.{d1}.com/api/v1/"}, + {URL: "https://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ + "d0": {Default: "www"}, + "d1": {Enum: []interface{}{"example"}}, + }}, } - err = swagger.Validate(context.Background()) + err = doc.Validate(context.Background()) + require.NoError(t, err) + r, err = NewRouter(doc) require.NoError(t, err) - r = NewRouter().WithSwagger(swagger) expect(r, http.MethodGet, "/hello", nil, nil) expect(r, http.MethodGet, "/api/v1/hello", nil, nil) expect(r, http.MethodGet, "www.example.com/api/v1/hello", nil, nil) @@ -180,8 +184,8 @@ func TestRouter(t *testing.T) { req, err := http.NewRequest(http.MethodDelete, uri, nil) require.NoError(t, err) require.NotNil(t, req) - route, pathParams, err := r.FindRoute(req.Method, req.URL) - require.EqualError(t, err, ErrMethodNotAllowed.Error()) + route, pathParams, err := r.FindRoute(req) + require.EqualError(t, err, routers.ErrMethodNotAllowed.Error()) require.Nil(t, route) require.Nil(t, pathParams) } diff --git a/pathpattern/node.go b/routers/legacy/pathpattern/node.go similarity index 100% rename from pathpattern/node.go rename to routers/legacy/pathpattern/node.go diff --git a/pathpattern/node_test.go b/routers/legacy/pathpattern/node_test.go similarity index 79% rename from pathpattern/node_test.go rename to routers/legacy/pathpattern/node_test.go index f4842397c..14d8457ce 100644 --- a/pathpattern/node_test.go +++ b/routers/legacy/pathpattern/node_test.go @@ -1,14 +1,12 @@ -package pathpattern_test +package pathpattern import ( "testing" - - "github.com/getkin/kin-openapi/pathpattern" ) func TestPatterns(t *testing.T) { - pathpattern.DefaultOptions.SupportRegExp = true - rootNode := &pathpattern.Node{} + DefaultOptions.SupportRegExp = true + rootNode := &Node{} add := func(path, value string) { rootNode.MustAdd(path, value, nil) } @@ -24,8 +22,8 @@ func TestPatterns(t *testing.T) { add("/root/{path*}", "DIRECTORY") add("/impossible_route", "IMPOSSIBLE") - add(pathpattern.PathFromHost("www.nike.com", true), "WWW-HOST") - add(pathpattern.PathFromHost("{other}.nike.com", true), "OTHER-HOST") + add(PathFromHost("www.nike.com", true), "WWW-HOST") + add(PathFromHost("{other}.nike.com", true), "OTHER-HOST") expect := func(uri string, expected string, expectedArgs ...string) { actually := "not found" @@ -65,9 +63,9 @@ func TestPatterns(t *testing.T) { expect("/root/", "DIRECTORY", "") expect("/root/a/b/c", "DIRECTORY", "a/b/c") - expect(pathpattern.PathFromHost("www.nike.com", true), "WWW-HOST") - expect(pathpattern.PathFromHost("example.nike.com", true), "OTHER-HOST", "example") - expect(pathpattern.PathFromHost("subdomain.example.nike.com", true), "not found") + expect(PathFromHost("www.nike.com", true), "WWW-HOST") + expect(PathFromHost("example.nike.com", true), "OTHER-HOST", "example") + expect(PathFromHost("subdomain.example.nike.com", true), "not found") } func argsEqual(a, b []string) bool { diff --git a/routers/legacy/router.go b/routers/legacy/router.go new file mode 100644 index 000000000..1cb1426b6 --- /dev/null +++ b/routers/legacy/router.go @@ -0,0 +1,164 @@ +// Package legacy implements a router. +// +// It differs from the gorilla/mux router: +// * it provides granular errors: "path not found", "method not allowed", "variable missing from path" +// * it does not handle matching routes with extensions (e.g. /books/{id}.json) +// * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z.*}) +package legacy + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/legacy/pathpattern" +) + +// Routers maps a HTTP request to a Router. +type Routers []*Router + +// FindRoute extracts the route and parameters of an http.Request +func (rs Routers) FindRoute(req *http.Request) (routers.Router, *routers.Route, map[string]string, error) { + for _, router := range rs { + // Skip routers that have DO NOT have servers + if len(router.doc.Servers) == 0 { + continue + } + route, pathParams, err := router.FindRoute(req) + if err == nil { + return router, route, pathParams, nil + } + } + for _, router := range rs { + // Skip routers that DO have servers + if len(router.doc.Servers) > 0 { + continue + } + route, pathParams, err := router.FindRoute(req) + if err == nil { + return router, route, pathParams, nil + } + } + return nil, nil, nil, &routers.RouteError{ + Reason: "none of the routers match", + } +} + +// Router maps a HTTP request to an OpenAPI operation. +type Router struct { + doc *openapi3.Swagger + pathNode *pathpattern.Node +} + +// NewRouter creates a new router. +// +// If the given Swagger has servers, router will use them. +// All operations of the Swagger will be added to the router. +func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { + if err := doc.Validate(context.Background()); err != nil { + return nil, fmt.Errorf("validating OpenAPI failed: %v", err) + } + router := &Router{doc: doc} + root := router.node() + for path, pathItem := range doc.Paths { + for method, operation := range pathItem.Operations() { + method = strings.ToUpper(method) + if err := root.Add(method+" "+path, &routers.Route{ + Swagger: doc, + Path: path, + PathItem: pathItem, + Method: method, + Operation: operation, + }, nil); err != nil { + return nil, err + } + } + } + return router, nil +} + +// AddRoute adds a route in the router. +func (router *Router) AddRoute(route *routers.Route) error { + method := route.Method + if method == "" { + return errors.New("route is missing method") + } + method = strings.ToUpper(method) + path := route.Path + if path == "" { + return errors.New("route is missing path") + } + return router.node().Add(method+" "+path, router, nil) +} + +func (router *Router) node() *pathpattern.Node { + root := router.pathNode + if root == nil { + root = &pathpattern.Node{} + router.pathNode = root + } + return root +} + +// FindRoute extracts the route and parameters of an http.Request +func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) { + method, url := req.Method, req.URL + doc := router.doc + + // Get server + servers := doc.Servers + var server *openapi3.Server + var remainingPath string + var pathParams map[string]string + if len(servers) == 0 { + remainingPath = url.Path + } else { + var paramValues []string + server, paramValues, remainingPath = servers.MatchURL(url) + if server == nil { + return nil, nil, &routers.RouteError{ + Reason: routers.ErrPathNotFound.Error(), + } + } + pathParams = make(map[string]string, 8) + paramNames, _ := server.ParameterNames() + for i, value := range paramValues { + name := paramNames[i] + pathParams[name] = value + } + } + + // Get PathItem + root := router.node() + var route *routers.Route + node, paramValues := root.Match(method + " " + remainingPath) + if node != nil { + route, _ = node.Value.(*routers.Route) + } + if route == nil { + pathItem := doc.Paths[remainingPath] + if pathItem == nil { + return nil, nil, &routers.RouteError{Reason: routers.ErrPathNotFound.Error()} + } + if pathItem.GetOperation(method) == nil { + return nil, nil, &routers.RouteError{Reason: routers.ErrMethodNotAllowed.Error()} + } + } + + if pathParams == nil { + pathParams = make(map[string]string, len(paramValues)) + } + paramKeys := node.VariableNames + for i, value := range paramValues { + key := paramKeys[i] + if strings.HasSuffix(key, "*") { + key = key[:len(key)-1] + } + pathParams[key] = value + } + return route, pathParams, nil +} diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go new file mode 100644 index 000000000..af0dfba28 --- /dev/null +++ b/routers/legacy/router_test.go @@ -0,0 +1,195 @@ +package legacy + +import ( + "context" + "net/http" + "sort" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" + "github.com/stretchr/testify/require" +) + +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()} + doc := &openapi3.Swagger{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "MyAPI", + Version: "0.1", + }, + Paths: openapi3.Paths{ + "/hello": &openapi3.PathItem{ + Connect: helloCONNECT, + Delete: helloDELETE, + Get: helloGET, + Head: helloHEAD, + Options: helloOPTIONS, + Patch: helloPATCH, + Post: helloPOST, + Put: helloPUT, + Trace: helloTRACE, + }, + "/onlyGET": &openapi3.PathItem{ + Get: helloGET, + }, + "/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{ + Get: paramsGET, + Parameters: openapi3.Parameters{ + &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, + }, + }, + "/books/{bookid2}.json": &openapi3.PathItem{ + Post: booksPOST, + Parameters: openapi3.Parameters{ + &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")}, + }, + }, + "/partial": &openapi3.PathItem{ + Get: partialGET, + }, + }, + } + + expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { + req, err := http.NewRequest(method, uri, nil) + require.NoError(t, err) + route, pathParams, err := r.FindRoute(req) + if err != nil { + if operation == nil { + pathItem := doc.Paths[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) + } + return + } + if pathItem.GetOperation(method) == nil { + if err.Error() != routers.ErrMethodNotAllowed.Error() { + t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrMethodNotAllowed, err) + } + } + } else { + t.Fatalf("'%s %s': should have returned an operation, but it returned an error: %v", method, uri, err) + } + } + if operation == nil && err == nil { + t.Fatalf("'%s %s': should have failed, but returned\nroute = %+v\npathParams = %+v", method, uri, route, pathParams) + } + if route == nil { + return + } + if route.Operation != operation { + t.Fatalf("'%s %s': Returned wrong operation (%v)", + method, uri, route.Operation) + } + if len(params) == 0 { + if len(pathParams) != 0 { + t.Fatalf("'%s %s': should return no path arguments, but found %+v", method, uri, pathParams) + } + } else { + names := make([]string, 0, len(params)) + for name := range params { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + expected := params[name] + actual, exists := pathParams[name] + if !exists { + t.Fatalf("'%s %s': path parameter %q should be %q, but it's not defined.", method, uri, name, expected) + } + if actual != expected { + t.Fatalf("'%s %s': path parameter %q should be %q, but it's %q", method, uri, name, expected, actual) + } + } + } + } + + err := doc.Validate(context.Background()) + require.NoError(t, err) + r, err := NewRouter(doc) + require.NoError(t, err) + + expect(r, http.MethodGet, "/not_existing", nil, nil) + expect(r, http.MethodDelete, "/hello", helloDELETE, nil) + expect(r, http.MethodGet, "/hello", helloGET, nil) + expect(r, http.MethodHead, "/hello", helloHEAD, nil) + expect(r, http.MethodPatch, "/hello", helloPATCH, nil) + expect(r, http.MethodPost, "/hello", helloPOST, nil) + expect(r, http.MethodPut, "/hello", helloPUT, nil) + expect(r, http.MethodGet, "/params/a/b/", paramsGET, map[string]string{ + "x": "a", + "y": "b", + // "z": "", + }) + expect(r, http.MethodGet, "/params/a/b/c%2Fd", paramsGET, map[string]string{ + "x": "a", + "y": "b", + // "z": "c/d", + }) + expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{ + "bookid": "War.and.Peace", + }) + { + req, err := http.NewRequest(http.MethodPost, "/books/War.and.Peace.json", nil) + require.NoError(t, err) + _, _, err = r.FindRoute(req) + require.EqualError(t, err, routers.ErrPathNotFound.Error()) + } + expect(r, http.MethodPost, "/partial", nil, nil) + + doc.Servers = []*openapi3.Server{ + {URL: "https://www.example.com/api/v1"}, + {URL: "https://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ + "d0": {Default: "www"}, + "d1": {Enum: []interface{}{"example"}}, + }}, + } + err = doc.Validate(context.Background()) + require.NoError(t, err) + r, err = NewRouter(doc) + require.NoError(t, err) + expect(r, http.MethodGet, "/hello", nil, nil) + expect(r, http.MethodGet, "/api/v1/hello", nil, nil) + expect(r, http.MethodGet, "www.example.com/api/v1/hello", nil, nil) + expect(r, http.MethodGet, "https:///api/v1/hello", nil, nil) + expect(r, http.MethodGet, "https://www.example.com/hello", nil, nil) + expect(r, http.MethodGet, "https://www.example.com/api/v1/hello", helloGET, nil) + expect(r, http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{ + "d0": "domain0", + "d1": "domain1", + }) + + { + uri := "https://www.example.com/api/v1/onlyGET" + expect(r, http.MethodGet, uri, helloGET, nil) + req, err := http.NewRequest(http.MethodDelete, uri, nil) + require.NoError(t, err) + require.NotNil(t, req) + route, pathParams, err := r.FindRoute(req) + require.EqualError(t, err, routers.ErrMethodNotAllowed.Error()) + require.Nil(t, route) + require.Nil(t, pathParams) + } +} diff --git a/routers/types.go b/routers/types.go new file mode 100644 index 000000000..b15b3ba94 --- /dev/null +++ b/routers/types.go @@ -0,0 +1,35 @@ +package routers + +import ( + "net/http" + + "github.com/getkin/kin-openapi/openapi3" +) + +// Router helps link http.Request.s and an OpenAPIv3 spec +type Router interface { + FindRoute(req *http.Request) (route *Route, pathParams map[string]string, err error) +} + +// Route describes the operation an http.Request can match +type Route struct { + Swagger *openapi3.Swagger + Server *openapi3.Server + Path string + PathItem *openapi3.PathItem + Method string + Operation *openapi3.Operation +} + +// ErrPathNotFound is returned when no route match is found +var ErrPathNotFound error = &RouteError{"no matching operation was found"} + +// ErrMethodNotAllowed is returned when no method of the matched route matches +var ErrMethodNotAllowed error = &RouteError{"method not allowed"} + +// RouteError describes Router errors +type RouteError struct { + Reason string +} + +func (e *RouteError) Error() string { return e.Reason } From 2b6e6b53ffdf07231163595a9eca8ddaabca46c8 Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Sat, 20 Mar 2021 21:55:08 +0200 Subject: [PATCH 055/260] Reset compiledPattern when updating Pattern (#327) --- openapi3/schema.go | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi3/schema.go b/openapi3/schema.go index c39023e53..9c3103a27 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -448,6 +448,7 @@ func (schema *Schema) WithMaxLengthDecodedBase64(i int64) *Schema { func (schema *Schema) WithPattern(pattern string) *Schema { schema.Pattern = pattern + schema.compiledPattern = nil return schema } From 0973da5030159dc0c7b6d4e6958ac37d7ae1f27b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 23 Mar 2021 21:11:30 +0100 Subject: [PATCH 056/260] address #326 (#330) Signed-off-by: Pierre Fenoll --- README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c46f64616..e6a6f898b 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,9 @@ swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("swagger.json") ## Getting OpenAPI operation that matches request ```go loader := openapi3.NewSwaggerLoader() -spec, _ := loader.LoadSwaggerFromData([]byte(`...`)) -_ := spec.Validate(loader.Context) -router, _ := openapi3filter.NewRouter(spec) +doc, _ := loader.LoadSwaggerFromData([]byte(`...`)) +_ := doc.Validate(loader.Context) +router, _ := gorillamux.NewRouter(doc) route, pathParams, _ := router.FindRoute(httpRequest) // Do something with route.Operation ``` @@ -70,14 +70,15 @@ import ( "net/http" "github.com/getkin/kin-openapi/openapi3filter" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) func main() { ctx := context.Background() loader := &openapi3.SwaggerLoader{Context: ctx} - spec, _ := loader.LoadSwaggerFromFile("openapi3_spec.json") - _ := spec.Validate(ctx) - router, _ := openapi3filter.NewRouter(spec) + doc, _ := loader.LoadSwaggerFromFile("openapi3_spec.json") + _ := doc.Validate(ctx) + router, _ := legacyrouter.NewRouter(doc) httpReq, _ := http.NewRequest(http.MethodGet, "/items", nil) // Find route @@ -191,5 +192,13 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.51.0 +* Type `openapi3filter.Route` moved to `routers` (and `Route.Handler` was dropped. See https://github.com/getkin/kin-openapi/issues/329) +* Type `openapi3filter.RouteError` moved to `routers` (so did `ErrPathNotFound` and `ErrMethodNotAllowed` which are now `RouteError`s) +* Routers' `FindRoute(...)` method now takes only one argument: `*http.Request` +* `getkin/kin-openapi/openapi3filter.Router` moved to `getkin/kin-openapi/routers/legacy` +* `openapi3filter.NewRouter()` and its related `WithSwaggerFromFile(string)`, `WithSwagger(*openapi3.Swagger)`, `AddSwaggerFromFile(string)` and `AddSwagger(*openapi3.Swagger)` are all replaced with a single `.NewRouter(*openapi3.Swagger)` + * NOTE: the `NewRouter(doc)` call now requires that the user ensures `doc` is valid (`doc.Validate() != nil`). This used to be asserted. + ### v0.47.0 Field `(*openapi3.SwaggerLoader).LoadSwaggerFromURIFunc` of type `func(*openapi3.SwaggerLoader, *url.URL) (*openapi3.Swagger, error)` was removed after the addition of the field `(*openapi3.SwaggerLoader).ReadFromURIFunc` of type `func(*openapi3.SwaggerLoader, *url.URL) ([]byte, error)`. From 17153345908503543b50b7b6409f9d030bae0beb Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 23 Mar 2021 21:38:46 +0100 Subject: [PATCH 057/260] Drop test dependency on go:embed (#331) --- .github/workflows/go.yml | 1 + go.mod | 2 +- openapi3/testdata/go.mod | 5 ++++ openapi3/testdata/go.sum | 26 +++++++++++++++++++ .../{ => testdata}/load_with_go_embed_test.go | 6 +++-- 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 openapi3/testdata/go.mod create mode 100644 openapi3/testdata/go.sum rename openapi3/{ => testdata}/load_with_go_embed_test.go (84%) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d82af1e6b..d095ebed6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -60,6 +60,7 @@ jobs: run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - run: go test ./... + - run: cd openapi3/testdata && go test -tags with_embed ./... && cd - - if: runner.os == 'Linux' run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] diff --git a/go.mod b/go.mod index 6da959250..f84f470c1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/getkin/kin-openapi -go 1.16 +go 1.14 require ( github.com/ghodss/yaml v1.0.0 diff --git a/openapi3/testdata/go.mod b/openapi3/testdata/go.mod new file mode 100644 index 000000000..b58d02338 --- /dev/null +++ b/openapi3/testdata/go.mod @@ -0,0 +1,5 @@ +module github.com/getkin/kin-openapi/openapi3/testdata.test + +go 1.16 + +require github.com/getkin/kin-openapi v0.52.0 // indirect diff --git a/openapi3/testdata/go.sum b/openapi3/testdata/go.sum new file mode 100644 index 000000000..0739f618e --- /dev/null +++ b/openapi3/testdata/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M= +github.com/getkin/kin-openapi v0.52.0/go.mod h1:fRpo2Nw4Czgy0QnrIesRrEXs5+15N1F9mGZLP/aIomE= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/openapi3/load_with_go_embed_test.go b/openapi3/testdata/load_with_go_embed_test.go similarity index 84% rename from openapi3/load_with_go_embed_test.go rename to openapi3/testdata/load_with_go_embed_test.go index 67ecfea0a..ffcf35fcb 100644 --- a/openapi3/load_with_go_embed_test.go +++ b/openapi3/testdata/load_with_go_embed_test.go @@ -1,3 +1,5 @@ +//+build with_embed + package openapi3_test import ( @@ -8,7 +10,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -//go:embed testdata/recursiveRef/* +//go:embed recursiveRef/* var fs embed.FS func Example() { @@ -18,7 +20,7 @@ func Example() { return fs.ReadFile(uri.Path) } - doc, err := loader.LoadSwaggerFromFile("testdata/recursiveRef/openapi.yml") + doc, err := loader.LoadSwaggerFromFile("recursiveRef/openapi.yml") if err != nil { panic(err) } From 45c1543c7976be04a87b2f8ff4baf7116390daca Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Thu, 25 Mar 2021 11:57:51 +0200 Subject: [PATCH 058/260] Update README.md (#333) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e6a6f898b..0f3f35abe 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Licensed under the [MIT License](LICENSE). The project has received pull requests from many people. Thanks to everyone! Here's some projects that depend on _kin-openapi_: + * [https://github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" * [github.com/getkin/kin](https://github.com/getkin/kin) - "A configurable backend" * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPI 3 spec From 13a13985d18353170014c743b74a36696c20f5f3 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 12 Apr 2021 15:21:43 +0200 Subject: [PATCH 059/260] introduce openapi3filter.RegisteredBodyDecoder (#340) Signed-off-by: Pierre Fenoll --- openapi3filter/req_resp_decoder.go | 10 ++++++ openapi3filter/req_resp_decoder_test.go | 47 +++++++++++++------------ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index d19266440..b9b0cd8e5 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -767,10 +767,19 @@ type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) ( // By default, there is content type "application/json" is supported only. var bodyDecoders = make(map[string]BodyDecoder) +// RegisteredBodyDecoder returns the registered body decoder for the given content type. +// +// If no decoder was registered for the given content type, nil is returned. +// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines. +func RegisteredBodyDecoder(contentType string) BodyDecoder { + return bodyDecoders[contentType] +} + // RegisterBodyDecoder registers a request body's decoder for a content type. // // If a decoder for the specified content type already exists, the function replaces // it with the specified decoder. +// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines. func RegisterBodyDecoder(contentType string, decoder BodyDecoder) { if contentType == "" { panic("contentType is empty") @@ -784,6 +793,7 @@ func RegisterBodyDecoder(contentType string, decoder BodyDecoder) { // UnregisterBodyDecoder dissociates a body decoder from a content type. // // Decoding this content type will result in an error. +// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines. func UnregisterBodyDecoder(contentType string) { if contentType == "" { panic("contentType is empty") diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index b6d603e2b..c461da94f 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -1187,39 +1187,42 @@ func newTestMultipartForm(parts []*testFormPart) (io.Reader, string, error) { } func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { - var ( - contentType = "text/csv" - decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { - data, err := ioutil.ReadAll(body) - if err != nil { - return nil, err - } - var vv []interface{} - for _, v := range strings.Split(string(data), ",") { - vv = append(vv, v) - } - return vv, nil + var decoder BodyDecoder + decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (decoded interface{}, err error) { + var data []byte + if data, err = ioutil.ReadAll(body); err != nil { + return } - schema = openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema()).NewRef() - encFn = func(string) *openapi3.Encoding { return nil } - body = strings.NewReader("foo,bar") - want = []interface{}{"foo", "bar"} - wantErr = &ParseError{Kind: KindUnsupportedFormat} - ) + return strings.Split(string(data), ","), nil + } + contentType := "text/csv" h := make(http.Header) h.Set(headerCT, contentType) + originalDecoder := RegisteredBodyDecoder(contentType) + require.Nil(t, originalDecoder) + RegisterBodyDecoder(contentType, decoder) + require.Equal(t, fmt.Sprintf("%v", decoder), fmt.Sprintf("%v", RegisteredBodyDecoder(contentType))) + + body := strings.NewReader("foo,bar") + schema := openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema()).NewRef() + encFn := func(string) *openapi3.Encoding { return nil } got, err := decodeBody(body, h, schema, encFn) require.NoError(t, err) - require.Truef(t, reflect.DeepEqual(got, want), "got %v, want %v", got, want) + require.Equal(t, []string{"foo", "bar"}, got) UnregisterBodyDecoder(contentType) - _, err = decodeBody(body, h, schema, encFn) - require.Error(t, err) - require.Truef(t, matchParseError(err, wantErr), "got error:\n%v\nwant error:\n%v", err, wantErr) + originalDecoder = RegisteredBodyDecoder(contentType) + require.Nil(t, originalDecoder) + + _, err = decodeBody(body, h, schema, encFn) + require.Equal(t, &ParseError{ + Kind: KindUnsupportedFormat, + Reason: prefixUnsupportedCT + ` "text/csv"`, + }, err) } func matchParseError(got, want error) bool { From 349fb583776dcb4a0fb371d3a43f7ed952d2b206 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sat, 17 Apr 2021 11:55:31 +0200 Subject: [PATCH 060/260] openapi3: allow variables in schemes in gorillamux router + better server variables validation (#337) --- openapi3/server.go | 21 ++++------ openapi3/swagger_loader_test.go | 9 ++-- routers/gorillamux/router.go | 68 +++++++++++++++++++++++++++---- routers/gorillamux/router_test.go | 22 ++++++++-- routers/legacy/router_test.go | 2 +- 5 files changed, 92 insertions(+), 30 deletions(-) diff --git a/openapi3/server.go b/openapi3/server.go index 682cb4d6f..6cdeb4afd 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -150,9 +150,9 @@ func (server *Server) Validate(c context.Context) (err error) { // ServerVariable is specified by OpenAPI/Swagger standard version 3.0. type ServerVariable struct { ExtensionProps - Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` + Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` + Default string `json:"default,omitempty" yaml:"default,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` } func (serverVariable *ServerVariable) MarshalJSON() ([]byte, error) { @@ -164,17 +164,12 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { } func (serverVariable *ServerVariable) Validate(c context.Context) error { - switch serverVariable.Default.(type) { - case float64, string, nil: - default: - return errors.New("value of default must be either a number or a string") - } - for _, item := range serverVariable.Enum { - switch item.(type) { - case float64, string: - default: - return errors.New("all 'enum' items must be either a number or a string") + if serverVariable.Default == "" { + data, err := serverVariable.MarshalJSON() + if err != nil { + return err } + return fmt.Errorf("field default is required in %s", data) } return nil } diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go index 71c176377..0662f16c7 100644 --- a/openapi3/swagger_loader_test.go +++ b/openapi3/swagger_loader_test.go @@ -514,10 +514,11 @@ servers: `{url: "http://{x}.{y}.example.com"}`: errors.New("invalid servers: server has undeclared variables"), `{url: "http://{x}.y}.example.com"}`: errors.New("invalid servers: server URL has mismatched { and }"), `{url: "http://{x.example.com"}`: errors.New("invalid servers: server URL has mismatched { and }"), - `{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`: nil, - `{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`: nil, - `{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), - `{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), + `{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`: nil, + `{url: "http://{x}.example.com", variables: {x: {default: "www", enum: ["www"]}}}`: nil, + `{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New(`invalid servers: field default is required in {"enum":["www"]}`), + `{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), + `{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), } { t.Run(value, func(t *testing.T) { loader := NewSwaggerLoader() diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 7973398a7..6bdda1a17 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -28,12 +28,17 @@ type Router struct { // TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request)) func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { type srv struct { - scheme, host, base string - server *openapi3.Server + schemes []string + host, base string + server *openapi3.Server } servers := make([]srv, 0, len(doc.Servers)) for _, server := range doc.Servers { - u, err := url.Parse(bEncode(server.URL)) + serverURL := server.URL + scheme0 := strings.Split(serverURL, "://")[0] + schemes := permutePart(scheme0, server) + + u, err := url.Parse(bEncode(strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1))) if err != nil { return nil, err } @@ -42,10 +47,10 @@ func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { path = path[:len(path)-1] } servers = append(servers, srv{ - host: bDecode(u.Host), //u.Hostname()? - base: path, - scheme: bDecode(u.Scheme), - server: server, + host: bDecode(u.Host), //u.Hostname()? + base: path, + schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624 + server: server, }) } if len(servers) == 0 { @@ -65,8 +70,8 @@ func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { for _, s := range servers { muxRoute := muxRouter.Path(s.base + path).Methods(methods...) - if scheme := s.scheme; scheme != "" { - muxRoute.Schemes(scheme) + if schemes := s.schemes; len(schemes) != 0 { + muxRoute.Schemes(schemes...) } if host := s.host; host != "" { muxRoute.Host(host) @@ -151,3 +156,48 @@ func bDecode(s string) string { s = strings.Replace(s, brURL, "}", -1) return s } + +func permutePart(part0 string, srv *openapi3.Server) []string { + type mapAndSlice struct { + m map[string]struct{} + s []string + } + var2val := make(map[string]mapAndSlice) + max := 0 + for name0, v := range srv.Variables { + name := "{" + name0 + "}" + if !strings.Contains(part0, name) { + continue + } + m := map[string]struct{}{v.Default: {}} + for _, value := range v.Enum { + m[value] = struct{}{} + } + if l := len(m); l > max { + max = l + } + s := make([]string, 0, len(m)) + for value := range m { + s = append(s, value) + } + var2val[name] = mapAndSlice{m: m, s: s} + } + if len(var2val) == 0 { + return []string{part0} + } + + partsMap := make(map[string]struct{}, max*len(var2val)) + for i := 0; i < max; i++ { + part := part0 + for name, mas := range var2val { + part = strings.Replace(part, name, mas.s[i%len(mas.s)], -1) + } + partsMap[part] = struct{}{} + } + parts := make([]string, 0, len(partsMap)) + for part := range partsMap { + parts = append(parts, part) + } + sort.Strings(parts) + return parts +} diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index fd04d4ea9..1a3d1123e 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -158,9 +158,10 @@ func TestRouter(t *testing.T) { doc.Servers = []*openapi3.Server{ {URL: "https://www.example.com/api/v1"}, - {URL: "https://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ - "d0": {Default: "www"}, - "d1": {Enum: []interface{}{"example"}}, + {URL: "{scheme}://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ + "d0": {Default: "www"}, + "d1": {Default: "example", Enum: []string{"example"}}, + "scheme": {Default: "https", Enum: []string{"https", "http"}}, }}, } err = doc.Validate(context.Background()) @@ -176,6 +177,7 @@ func TestRouter(t *testing.T) { expect(r, http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{ "d0": "domain0", "d1": "domain1", + // "scheme": "https", TODO: https://github.com/gorilla/mux/issues/624 }) { @@ -190,3 +192,17 @@ func TestRouter(t *testing.T) { require.Nil(t, pathParams) } } + +func TestPermuteScheme(t *testing.T) { + scheme0 := "{sche}{me}" + server := &openapi3.Server{URL: scheme0 + "://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ + "d0": {Default: "www"}, + "d1": {Default: "example", Enum: []string{"example"}}, + "sche": {Default: "http"}, + "me": {Default: "s", Enum: []string{"", "s"}}, + }} + err := server.Validate(context.Background()) + require.NoError(t, err) + perms := permutePart(scheme0, server) + require.Equal(t, []string{"http", "https"}, perms) +} diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go index af0dfba28..082eab571 100644 --- a/routers/legacy/router_test.go +++ b/routers/legacy/router_test.go @@ -163,7 +163,7 @@ func TestRouter(t *testing.T) { {URL: "https://www.example.com/api/v1"}, {URL: "https://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ "d0": {Default: "www"}, - "d1": {Enum: []interface{}{"example"}}, + "d1": {Default: "example", Enum: []string{"example"}}, }}, } err = doc.Validate(context.Background()) From 751a395873dcd893bbbc2f477bb94364ca6a0e18 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 23 Apr 2021 12:40:39 +0200 Subject: [PATCH 061/260] Fix following refs to non-openapi3 root documents (but that are sub-documents) (#346) --- .gitignore | 1 - openapi3/callback.go | 2 +- openapi3/examples.go | 2 +- openapi3/header.go | 2 +- openapi3/issue344_test.go | 20 ++ openapi3/schema.go | 2 +- openapi3/swagger_loader.go | 271 ++++++++---------- ...er_loader_referenced_document_path_test.go | 60 ---- openapi3/swagger_loader_relative_refs_test.go | 22 +- openapi3/testdata/ext.json | 17 ++ .../CustomTestHeader.yml | 2 +- .../CustomTestHeader1.yml | 1 + .../CustomTestHeader1bis.yml | 2 + .../CustomTestHeader2.yml | 1 + .../CustomTestHeader2bis.yml | 2 + .../paths/nesteddir/CustomTestPath.yml | 4 + .../nesteddir/morenested/CustomTestPath.yml | 4 + openapi3/testdata/spec.yaml | 14 + openapi3filter/validation_error_encoder.go | 2 +- openapi3gen/openapi3gen_test.go | 2 +- routers/gorillamux/router.go | 4 +- 21 files changed, 198 insertions(+), 239 deletions(-) create mode 100644 openapi3/issue344_test.go delete mode 100644 openapi3/swagger_loader_referenced_document_path_test.go create mode 100644 openapi3/testdata/ext.json create mode 100644 openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml create mode 100644 openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml create mode 100644 openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml create mode 100644 openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml create mode 100644 openapi3/testdata/spec.yaml diff --git a/.gitignore b/.gitignore index a5bf17cb5..31ab03fb6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,3 @@ # IntelliJ / GoLand .idea - diff --git a/openapi3/callback.go b/openapi3/callback.go index 334233104..b216d1a9d 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -13,7 +13,7 @@ var _ jsonpointer.JSONPointable = (*Callbacks)(nil) func (c Callbacks) JSONLookup(token string) (interface{}, error) { ref, ok := c[token] - if ref == nil || ok == false { + if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } diff --git a/openapi3/examples.go b/openapi3/examples.go index 5f6255bf3..98f79b884 100644 --- a/openapi3/examples.go +++ b/openapi3/examples.go @@ -13,7 +13,7 @@ var _ jsonpointer.JSONPointable = (*Examples)(nil) func (e Examples) JSONLookup(token string) (interface{}, error) { ref, ok := e[token] - if ref == nil || ok == false { + if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } diff --git a/openapi3/header.go b/openapi3/header.go index 3adb2ea5a..bab30a412 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -14,7 +14,7 @@ var _ jsonpointer.JSONPointable = (*Headers)(nil) func (h Headers) JSONLookup(token string) (interface{}, error) { ref, ok := h[token] - if ref == nil || ok == false { + if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } diff --git a/openapi3/issue344_test.go b/openapi3/issue344_test.go new file mode 100644 index 000000000..8a53394c5 --- /dev/null +++ b/openapi3/issue344_test.go @@ -0,0 +1,20 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue344(t *testing.T) { + sl := NewSwaggerLoader() + sl.IsExternalRefsAllowed = true + + doc, err := sl.LoadSwaggerFromFile("testdata/spec.yaml") + require.NoError(t, err) + + err = doc.Validate(sl.Context) + require.NoError(t, err) + + require.Equal(t, "string", doc.Components.Schemas["Test"].Value.Properties["test"].Value.Properties["name"].Value.Type) +} diff --git a/openapi3/schema.go b/openapi3/schema.go index 9c3103a27..22cb84210 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1266,7 +1266,7 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ Value: value, Schema: schema, SchemaField: "uniqueItems", - Reason: fmt.Sprintf("duplicate items found"), + Reason: "duplicate items found", } if !settings.multiError { return err diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 8ae8d0406..2cb951e53 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -63,6 +63,11 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swag return swaggerLoader.loadSwaggerFromURIInternal(location) } +// LoadSwaggerFromFile loads a spec from a local file path +func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) { + return swaggerLoader.LoadSwaggerFromURI(&url.URL{Path: path}) +} + func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL) (*Swagger, error) { data, err := swaggerLoader.readURL(location) if err != nil { @@ -71,35 +76,41 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location) } -// loadSingleElementFromURI reads the data from ref and unmarshals to the passed element. -func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) error { +func (swaggerLoader *SwaggerLoader) allowsExternalRefs(ref string) (err error) { if !swaggerLoader.IsExternalRefsAllowed { - return fmt.Errorf("encountered non-allowed external reference: %q", ref) + err = fmt.Errorf("encountered disallowed external reference: %q", ref) + } + return +} + +// loadSingleElementFromURI reads the data from ref and unmarshals to the passed element. +func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) (*url.URL, error) { + if err := swaggerLoader.allowsExternalRefs(ref); err != nil { + return nil, err } parsedURL, err := url.Parse(ref) if err != nil { - return err + return nil, err } - - if parsedURL.Fragment != "" { - return errors.New("references to files which contain more than one element definition are not supported") + if fragment := parsedURL.Fragment; fragment != "" { + return nil, fmt.Errorf("unexpected ref fragment %q", fragment) } resolvedPath, err := resolvePath(rootPath, parsedURL) if err != nil { - return fmt.Errorf("could not resolve path: %v", err) + return nil, fmt.Errorf("could not resolve path: %v", err) } data, err := swaggerLoader.readURL(resolvedPath) if err != nil { - return err + return nil, err } if err := yaml.Unmarshal(data, element); err != nil { - return err + return nil, err } - return nil + return resolvedPath, nil } func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { @@ -121,28 +132,9 @@ func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { return ioutil.ReadFile(location.Path) } -// LoadSwaggerFromFile loads a spec from a local file path -func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) { - swaggerLoader.resetVisitedPathItemRefs() - return swaggerLoader.loadSwaggerFromFileInternal(path) -} - -func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*Swagger, error) { - pathAsURL := &url.URL{Path: path} - data, err := swaggerLoader.readURL(pathAsURL) - if err != nil { - return nil, err - } - return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, pathAsURL) -} - // LoadSwaggerFromData loads a spec from a byte array func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, error) { swaggerLoader.resetVisitedPathItemRefs() - return swaggerLoader.loadSwaggerFromDataInternal(data) -} - -func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataInternal(data []byte) (*Swagger, error) { doc := &Swagger{} if err := yaml.Unmarshal(data, doc); err != nil { return nil, err @@ -239,15 +231,11 @@ func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.UR return } -func copyURL(basePath *url.URL) (*url.URL, error) { - return url.Parse(basePath.String()) -} - func join(basePath *url.URL, relativePath *url.URL) (*url.URL, error) { if basePath == nil { return relativePath, nil } - newPath, err := copyURL(basePath) + newPath, err := url.Parse(basePath.String()) if err != nil { return nil, fmt.Errorf("cannot copy path: %q", basePath.String()) } @@ -292,18 +280,34 @@ func (swaggerLoader *SwaggerLoader) resolveComponent( return nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) } - var cursor interface{} - cursor = swagger - for _, pathPart := range strings.Split(fragment[1:], "/") { - pathPart = unescapeRefString(pathPart) + drill := func(cursor interface{}) (interface{}, error) { + for _, pathPart := range strings.Split(fragment[1:], "/") { + pathPart = unescapeRefString(pathPart) - if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil { - e := failedToResolveRefFragmentPart(ref, pathPart) - return nil, fmt.Errorf("%s: %s", e.Error(), err.Error()) + if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil { + e := failedToResolveRefFragmentPart(ref, pathPart) + return nil, fmt.Errorf("%s: %s", e.Error(), err.Error()) + } + if cursor == nil { + return nil, failedToResolveRefFragmentPart(ref, pathPart) + } + } + return cursor, nil + } + var cursor interface{} + if cursor, err = drill(swagger); err != nil { + var err2 error + data, err2 := swaggerLoader.readURL(path) + if err2 != nil { + return nil, err } - if cursor == nil { - return nil, failedToResolveRefFragmentPart(ref, pathPart) + if err2 = yaml.Unmarshal(data, &cursor); err2 != nil { + return nil, err + } + if cursor, err2 = drill(cursor); err2 != nil || cursor == nil { + return nil, err } + err = nil } switch { @@ -388,33 +392,34 @@ func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, e } func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref string, path *url.URL) (*Swagger, string, *url.URL, error) { - componentPath := path - if !strings.HasPrefix(ref, "#") { - if !swaggerLoader.IsExternalRefsAllowed { - return nil, "", nil, fmt.Errorf("encountered non-allowed external reference: %q", ref) - } - parsedURL, err := url.Parse(ref) - if err != nil { - return nil, "", nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) - } - fragment := parsedURL.Fragment - parsedURL.Fragment = "" + if ref != "" && ref[0] == '#' { + return swagger, ref, path, nil + } - resolvedPath, err := resolvePath(path, parsedURL) - if err != nil { - return nil, "", nil, fmt.Errorf("error resolving path: %v", err) - } + if err := swaggerLoader.allowsExternalRefs(ref); err != nil { + return nil, "", nil, err + } - if swagger, err = swaggerLoader.loadSwaggerFromURIInternal(resolvedPath); err != nil { - return nil, "", nil, fmt.Errorf("error resolving reference %q: %v", ref, err) - } - ref = "#" + fragment - componentPath = resolvedPath + parsedURL, err := url.Parse(ref) + if err != nil { + return nil, "", nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) + } + fragment := parsedURL.Fragment + parsedURL.Fragment = "" + + var resolvedPath *url.URL + if resolvedPath, err = resolvePath(path, parsedURL); err != nil { + return nil, "", nil, fmt.Errorf("error resolving path: %v", err) + } + + if swagger, err = swaggerLoader.loadSwaggerFromURIInternal(resolvedPath); err != nil { + return nil, "", nil, fmt.Errorf("error resolving reference %q: %v", ref, err) } - return swagger, ref, componentPath, nil + + return swagger, "#" + fragment, resolvedPath, nil } -func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedHeader == nil { swaggerLoader.visitedHeader = make(map[*Header]struct{}) @@ -425,17 +430,15 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component swaggerLoader.visitedHeader[component.Value] = struct{}{} } - const prefix = "#/components/headers/" if component == nil { return errors.New("invalid header: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var header Header - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { return err } - component.Value = &header } else { var resolved HeaderRef @@ -453,6 +456,7 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component if value == nil { return nil } + if schema := value.Schema; schema != nil { if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err @@ -461,7 +465,7 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component return nil } -func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedParameter == nil { swaggerLoader.visitedParameter = make(map[*Parameter]struct{}) @@ -472,7 +476,6 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon swaggerLoader.visitedParameter[component.Value] = struct{}{} } - const prefix = "#/components/parameters/" if component == nil { return errors.New("invalid parameter: value MUST be an object") } @@ -480,7 +483,7 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon if ref != "" { if isSingleRefElement(ref) { var param Parameter - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { return err } component.Value = ¶m @@ -501,30 +504,25 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon return nil } - refDocumentPath, err := referencedDocumentPath(documentPath, ref) - if err != nil { - return err - } - if value.Content != nil && value.Schema != nil { return errors.New("cannot contain both schema and content in a parameter") } for _, contentType := range value.Content { if schema := contentType.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err } } } if schema := value.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err } } return nil } -func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedRequestBody == nil { swaggerLoader.visitedRequestBody = make(map[*RequestBody]struct{}) @@ -535,17 +533,15 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp swaggerLoader.visitedRequestBody[component.Value] = struct{}{} } - const prefix = "#/components/requestBodies/" if component == nil { return errors.New("invalid requestBody: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var requestBody RequestBody - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { return err } - component.Value = &requestBody } else { var resolved RequestBodyRef @@ -563,6 +559,7 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp if value == nil { return nil } + for _, contentType := range value.Content { for name, example := range contentType.Examples { if err := swaggerLoader.resolveExampleRef(swagger, example, documentPath); err != nil { @@ -579,7 +576,7 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp return nil } -func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedResponse == nil { swaggerLoader.visitedResponse = make(map[*Response]struct{}) @@ -590,7 +587,6 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone swaggerLoader.visitedResponse[component.Value] = struct{}{} } - const prefix = "#/components/responses/" if component == nil { return errors.New("invalid response: value MUST be an object") } @@ -598,7 +594,7 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone if ref != "" { if isSingleRefElement(ref) { var resp Response - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { return err } component.Value = &resp @@ -614,17 +610,13 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone component.Value = resolved.Value } } - refDocumentPath, err := referencedDocumentPath(documentPath, ref) - if err != nil { - return err - } - value := component.Value if value == nil { return nil } + for _, header := range value.Headers { - if err := swaggerLoader.resolveHeaderRef(swagger, header, refDocumentPath); err != nil { + if err := swaggerLoader.resolveHeaderRef(swagger, header, documentPath); err != nil { return err } } @@ -633,27 +625,27 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone continue } for name, example := range contentType.Examples { - if err := swaggerLoader.resolveExampleRef(swagger, example, refDocumentPath); err != nil { + if err := swaggerLoader.resolveExampleRef(swagger, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err } contentType.Schema = schema } } for _, link := range value.Links { - if err := swaggerLoader.resolveLinkRef(swagger, link, refDocumentPath); err != nil { + if err := swaggerLoader.resolveLinkRef(swagger, link, documentPath); err != nil { return err } } return nil } -func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedSchema == nil { swaggerLoader.visitedSchema = make(map[*Schema]struct{}) @@ -664,7 +656,6 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component swaggerLoader.visitedSchema[component.Value] = struct{}{} } - const prefix = "#/components/schemas/" if component == nil { return errors.New("invalid schema: value MUST be an object") } @@ -672,7 +663,7 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component if ref != "" { if isSingleRefElement(ref) { var schema Schema - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { return err } component.Value = &schema @@ -688,12 +679,6 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component component.Value = resolved.Value } } - - refDocumentPath, err := referencedDocumentPath(documentPath, ref) - if err != nil { - return err - } - value := component.Value if value == nil { return nil @@ -701,45 +686,44 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component // ResolveRefs referred schemas if v := value.Items; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } for _, v := range value.Properties { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } if v := value.AdditionalProperties; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } if v := value.Not; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } for _, v := range value.AllOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } for _, v := range value.AnyOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } for _, v := range value.OneOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } - return nil } -func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedSecurityScheme == nil { swaggerLoader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) @@ -750,17 +734,15 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c swaggerLoader.visitedSecurityScheme[component.Value] = struct{}{} } - const prefix = "#/components/securitySchemes/" if component == nil { return errors.New("invalid securityScheme: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var scheme SecurityScheme - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { return err } - component.Value = &scheme } else { var resolved SecuritySchemeRef @@ -777,7 +759,7 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c return nil } -func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedExample == nil { swaggerLoader.visitedExample = make(map[*Example]struct{}) @@ -788,17 +770,15 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen swaggerLoader.visitedExample[component.Value] = struct{}{} } - const prefix = "#/components/examples/" if component == nil { return errors.New("invalid example: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var example Example - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { return err } - component.Value = &example } else { var resolved ExampleRef @@ -815,7 +795,7 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen return nil } -func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedLink == nil { swaggerLoader.visitedLink = make(map[*Link]struct{}) @@ -826,17 +806,15 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * swaggerLoader.visitedLink[component.Value] = struct{}{} } - const prefix = "#/components/links/" if component == nil { return errors.New("invalid link: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var link Link - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { return err } - component.Value = &link } else { var resolved LinkRef @@ -864,7 +842,6 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo } swaggerLoader.visitedPathItemRefs[key] = struct{}{} - const prefix = "#/paths/" if pathItem == nil { return errors.New("invalid path item: value MUST be an object") } @@ -872,7 +849,7 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo if ref != "" { if isSingleRefElement(ref) { var p PathItem - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { return err } *pathItem = p @@ -881,11 +858,11 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo return } - if !strings.HasPrefix(ref, prefix) { - err = fmt.Errorf("expected prefix %q in URI %q", prefix, ref) - return + rest := strings.TrimPrefix(ref, "#/paths/") + if rest == ref { + return fmt.Errorf(`expected prefix "#/paths/" in URI %q`, ref) } - id := unescapeRefString(ref[len(prefix):]) + id := unescapeRefString(rest) definitions := swagger.Paths if definitions == nil { @@ -900,55 +877,31 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo } } - refDocumentPath, err := referencedDocumentPath(documentPath, ref) - if err != nil { - return err - } - for _, parameter := range pathItem.Parameters { - if err = swaggerLoader.resolveParameterRef(swagger, parameter, refDocumentPath); err != nil { + if err = swaggerLoader.resolveParameterRef(swagger, parameter, documentPath); err != nil { return } } for _, operation := range pathItem.Operations() { for _, parameter := range operation.Parameters { - if err = swaggerLoader.resolveParameterRef(swagger, parameter, refDocumentPath); err != nil { + if err = swaggerLoader.resolveParameterRef(swagger, parameter, documentPath); err != nil { return } } if requestBody := operation.RequestBody; requestBody != nil { - if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, refDocumentPath); err != nil { + if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, documentPath); err != nil { return } } for _, response := range operation.Responses { - if err = swaggerLoader.resolveResponseRef(swagger, response, refDocumentPath); err != nil { + if err = swaggerLoader.resolveResponseRef(swagger, response, documentPath); err != nil { return } } } - - return nil + return } func unescapeRefString(ref string) string { return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1) } - -func referencedDocumentPath(documentPath *url.URL, ref string) (*url.URL, error) { - if documentPath == nil { - return nil, nil - } - - newDocumentPath, err := copyURL(documentPath) - if err != nil { - return nil, err - } - refPath, err := url.Parse(ref) - if err != nil { - return nil, err - } - newDocumentPath.Path = path.Join(path.Dir(newDocumentPath.Path), path.Dir(refPath.Path)) + "/" - - return newDocumentPath, nil -} diff --git a/openapi3/swagger_loader_referenced_document_path_test.go b/openapi3/swagger_loader_referenced_document_path_test.go deleted file mode 100644 index de219c369..000000000 --- a/openapi3/swagger_loader_referenced_document_path_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package openapi3 - -import ( - "net/url" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestReferencedDocumentPath(t *testing.T) { - httpURL, err := url.Parse("http://example.com/path/to/schemas/test1.yaml") - require.NoError(t, err) - - fileURL, err := url.Parse("path/to/schemas/test1.yaml") - require.NoError(t, err) - - refEmpty := "" - refNoComponent := "moreschemas/test2.yaml" - refWithComponent := "moreschemas/test2.yaml#/components/schemas/someobject" - - for _, test := range []struct { - path *url.URL - ref, expected string - }{ - { - path: httpURL, - ref: refEmpty, - expected: "http://example.com/path/to/schemas/", - }, - { - path: httpURL, - ref: refNoComponent, - expected: "http://example.com/path/to/schemas/moreschemas/", - }, - { - path: httpURL, - ref: refWithComponent, - expected: "http://example.com/path/to/schemas/moreschemas/", - }, - { - path: fileURL, - ref: refEmpty, - expected: "path/to/schemas/", - }, - { - path: fileURL, - ref: refNoComponent, - expected: "path/to/schemas/moreschemas/", - }, - { - path: fileURL, - ref: refWithComponent, - expected: "path/to/schemas/moreschemas/", - }, - } { - result, err := referencedDocumentPath(test.path, test.ref) - require.NoError(t, err) - require.Equal(t, test.expected, result.String()) - } -} diff --git a/openapi3/swagger_loader_relative_refs_test.go b/openapi3/swagger_loader_relative_refs_test.go index 8f074680a..cb5ab3f05 100644 --- a/openapi3/swagger_loader_relative_refs_test.go +++ b/openapi3/swagger_loader_relative_refs_test.go @@ -814,25 +814,25 @@ func TestLoadSpecWithRelativeDocumentRefs(t *testing.T) { const relativeSchemaDocsRefTemplate = ` openapi: 3.0.0 -info: +info: title: "" version: "1.0" paths: {} -components: - schemas: - TestSchema: +components: + schemas: + TestSchema: $ref: relativeDocs/CustomTestSchema.yml ` const relativeResponseDocsRefTemplate = ` openapi: 3.0.0 -info: +info: title: "" version: "1.0" paths: {} -components: - responses: - TestResponse: +components: + responses: + TestResponse: $ref: relativeDocs/CustomTestResponse.yml ` @@ -844,7 +844,7 @@ info: paths: {} components: parameters: - TestParameter: + TestParameter: $ref: relativeDocs/CustomTestParameter.yml ` @@ -921,6 +921,8 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // 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) // check request body require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description) @@ -939,6 +941,8 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // 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) // check request body require.Equal(t, "example request", moreNestedDirPath.Patch.RequestBody.Value.Description) diff --git a/openapi3/testdata/ext.json b/openapi3/testdata/ext.json new file mode 100644 index 000000000..df227e62e --- /dev/null +++ b/openapi3/testdata/ext.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "a": { + "type": "string" + }, + "b": { + "type": "object", + "description": "I use a local reference.", + "properties": { + "name": { + "$ref": "#/definitions/a" + } + } + } + } +} diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml index 9d12ac352..e5cf2b15b 100644 --- a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml @@ -1 +1 @@ -description: header \ No newline at end of file +description: header diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml new file mode 100644 index 000000000..4a5d8f994 --- /dev/null +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml @@ -0,0 +1 @@ +description: header1 diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml new file mode 100644 index 000000000..532e79203 --- /dev/null +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml @@ -0,0 +1,2 @@ +header: + description: header1 diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml new file mode 100644 index 000000000..71536c564 --- /dev/null +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml @@ -0,0 +1 @@ +description: header2 diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml new file mode 100644 index 000000000..c14ae4710 --- /dev/null +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml @@ -0,0 +1,2 @@ +header: + description: header2 diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml index 45046c421..6e608808c 100644 --- a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml +++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml @@ -10,6 +10,10 @@ patch: headers: X-Rate-Limit-Reset: $ref: "../../../CustomTestHeader.yml" + X-Another: + $ref: ../../../CustomTestHeader1.yml + X-And-Another: + $ref: ../../../CustomTestHeader2.yml content: application/json: schema: diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml index 647852fc5..35c5ccf51 100644 --- a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml +++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml @@ -10,6 +10,10 @@ patch: headers: X-Rate-Limit-Reset: $ref: "../../../../CustomTestHeader.yml" + X-Another: + $ref: '../../../../CustomTestHeader1bis.yml#/header' + X-And-Another: + $ref: '../../../../CustomTestHeader2bis.yml#/header' content: application/json: schema: diff --git a/openapi3/testdata/spec.yaml b/openapi3/testdata/spec.yaml new file mode 100644 index 000000000..781312f8e --- /dev/null +++ b/openapi3/testdata/spec.yaml @@ -0,0 +1,14 @@ +openapi: 3.0.1 +info: + version: 1.0.0 + title: Some Swagger + license: + name: MIT +paths: {} +components: + schemas: + Test: + type: object + properties: + test: + $ref: 'ext.json#/definitions/b' diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 47f9cd9f0..707b22d4a 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -152,7 +152,7 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida strings.Join(enums, ", ")) value := fmt.Sprintf("%v", innerErr.Value) if e.Parameter != nil && - (e.Parameter.Explode == nil || *e.Parameter.Explode == true) && + (e.Parameter.Explode == nil || *e.Parameter.Explode) && (e.Parameter.Style == "" || e.Parameter.Style == "form") && strings.Contains(value, ",") { parts := strings.Split(value, ",") diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 1422017a5..a975b2a7a 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -25,7 +25,7 @@ func TestExportedNonTagged(t *testing.T) { type Bla struct { A string Another string `json:"another"` - yetAnother string + yetAnother string // unused because unexported EvenAYaml string `yaml:"even_a_yaml"` } diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 6bdda1a17..896f5fa47 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -135,9 +135,7 @@ func orderedPaths(paths map[string]*openapi3.PathItem) []string { for c := 0; c <= max; c++ { if ps, ok := vars[c]; ok { sort.Strings(ps) - for _, p := range ps { - ordered = append(ordered, p) - } + ordered = append(ordered, ps...) } } return ordered From 61b5dd987e6b84bc563b3dc7f2ad1a48d3c16a85 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 23 Apr 2021 13:00:20 +0200 Subject: [PATCH 062/260] reproduce failing to load JSON refs in non-openapi document (#314) --- openapi3/swagger_loader_outside_refs_test.go | 20 +++++++++++++ .../testdata/303bis/common/properties.yaml | 16 +++++++++++ openapi3/testdata/303bis/service.yaml | 28 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 openapi3/swagger_loader_outside_refs_test.go create mode 100644 openapi3/testdata/303bis/common/properties.yaml create mode 100644 openapi3/testdata/303bis/service.yaml diff --git a/openapi3/swagger_loader_outside_refs_test.go b/openapi3/swagger_loader_outside_refs_test.go new file mode 100644 index 000000000..1a5cb1c62 --- /dev/null +++ b/openapi3/swagger_loader_outside_refs_test.go @@ -0,0 +1,20 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadOutsideRefs(t *testing.T) { + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadSwaggerFromFile("testdata/303bis/service.yaml") + require.NoError(t, err) + require.NotNil(t, doc) + + 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) +} diff --git a/openapi3/testdata/303bis/common/properties.yaml b/openapi3/testdata/303bis/common/properties.yaml new file mode 100644 index 000000000..e5b6cdb46 --- /dev/null +++ b/openapi3/testdata/303bis/common/properties.yaml @@ -0,0 +1,16 @@ +timestamp: + type: string + description: Date and time in ISO 8601 format. + example: "2020-04-09T18:14:30Z" + readOnly: true + nullable: true + +timestamps: + type: object + properties: + created_at: + $ref: "#/timestamp" + deleted_at: + $ref: "#/timestamp" + updated_at: + $ref: "#/timestamp" diff --git a/openapi3/testdata/303bis/service.yaml b/openapi3/testdata/303bis/service.yaml new file mode 100644 index 000000000..39dd06639 --- /dev/null +++ b/openapi3/testdata/303bis/service.yaml @@ -0,0 +1,28 @@ +openapi: 3.0.0 +info: + title: 'some service spec' + version: 1.2.3 + +paths: + /service: + get: + tags: + - services/service + summary: List services + description: List services. + operationId: list-services + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/model_service" + +components: + schemas: + model_service: + allOf: + - $ref: "common/properties.yaml#/timestamps" From 983bf118d0a99346a1bb7bdeb14eae0e64154ef3 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 23 Apr 2021 13:01:51 +0200 Subject: [PATCH 063/260] repro #341 (#342) --- openapi3/issue341_test.go | 26 ++++++++++++++++++++++++++ openapi3/testdata/main.yaml | 7 +++++++ openapi3/testdata/testpath.yaml | 15 +++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 openapi3/issue341_test.go create mode 100644 openapi3/testdata/main.yaml create mode 100644 openapi3/testdata/testpath.yaml diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go new file mode 100644 index 000000000..c026d6d4f --- /dev/null +++ b/openapi3/issue341_test.go @@ -0,0 +1,26 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue341(t *testing.T) { + sl := NewSwaggerLoader() + sl.IsExternalRefsAllowed = true + doc, err := sl.LoadSwaggerFromFile("testdata/main.yaml") + require.NoError(t, err) + + err = doc.Validate(sl.Context) + require.NoError(t, err) + + err = sl.ResolveRefsIn(doc, nil) + require.NoError(t, err) + + bs, err := doc.MarshalJSON() + require.NoError(t, err) + require.Equal(t, []byte(`{"components":{},"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`), bs) + + require.Equal(t, "string", doc.Paths["/testpath"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) +} diff --git a/openapi3/testdata/main.yaml b/openapi3/testdata/main.yaml new file mode 100644 index 000000000..e973cbecd --- /dev/null +++ b/openapi3/testdata/main.yaml @@ -0,0 +1,7 @@ +openapi: "3.0.0" +info: + title: "test file" + version: "n/a" +paths: + /testpath: + $ref: "testpath.yaml#/paths/~1testpath" diff --git a/openapi3/testdata/testpath.yaml b/openapi3/testdata/testpath.yaml new file mode 100644 index 000000000..de85bb418 --- /dev/null +++ b/openapi3/testdata/testpath.yaml @@ -0,0 +1,15 @@ +paths: + /testpath: + get: + responses: + "200": + $ref: "#/components/responses/testpath_200_response" + +components: + responses: + testpath_200_response: + description: a custom response + content: + application/json: + schema: + type: string From 8407517f2b22e64902956cdf0a4063f9b2eeb77b Mon Sep 17 00:00:00 2001 From: Steffen Rumpf <39158011+steffakasid@users.noreply.github.com> Date: Fri, 23 Apr 2021 14:41:06 +0200 Subject: [PATCH 064/260] [Bugfix] fail readURL on http code > 399 (#345) * [Bugfix] fail readUR if external reference returned http code > 399 * Replaced httpmock with httptest * Use require.EqualError instead testing Error and Contains of the errormessage * Test LoadSwaggerFromData as well --- openapi3/swagger_loader.go | 3 ++ openapi3/swagger_loader_http_error_test.go | 55 ++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 openapi3/swagger_loader_http_error_test.go diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 2cb951e53..517eca910 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -124,6 +124,9 @@ func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { return nil, err } defer resp.Body.Close() + if resp.StatusCode > 399 { + return nil, fmt.Errorf("request returned status code %d", resp.StatusCode) + } return ioutil.ReadAll(resp.Body) } if location.Scheme != "" || location.Host != "" || location.RawQuery != "" { diff --git a/openapi3/swagger_loader_http_error_test.go b/openapi3/swagger_loader_http_error_test.go new file mode 100644 index 000000000..3b3d06c0b --- /dev/null +++ b/openapi3/swagger_loader_http_error_test.go @@ -0,0 +1,55 @@ +package openapi3 + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadFromRemoteURLFailsWithHttpError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, "") + })) + defer ts.Close() + + spec := []byte(` +{ + "openapi": "3.0.0", + "info": { + "title": "", + "version": "1" + }, + "paths": { + "/test": { + "post": { + "responses": { + "default": { + "description": "test", + "headers": { + "X-TEST-HEADER": { + "$ref": "` + ts.URL + `/components.openapi.json#/components/headers/CustomTestHeader" + } + } + } + } + } + } + } +}`) + + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) + + require.Nil(t, swagger) + require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": request returned status code 400", ts.URL)) + + swagger, err = loader.LoadSwaggerFromData(spec) + require.Nil(t, swagger) + require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": request returned status code 400", ts.URL)) +} From a4e36cd21eb51476452efee483ba1f3c12701e5c Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 23 Apr 2021 16:19:19 +0200 Subject: [PATCH 065/260] Support loading documents with `filepath.FromSlash` (#251) --- openapi3/swagger_loader.go | 33 ++++++++++++----------- openapi3/swagger_loader_issue220_test.go | 27 +++++++++++++++++++ openapi3/testdata/my-openapi.json | 18 +++++++++++++ openapi3/testdata/my-other-openapi.json | 34 ++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 openapi3/swagger_loader_issue220_test.go create mode 100644 openapi3/testdata/my-openapi.json create mode 100644 openapi3/testdata/my-other-openapi.json diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 517eca910..65904d76b 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "path" + "path/filepath" "reflect" "strconv" "strings" @@ -64,8 +65,8 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swag } // LoadSwaggerFromFile loads a spec from a local file path -func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) { - return swaggerLoader.LoadSwaggerFromURI(&url.URL{Path: path}) +func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(location string) (*Swagger, error) { + return swaggerLoader.LoadSwaggerFromURI(&url.URL{Path: filepath.ToSlash(location)}) } func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL) (*Swagger, error) { @@ -150,16 +151,16 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, // LoadSwaggerFromDataWithPath takes the OpenApi spec data in bytes and a path where the resolver can find referred // elements and returns a *Swagger with all resolved data or an error if unable to load data or resolve refs. -func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, path *url.URL) (*Swagger, error) { +func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, location *url.URL) (*Swagger, error) { swaggerLoader.resetVisitedPathItemRefs() - return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, path) + return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location) } -func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []byte, path *url.URL) (*Swagger, error) { +func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []byte, location *url.URL) (*Swagger, error) { if swaggerLoader.visitedDocuments == nil { swaggerLoader.visitedDocuments = make(map[string]*Swagger) } - uri := path.String() + uri := location.String() if doc, ok := swaggerLoader.visitedDocuments[uri]; ok { return doc, nil } @@ -170,7 +171,7 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []b if err := yaml.Unmarshal(data, swagger); err != nil { return nil, err } - if err := swaggerLoader.ResolveRefsIn(swagger, path); err != nil { + if err := swaggerLoader.ResolveRefsIn(swagger, location); err != nil { return nil, err } @@ -178,7 +179,7 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []b } // ResolveRefsIn expands references if for instance spec was just unmarshalled -func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.URL) (err error) { +func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, location *url.URL) (err error) { if swaggerLoader.visitedPathItemRefs == nil { swaggerLoader.resetVisitedPathItemRefs() } @@ -186,37 +187,37 @@ func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.UR // Visit all components components := swagger.Components for _, component := range components.Headers { - if err = swaggerLoader.resolveHeaderRef(swagger, component, path); err != nil { + if err = swaggerLoader.resolveHeaderRef(swagger, component, location); err != nil { return } } for _, component := range components.Parameters { - if err = swaggerLoader.resolveParameterRef(swagger, component, path); err != nil { + if err = swaggerLoader.resolveParameterRef(swagger, component, location); err != nil { return } } for _, component := range components.RequestBodies { - if err = swaggerLoader.resolveRequestBodyRef(swagger, component, path); err != nil { + if err = swaggerLoader.resolveRequestBodyRef(swagger, component, location); err != nil { return } } for _, component := range components.Responses { - if err = swaggerLoader.resolveResponseRef(swagger, component, path); err != nil { + if err = swaggerLoader.resolveResponseRef(swagger, component, location); err != nil { return } } for _, component := range components.Schemas { - if err = swaggerLoader.resolveSchemaRef(swagger, component, path); err != nil { + if err = swaggerLoader.resolveSchemaRef(swagger, component, location); err != nil { return } } for _, component := range components.SecuritySchemes { - if err = swaggerLoader.resolveSecuritySchemeRef(swagger, component, path); err != nil { + if err = swaggerLoader.resolveSecuritySchemeRef(swagger, component, location); err != nil { return } } for _, component := range components.Examples { - if err = swaggerLoader.resolveExampleRef(swagger, component, path); err != nil { + if err = swaggerLoader.resolveExampleRef(swagger, component, location); err != nil { return } } @@ -226,7 +227,7 @@ func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.UR if pathItem == nil { continue } - if err = swaggerLoader.resolvePathItemRef(swagger, entrypoint, pathItem, path); err != nil { + if err = swaggerLoader.resolvePathItemRef(swagger, entrypoint, pathItem, location); err != nil { return } } diff --git a/openapi3/swagger_loader_issue220_test.go b/openapi3/swagger_loader_issue220_test.go new file mode 100644 index 000000000..14c4d648f --- /dev/null +++ b/openapi3/swagger_loader_issue220_test.go @@ -0,0 +1,27 @@ +package openapi3 + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue220(t *testing.T) { + for _, specPath := range []string{ + "testdata/my-openapi.json", + filepath.FromSlash("testdata/my-openapi.json"), + } { + t.Logf("specPath: %q", specPath) + + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadSwaggerFromFile(specPath) + require.NoError(t, err) + + 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) + } +} diff --git a/openapi3/testdata/my-openapi.json b/openapi3/testdata/my-openapi.json new file mode 100644 index 000000000..b75d9ff3e --- /dev/null +++ b/openapi3/testdata/my-openapi.json @@ -0,0 +1,18 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "My API", + "version": "0.1.0" + }, + "paths": { + "/foo": { + "get": { + "responses": { + "200": { + "$ref": "my-other-openapi.json#/components/responses/DefaultResponse" + } + } + } + } + } +} diff --git a/openapi3/testdata/my-other-openapi.json b/openapi3/testdata/my-other-openapi.json new file mode 100644 index 000000000..0c92486b3 --- /dev/null +++ b/openapi3/testdata/my-other-openapi.json @@ -0,0 +1,34 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "My other API", + "version": "0.1.0" + }, + "components": { + "schemas": { + "DefaultObject": { + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "integer" + } + } + } + }, + "responses": { + "DefaultResponse": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DefaultObject" + } + } + } + } + } + } +} From 1b47cceae470618e6cc396f57e81cffd5c529ef7 Mon Sep 17 00:00:00 2001 From: Steffen Rumpf <39158011+steffakasid@users.noreply.github.com> Date: Fri, 23 Apr 2021 21:43:22 +0200 Subject: [PATCH 066/260] [Bugfix] fixed error message when only file is referenced (#348) without internal reference --- openapi3/swagger_loader.go | 2 +- openapi3/swagger_loader_http_error_test.go | 50 ++++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 65904d76b..eaaf9bd06 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -126,7 +126,7 @@ func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { } defer resp.Body.Close() if resp.StatusCode > 399 { - return nil, fmt.Errorf("request returned status code %d", resp.StatusCode) + return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode) } return ioutil.ReadAll(resp.Body) } diff --git a/openapi3/swagger_loader_http_error_test.go b/openapi3/swagger_loader_http_error_test.go index 3b3d06c0b..ba361f406 100644 --- a/openapi3/swagger_loader_http_error_test.go +++ b/openapi3/swagger_loader_http_error_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestLoadFromRemoteURLFailsWithHttpError(t *testing.T) { +func TestLoadReferenceFromRemoteURLFailsWithHttpError(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, "") @@ -47,9 +47,53 @@ func TestLoadFromRemoteURLFailsWithHttpError(t *testing.T) { swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.Nil(t, swagger) - require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": request returned status code 400", ts.URL)) + require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL, ts.URL)) + + swagger, err = loader.LoadSwaggerFromData(spec) + require.Nil(t, swagger) + require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL, ts.URL)) +} + +func TestLoadFromRemoteURLFailsWithHttpError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, "") + })) + defer ts.Close() + + spec := []byte(` +{ + "openapi": "3.0.0", + "info": { + "title": "", + "version": "1" + }, + "paths": { + "/test": { + "post": { + "responses": { + "default": { + "description": "test", + "headers": { + "X-TEST-HEADER": { + "$ref": "` + ts.URL + `/components.openapi.json" + } + } + } + } + } + } + } +}`) + + loader := NewSwaggerLoader() + loader.IsExternalRefsAllowed = true + swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) + + require.Nil(t, swagger) + require.EqualError(t, err, fmt.Sprintf("error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL)) swagger, err = loader.LoadSwaggerFromData(spec) require.Nil(t, swagger) - require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": request returned status code 400", ts.URL)) + require.EqualError(t, err, fmt.Sprintf("error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL)) } From f6c20c3128c117d5e2e4eed3b76bea1d75494e86 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 23 Apr 2021 22:00:03 +0200 Subject: [PATCH 067/260] Follow callbacks references (#347) --- openapi3/issue301_test.go | 28 +++++ openapi3/swagger_loader.go | 103 ++++++++++++++++++- openapi3/testdata/callback-transactioned.yml | 10 ++ openapi3/testdata/callbacks.yml | 71 +++++++++++++ 4 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 openapi3/issue301_test.go create mode 100644 openapi3/testdata/callback-transactioned.yml create mode 100644 openapi3/testdata/callbacks.yml diff --git a/openapi3/issue301_test.go b/openapi3/issue301_test.go new file mode 100644 index 000000000..15298a569 --- /dev/null +++ b/openapi3/issue301_test.go @@ -0,0 +1,28 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue301(t *testing.T) { + sl := NewSwaggerLoader() + sl.IsExternalRefsAllowed = true + + doc, err := sl.LoadSwaggerFromFile("testdata/callbacks.yml") + require.NoError(t, err) + + 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) + + 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) +} diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index eaaf9bd06..d97f8034b 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -85,7 +85,7 @@ func (swaggerLoader *SwaggerLoader) allowsExternalRefs(ref string) (err error) { } // loadSingleElementFromURI reads the data from ref and unmarshals to the passed element. -func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) (*url.URL, error) { +func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element interface{}) (*url.URL, error) { if err := swaggerLoader.allowsExternalRefs(ref); err != nil { return nil, err } @@ -221,6 +221,11 @@ func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, location *ur return } } + for _, component := range components.Callbacks { + if err = swaggerLoader.resolveCallbackRef(swagger, component, location); err != nil { + return + } + } // Visit all operations for entrypoint, pathItem := range swagger.Paths { @@ -799,6 +804,94 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen return nil } +func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, component *CallbackRef, documentPath *url.URL) (err error) { + + if component == nil { + return errors.New("invalid callback: value MUST be an object") + } + if ref := component.Ref; ref != "" { + if isSingleRefElement(ref) { + var resolved Callback + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil { + return err + } + component.Value = &resolved + } else { + var resolved CallbackRef + componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + if err != nil { + return err + } + if err := swaggerLoader.resolveCallbackRef(swagger, &resolved, componentPath); err != nil { + return err + } + component.Value = resolved.Value + } + } + value := component.Value + if value == nil { + return nil + } + + for entrypoint, pathItem := range *value { + entrypoint, pathItem := entrypoint, pathItem + err = func() (err error) { + key := "-" + if documentPath != nil { + key = documentPath.EscapedPath() + } + key += entrypoint + if _, ok := swaggerLoader.visitedPathItemRefs[key]; ok { + return nil + } + swaggerLoader.visitedPathItemRefs[key] = struct{}{} + + if pathItem == nil { + return errors.New("invalid path item: value MUST be an object") + } + ref := pathItem.Ref + if ref != "" { + if isSingleRefElement(ref) { + var p PathItem + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { + return err + } + *pathItem = p + } else { + if swagger, ref, documentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, documentPath); err != nil { + return + } + + rest := strings.TrimPrefix(ref, "#/components/callbacks/") + if rest == ref { + return fmt.Errorf(`expected prefix "#/components/callbacks/" in URI %q`, ref) + } + id := unescapeRefString(rest) + + definitions := swagger.Components.Callbacks + if definitions == nil { + return failedToResolveRefFragmentPart(ref, "callbacks") + } + resolved := definitions[id] + if resolved == nil { + return failedToResolveRefFragmentPart(ref, id) + } + + for _, p := range *resolved.Value { + *pathItem = *p + break + } + } + } + return swaggerLoader.resolvePathItemRefContinued(swagger, pathItem, documentPath) + }() + if err != nil { + return err + } + } + return nil +} + func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedLink == nil { @@ -880,7 +973,10 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo *pathItem = *resolved } } + return swaggerLoader.resolvePathItemRefContinued(swagger, pathItem, documentPath) +} +func (swaggerLoader *SwaggerLoader) resolvePathItemRefContinued(swagger *Swagger, pathItem *PathItem, documentPath *url.URL) (err error) { for _, parameter := range pathItem.Parameters { if err = swaggerLoader.resolveParameterRef(swagger, parameter, documentPath); err != nil { return @@ -902,6 +998,11 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo return } } + for _, callback := range operation.Callbacks { + if err = swaggerLoader.resolveCallbackRef(swagger, callback, documentPath); err != nil { + return + } + } } return } diff --git a/openapi3/testdata/callback-transactioned.yml b/openapi3/testdata/callback-transactioned.yml new file mode 100644 index 000000000..2d58b394b --- /dev/null +++ b/openapi3/testdata/callback-transactioned.yml @@ -0,0 +1,10 @@ +post: + requestBody: + description: Callback payload + content: + 'application/json': + schema: + $ref: 'callbacks.yml#/components/schemas/SomePayload' + responses: + '200': + description: callback successfully processed diff --git a/openapi3/testdata/callbacks.yml b/openapi3/testdata/callbacks.yml new file mode 100644 index 000000000..4ad3f7d73 --- /dev/null +++ b/openapi3/testdata/callbacks.yml @@ -0,0 +1,71 @@ +openapi: 3.1.0 +info: + title: Callback refd + version: 1.2.3 +paths: + /trans: + post: + description: '' + requestBody: + description: '' + content: + 'application/json': + schema: + properties: + id: {type: string} + email: {format: email} + responses: + '201': + description: subscription successfully created + content: + application/json: + schema: + type: object + callbacks: + transactionCallback: + 'http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}': + $ref: callback-transactioned.yml + + /other: + post: + description: '' + parameters: + - name: queryUrl + in: query + required: true + description: | + bla + bla + bla + schema: + type: string + format: uri + example: https://example.com + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + callbacks: + myEvent: + $ref: '#/components/callbacks/MyCallbackEvent' + +components: + schemas: + SomePayload: {type: object} + SomeOtherPayload: {type: boolean} + callbacks: + MyCallbackEvent: + '{$request.query.queryUrl}': + post: + requestBody: + description: Callback payload + content: + 'application/json': + schema: + $ref: '#/components/schemas/SomeOtherPayload' + responses: + '200': + description: callback successfully processed From 38a7368210360ef88df8e594c41641ff7a19737b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 26 Apr 2021 18:26:13 +0200 Subject: [PATCH 068/260] CI: test go1.14 (#349) --- .github/workflows/go.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d095ebed6..c770eaff6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,6 +12,7 @@ jobs: strategy: fail-fast: true matrix: + go: ['1.14', '1.x'] # Locked at https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on os: - ubuntu-20.04 @@ -21,28 +22,32 @@ jobs: defaults: run: shell: bash + name: ${{ matrix.go }} on ${{ matrix.os }} steps: - uses: actions/setup-go@v2 with: - go-version: 1.x + go-version: ${{ matrix.go }} - id: go-cache-paths run: | echo "::set-output name=go-build::$(go env GOCACHE)" echo "::set-output name=go-mod::$(go env GOMODCACHE)" + - run: echo ${{ steps.go-cache-paths.outputs.go-build }} + - run: echo ${{ steps.go-cache-paths.outputs.go-mod }} - name: Go Build Cache uses: actions/cache@v2 with: path: ${{ steps.go-cache-paths.outputs.go-build }} - key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} + key: ${{ runner.os }}-go-${{ matrix.go }}-build-${{ hashFiles('**/go.sum') }} - - name: Go Mod Cache + - name: Go Mod Cache (go>=1.15) uses: actions/cache@v2 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} - key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} + key: ${{ runner.os }}-go-${{ matrix.go }}-mod-${{ hashFiles('**/go.sum') }} + if: matrix.go != '1.14' - uses: actions/checkout@v2 @@ -61,6 +66,7 @@ jobs: - run: go test ./... - run: cd openapi3/testdata && go test -tags with_embed ./... && cd - + if: matrix.go != '1.14' - if: runner.os == 'Linux' run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] From cc9e37b212eab86de474d58c64f390b717b7bb69 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 26 Apr 2021 20:20:59 +0200 Subject: [PATCH 069/260] Clean APIs from trademarked name "Swagger" (#351) --- README.md | 28 +- openapi2/doc.go | 7 + openapi2/openapi2.go | 24 +- openapi2/openapi2_test.go | 36 +- openapi2conv/doc.go | 2 + openapi2conv/issue187_test.go | 12 +- openapi2conv/openapi2_conv.go | 213 ++++++----- openapi2conv/openapi2_conv_test.go | 10 +- openapi3/callback.go | 4 +- openapi3/components.go | 14 +- openapi3/content.go | 6 +- openapi3/discriminator.go | 2 +- openapi3/discriminator_test.go | 4 +- openapi3/doc.go | 5 +- openapi3/encoding.go | 10 +- openapi3/examples.go | 5 + openapi3/extension_test.go | 4 +- openapi3/header.go | 4 +- openapi3/info.go | 10 +- openapi3/issue301_test.go | 4 +- openapi3/issue341_test.go | 4 +- openapi3/issue344_test.go | 4 +- openapi3/link.go | 2 +- openapi3/{swagger_loader.go => loader.go} | 352 +++++++++--------- ...loader_empty_response_description_test.go} | 16 +- ...rror_test.go => loader_http_error_test.go} | 20 +- ...sue212_test.go => loader_issue212_test.go} | 4 +- ...sue220_test.go => loader_issue220_test.go} | 4 +- ...sue235_test.go => loader_issue235_test.go} | 8 +- ...fs_test.go => loader_outside_refs_test.go} | 4 +- ...der_paths_test.go => loader_paths_test.go} | 4 +- ...t.go => loader_read_from_uri_func_test.go} | 22 +- ...f_test.go => loader_recursive_ref_test.go} | 4 +- ...s_test.go => loader_relative_refs_test.go} | 214 +++++------ ...{swagger_loader_test.go => loader_test.go} | 110 +++--- openapi3/media_type.go | 8 +- openapi3/{swagger.go => openapi3.go} | 43 +-- .../{swagger_test.go => openapi3_test.go} | 38 +- openapi3/operation.go | 14 +- openapi3/parameter.go | 74 ++-- openapi3/parameter_issue223_test.go | 2 +- openapi3/path_item.go | 6 +- openapi3/paths.go | 10 +- openapi3/paths_test.go | 4 +- openapi3/refs.go | 80 ++-- openapi3/refs_test.go | 4 +- openapi3/request_body.go | 6 +- openapi3/response.go | 16 +- openapi3/response_issue224_test.go | 2 +- openapi3/schema.go | 20 +- openapi3/schema_issue289_test.go | 2 +- openapi3/schema_test.go | 2 +- openapi3/security_requirements.go | 8 +- openapi3/security_scheme.go | 50 +-- openapi3/server.go | 26 +- openapi3/testdata/load_with_go_embed_test.go | 6 +- openapi3filter/req_resp_decoder_test.go | 2 +- openapi3filter/validate_readonly_test.go | 4 +- openapi3filter/validate_request.go | 30 +- openapi3filter/validate_request_input.go | 2 +- openapi3filter/validate_request_test.go | 4 +- openapi3filter/validate_response.go | 2 +- .../validation_discriminator_test.go | 4 +- openapi3filter/validation_error_test.go | 12 +- openapi3filter/validation_handler.go | 6 +- openapi3filter/validation_test.go | 14 +- routers/gorillamux/router.go | 6 +- routers/gorillamux/router_test.go | 2 +- routers/legacy/router.go | 10 +- routers/legacy/router_test.go | 2 +- routers/types.go | 2 +- 71 files changed, 863 insertions(+), 836 deletions(-) create mode 100644 openapi2/doc.go create mode 100644 openapi2conv/doc.go rename openapi3/{swagger_loader.go => loader.go} (54%) rename openapi3/{swagger_loader_empty_response_description_test.go => loader_empty_response_description_test.go} (83%) rename openapi3/{swagger_loader_http_error_test.go => loader_http_error_test.go} (84%) rename openapi3/{swagger_loader_issue212_test.go => loader_issue212_test.go} (96%) rename openapi3/{swagger_loader_issue220_test.go => loader_issue220_test.go} (87%) rename openapi3/{swagger_loader_issue235_test.go => loader_issue235_test.go} (69%) rename openapi3/{swagger_loader_outside_refs_test.go => loader_outside_refs_test.go} (81%) rename openapi3/{swagger_loader_paths_test.go => loader_paths_test.go} (84%) rename openapi3/{swagger_loader_read_from_uri_func_test.go => loader_read_from_uri_func_test.go} (75%) rename openapi3/{swagger_loader_recursive_ref_test.go => loader_recursive_ref_test.go} (81%) rename openapi3/{swagger_loader_relative_refs_test.go => loader_relative_refs_test.go} (71%) rename openapi3/{swagger_loader_test.go => loader_test.go} (77%) rename openapi3/{swagger.go => openapi3.go} (66%) rename openapi3/{swagger_test.go => openapi3_test.go} (93%) diff --git a/README.md b/README.md index 0f3f35abe..f147fe1be 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Here's some projects that depend on _kin-openapi_: * [https://github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" * [github.com/getkin/kin](https://github.com/getkin/kin) - "A configurable backend" * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" - * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPI 3 spec + * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPIv3 spec document * [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs" * [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "...a CLI for interacting with REST-ish HTTP APIs with some nice features built-in" * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Goa is a framework for building micro-services and APIs in Go using a unique design-first approach." @@ -44,15 +44,15 @@ Here's some projects that depend on _kin-openapi_: # Some recipes ## Loading OpenAPI document -Use `SwaggerLoader`, which resolves all references: +Use `openapi3.Loader`, which resolves all references: ```go -swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("swagger.json") +doc, err := openapi3.NewLoader().LoadFromFile("swagger.json") ``` ## Getting OpenAPI operation that matches request ```go -loader := openapi3.NewSwaggerLoader() -doc, _ := loader.LoadSwaggerFromData([]byte(`...`)) +loader := openapi3.NewLoader() +doc, _ := loader.LoadFromData([]byte(`...`)) _ := doc.Validate(loader.Context) router, _ := gorillamux.NewRouter(doc) route, pathParams, _ := router.FindRoute(httpRequest) @@ -76,8 +76,8 @@ import ( func main() { ctx := context.Background() - loader := &openapi3.SwaggerLoader{Context: ctx} - doc, _ := loader.LoadSwaggerFromFile("openapi3_spec.json") + loader := &openapi3.Loader{Context: ctx} + doc, _ := loader.LoadFromFile("openapi3_spec.json") _ := doc.Validate(ctx) router, _ := legacyrouter.NewRouter(doc) httpReq, _ := http.NewRequest(http.MethodGet, "/items", nil) @@ -193,6 +193,20 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.61.0 +* Renamed `openapi2.Swagger` to `openapi2.T`. +* Renamed `openapi2conv.FromV3Swagger` to `openapi2conv.FromV3`. +* Renamed `openapi2conv.ToV3Swagger` to `openapi2conv.ToV3`. +* Renamed `openapi3.LoadSwaggerFromData` to `openapi3.LoadFromData`. +* Renamed `openapi3.LoadSwaggerFromDataWithPath` to `openapi3.LoadFromDataWithPath`. +* Renamed `openapi3.LoadSwaggerFromFile` to `openapi3.LoadFromFile`. +* Renamed `openapi3.LoadSwaggerFromURI` to `openapi3.LoadFromURI`. +* Renamed `openapi3.NewSwaggerLoader` to `openapi3.NewLoader`. +* Renamed `openapi3.Swagger` to `openapi3.T`. +* Renamed `openapi3.SwaggerLoader` to `openapi3.Loader`. +* Renamed `openapi3filter.ValidationHandler.SwaggerFile` to `openapi3filter.ValidationHandler.File`. +* Renamed `routers.Route.Swagger` to `routers.Route.Spec`. + ### v0.51.0 * Type `openapi3filter.Route` moved to `routers` (and `Route.Handler` was dropped. See https://github.com/getkin/kin-openapi/issues/329) * Type `openapi3filter.RouteError` moved to `routers` (so did `ErrPathNotFound` and `ErrMethodNotAllowed` which are now `RouteError`s) diff --git a/openapi2/doc.go b/openapi2/doc.go new file mode 100644 index 000000000..b4762d597 --- /dev/null +++ b/openapi2/doc.go @@ -0,0 +1,7 @@ +// Package openapi2 parses and writes OpenAPIv2 specification documents. +// +// Does not cover all elements of OpenAPIv2. +// When OpenAPI version 3 is backwards-compatible with version 2, version 3 elements have been used. +// +// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md +package openapi2 diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 8797c573a..937cc0831 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -1,10 +1,3 @@ -// Package openapi2 parses and writes OpenAPI 2 specifications. -// -// Does not cover all elements of OpenAPI 2. -// When OpenAPI version 3 is backwards-compatible with version 2, version 3 elements have been used. -// -// The specification: -// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md package openapi2 import ( @@ -16,7 +9,8 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -type Swagger struct { +// T is the root of an OpenAPI v2 document +type T struct { openapi3.ExtensionProps Swagger string `json:"swagger"` Info openapi3.Info `json:"info"` @@ -34,19 +28,19 @@ type Swagger struct { Tags openapi3.Tags `json:"tags,omitempty"` } -func (swagger *Swagger) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(swagger) +func (doc *T) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(doc) } -func (swagger *Swagger) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, swagger) +func (doc *T) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, doc) } -func (swagger *Swagger) AddOperation(path string, method string, operation *Operation) { - paths := swagger.Paths +func (doc *T) AddOperation(path string, method string, operation *Operation) { + paths := doc.Paths if paths == nil { paths = make(map[string]*PathItem, 8) - swagger.Paths = paths + doc.Paths = paths } pathItem := paths[path] if pathItem == nil { diff --git a/openapi2/openapi2_test.go b/openapi2/openapi2_test.go index 1a5135d05..87bd5ce41 100644 --- a/openapi2/openapi2_test.go +++ b/openapi2/openapi2_test.go @@ -1,24 +1,36 @@ -package openapi2 +package openapi2_test import ( "encoding/json" + "fmt" "io/ioutil" - "testing" + "reflect" - "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi2" ) -func TestReadingSwagger(t *testing.T) { - var swagger Swagger - +func Example() { input, err := ioutil.ReadFile("testdata/swagger.json") - require.NoError(t, err) + if err != nil { + panic(err) + } - err = json.Unmarshal(input, &swagger) - require.NoError(t, err) + var doc openapi2.T + if err = json.Unmarshal(input, &doc); err != nil { + panic(err) + } - output, err := json.Marshal(swagger) - require.NoError(t, err) + output, err := json.Marshal(doc) + if err != nil { + panic(err) + } - require.JSONEq(t, string(input), string(output)) + var docAgain openapi2.T + if err = json.Unmarshal(output, &docAgain); err != nil { + panic(err) + } + if !reflect.DeepEqual(doc, docAgain) { + fmt.Println("objects doc & docAgain should be the same") + } + // Output: } diff --git a/openapi2conv/doc.go b/openapi2conv/doc.go new file mode 100644 index 000000000..7b87ec224 --- /dev/null +++ b/openapi2conv/doc.go @@ -0,0 +1,2 @@ +// Package openapi2conv converts an OpenAPI v2 specification document to v3. +package openapi2conv diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index 16a6d1a1c..7ca85cace 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -11,21 +11,21 @@ import ( "github.com/stretchr/testify/require" ) -func v2v3JSON(spec2 []byte) (doc3 *openapi3.Swagger, err error) { - var doc2 openapi2.Swagger +func v2v3JSON(spec2 []byte) (doc3 *openapi3.T, err error) { + var doc2 openapi2.T if err = json.Unmarshal(spec2, &doc2); err != nil { return } - doc3, err = ToV3Swagger(&doc2) + doc3, err = ToV3(&doc2) return } -func v2v3YAML(spec2 []byte) (doc3 *openapi3.Swagger, err error) { - var doc2 openapi2.Swagger +func v2v3YAML(spec2 []byte) (doc3 *openapi3.T, err error) { + var doc2 openapi2.T if err = yaml.Unmarshal(spec2, &doc2); err != nil { return } - doc3, err = ToV3Swagger(&doc2) + doc3, err = ToV3(&doc2) return } diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 2622f76dc..05447ac67 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -1,4 +1,3 @@ -// Package openapi2conv converts an OpenAPI v2 specification to v3. package openapi2conv import ( @@ -13,116 +12,116 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -// ToV3Swagger converts an OpenAPIv2 spec to an OpenAPIv3 spec -func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) { - stripNonCustomExtensions(swagger.Extensions) +// ToV3 converts an OpenAPIv2 spec to an OpenAPIv3 spec +func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { + stripNonCustomExtensions(doc2.Extensions) - result := &openapi3.Swagger{ + doc3 := &openapi3.T{ OpenAPI: "3.0.3", - Info: &swagger.Info, + Info: &doc2.Info, Components: openapi3.Components{}, - Tags: swagger.Tags, - ExtensionProps: swagger.ExtensionProps, - ExternalDocs: swagger.ExternalDocs, + Tags: doc2.Tags, + ExtensionProps: doc2.ExtensionProps, + ExternalDocs: doc2.ExternalDocs, } - if host := swagger.Host; host != "" { - schemes := swagger.Schemes + if host := doc2.Host; host != "" { + schemes := doc2.Schemes if len(schemes) == 0 { schemes = []string{"https://"} } - basePath := swagger.BasePath + basePath := doc2.BasePath for _, scheme := range schemes { u := url.URL{ Scheme: scheme, Host: host, Path: basePath, } - result.AddServer(&openapi3.Server{URL: u.String()}) + doc3.AddServer(&openapi3.Server{URL: u.String()}) } } - result.Components.Schemas = make(map[string]*openapi3.SchemaRef) - if parameters := swagger.Parameters; len(parameters) != 0 { - result.Components.Parameters = make(map[string]*openapi3.ParameterRef) - result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) + doc3.Components.Schemas = make(map[string]*openapi3.SchemaRef) + if parameters := doc2.Parameters; len(parameters) != 0 { + doc3.Components.Parameters = make(map[string]*openapi3.ParameterRef) + doc3.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) for k, parameter := range parameters { - v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&result.Components, parameter, swagger.Consumes) + v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&doc3.Components, parameter, doc2.Consumes) switch { case err != nil: return nil, err case v3RequestBody != nil: - result.Components.RequestBodies[k] = v3RequestBody + doc3.Components.RequestBodies[k] = v3RequestBody case v3SchemaMap != nil: for _, v3Schema := range v3SchemaMap { - result.Components.Schemas[k] = v3Schema + doc3.Components.Schemas[k] = v3Schema } default: - result.Components.Parameters[k] = v3Parameter + doc3.Components.Parameters[k] = v3Parameter } } } - if paths := swagger.Paths; len(paths) != 0 { - resultPaths := make(map[string]*openapi3.PathItem, len(paths)) + if paths := doc2.Paths; len(paths) != 0 { + doc3Paths := make(map[string]*openapi3.PathItem, len(paths)) for path, pathItem := range paths { - r, err := ToV3PathItem(swagger, &result.Components, pathItem, swagger.Consumes) + r, err := ToV3PathItem(doc2, &doc3.Components, pathItem, doc2.Consumes) if err != nil { return nil, err } - resultPaths[path] = r + doc3Paths[path] = r } - result.Paths = resultPaths + doc3.Paths = doc3Paths } - if responses := swagger.Responses; len(responses) != 0 { - result.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses)) + if responses := doc2.Responses; len(responses) != 0 { + doc3.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses)) for k, response := range responses { r, err := ToV3Response(response) if err != nil { return nil, err } - result.Components.Responses[k] = r + doc3.Components.Responses[k] = r } } - for key, schema := range ToV3Schemas(swagger.Definitions) { - result.Components.Schemas[key] = schema + for key, schema := range ToV3Schemas(doc2.Definitions) { + doc3.Components.Schemas[key] = schema } - if m := swagger.SecurityDefinitions; len(m) != 0 { - resultSecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef) + if m := doc2.SecurityDefinitions; len(m) != 0 { + doc3SecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef) for k, v := range m { r, err := ToV3SecurityScheme(v) if err != nil { return nil, err } - resultSecuritySchemes[k] = r + doc3SecuritySchemes[k] = r } - result.Components.SecuritySchemes = resultSecuritySchemes + doc3.Components.SecuritySchemes = doc3SecuritySchemes } - result.Security = ToV3SecurityRequirements(swagger.Security) + doc3.Security = ToV3SecurityRequirements(doc2.Security) { - sl := openapi3.NewSwaggerLoader() - if err := sl.ResolveRefsIn(result, nil); err != nil { + sl := openapi3.NewLoader() + if err := sl.ResolveRefsIn(doc3, nil); err != nil { return nil, err } } - return result, nil + return doc3, nil } -func ToV3PathItem(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) { +func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) { stripNonCustomExtensions(pathItem.Extensions) - result := &openapi3.PathItem{ + doc3 := &openapi3.PathItem{ ExtensionProps: pathItem.ExtensionProps, } for method, operation := range pathItem.Operations() { - resultOperation, err := ToV3Operation(swagger, components, pathItem, operation, consumes) + doc3Operation, err := ToV3Operation(doc2, components, pathItem, operation, consumes) if err != nil { return nil, err } - result.SetOperation(method, resultOperation) + doc3.SetOperation(method, doc3Operation) } for _, parameter := range pathItem.Parameters { v3Parameter, v3RequestBody, v3Schema, err := ToV3Parameter(components, parameter, consumes) @@ -134,18 +133,18 @@ func ToV3PathItem(swagger *openapi2.Swagger, components *openapi3.Components, pa case v3Schema != nil: return nil, errors.New("pathItem must not have a schema parameter") default: - result.Parameters = append(result.Parameters, v3Parameter) + doc3.Parameters = append(doc3.Parameters, v3Parameter) } } - return result, nil + return doc3, nil } -func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation, consumes []string) (*openapi3.Operation, error) { +func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation, consumes []string) (*openapi3.Operation, error) { if operation == nil { return nil, nil } stripNonCustomExtensions(operation.Extensions) - result := &openapi3.Operation{ + doc3 := &openapi3.Operation{ OperationID: operation.OperationID, Summary: operation.Summary, Description: operation.Description, @@ -153,8 +152,8 @@ func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, p ExtensionProps: operation.ExtensionProps, } if v := operation.Security; v != nil { - resultSecurity := ToV3SecurityRequirements(*v) - result.Security = &resultSecurity + doc3Security := ToV3SecurityRequirements(*v) + doc3.Security = &doc3Security } if len(operation.Consumes) > 0 { @@ -175,26 +174,26 @@ func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, p formDataSchemas[key] = v3Schema } default: - result.Parameters = append(result.Parameters, v3Parameter) + doc3.Parameters = append(doc3.Parameters, v3Parameter) } } var err error - if result.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components, consumes); err != nil { + if doc3.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components, consumes); err != nil { return nil, err } if responses := operation.Responses; responses != nil { - resultResponses := make(openapi3.Responses, len(responses)) + doc3Responses := make(openapi3.Responses, len(responses)) for k, response := range responses { - result, err := ToV3Response(response) + doc3, err := ToV3Response(response) if err != nil { return nil, err } - resultResponses[k] = result + doc3Responses[k] = doc3 } - result.Responses = resultResponses + doc3.Responses = doc3Responses } - return result, nil + return doc3, nil } func getParameterNameFromOldRef(ref string) string { @@ -409,9 +408,7 @@ func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) { if schemaRef := response.Schema; schemaRef != nil { result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef)) } - return &openapi3.ResponseRef{ - Value: result, - }, nil + return &openapi3.ResponseRef{Value: result}, nil } func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef { @@ -528,28 +525,28 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu }, nil } -// FromV3Swagger converts an OpenAPIv3 spec to an OpenAPIv2 spec -func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { - resultResponses, err := FromV3Responses(swagger.Components.Responses, &swagger.Components) +// FromV3 converts an OpenAPIv3 spec to an OpenAPIv2 spec +func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { + doc2Responses, err := FromV3Responses(doc3.Components.Responses, &doc3.Components) if err != nil { return nil, err } - stripNonCustomExtensions(swagger.Extensions) - schemas, parameters := FromV3Schemas(swagger.Components.Schemas, &swagger.Components) - result := &openapi2.Swagger{ + stripNonCustomExtensions(doc3.Extensions) + schemas, parameters := FromV3Schemas(doc3.Components.Schemas, &doc3.Components) + doc2 := &openapi2.T{ Swagger: "2.0", - Info: *swagger.Info, + Info: *doc3.Info, Definitions: schemas, Parameters: parameters, - Responses: resultResponses, - Tags: swagger.Tags, - ExtensionProps: swagger.ExtensionProps, - ExternalDocs: swagger.ExternalDocs, + Responses: doc2Responses, + Tags: doc3.Tags, + ExtensionProps: doc3.ExtensionProps, + ExternalDocs: doc3.ExternalDocs, } isHTTPS := false isHTTP := false - servers := swagger.Servers + servers := doc3.Servers for i, server := range servers { parsedURL, err := url.Parse(server.URL) if err == nil { @@ -561,85 +558,85 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) { } // The first server is assumed to provide the base path if i == 0 { - result.Host = parsedURL.Host - result.BasePath = parsedURL.Path + doc2.Host = parsedURL.Host + doc2.BasePath = parsedURL.Path } } } if isHTTPS { - result.Schemes = append(result.Schemes, "https") + doc2.Schemes = append(doc2.Schemes, "https") } if isHTTP { - result.Schemes = append(result.Schemes, "http") + doc2.Schemes = append(doc2.Schemes, "http") } - for path, pathItem := range swagger.Paths { + for path, pathItem := range doc3.Paths { if pathItem == nil { continue } - result.AddOperation(path, "GET", nil) + doc2.AddOperation(path, "GET", nil) stripNonCustomExtensions(pathItem.Extensions) - addPathExtensions(result, path, pathItem.ExtensionProps) + addPathExtensions(doc2, path, pathItem.ExtensionProps) for method, operation := range pathItem.Operations() { if operation == nil { continue } - resultOperation, err := FromV3Operation(swagger, operation) + doc2Operation, err := FromV3Operation(doc3, operation) if err != nil { return nil, err } - result.AddOperation(path, method, resultOperation) + doc2.AddOperation(path, method, doc2Operation) } params := openapi2.Parameters{} for _, param := range pathItem.Parameters { - p, err := FromV3Parameter(param, &swagger.Components) + p, err := FromV3Parameter(param, &doc3.Components) if err != nil { return nil, err } params = append(params, p) } sort.Sort(params) - result.Paths[path].Parameters = params + doc2.Paths[path].Parameters = params } - for name, param := range swagger.Components.Parameters { - if result.Parameters[name], err = FromV3Parameter(param, &swagger.Components); err != nil { + for name, param := range doc3.Components.Parameters { + if doc2.Parameters[name], err = FromV3Parameter(param, &doc3.Components); err != nil { return nil, err } } - for name, requestBodyRef := range swagger.Components.RequestBodies { - bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, &swagger.Components) + for name, requestBodyRef := range doc3.Components.RequestBodies { + bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, &doc3.Components) if err != nil { return nil, err } if len(formDataParameters) != 0 { for _, param := range formDataParameters { - result.Parameters[param.Name] = param + doc2.Parameters[param.Name] = param } } else if len(bodyOrRefParameters) != 0 { for _, param := range bodyOrRefParameters { - result.Parameters[name] = param + doc2.Parameters[name] = param } } if len(consumes) != 0 { - result.Consumes = consumesToArray(consumes) + doc2.Consumes = consumesToArray(consumes) } } - if m := swagger.Components.SecuritySchemes; m != nil { - resultSecuritySchemes := make(map[string]*openapi2.SecurityScheme) + if m := doc3.Components.SecuritySchemes; m != nil { + doc2SecuritySchemes := make(map[string]*openapi2.SecurityScheme) for id, securityScheme := range m { - v, err := FromV3SecurityScheme(swagger, securityScheme) + v, err := FromV3SecurityScheme(doc3, securityScheme) if err != nil { return nil, err } - resultSecuritySchemes[id] = v + doc2SecuritySchemes[id] = v } - result.SecurityDefinitions = resultSecuritySchemes + doc2.SecurityDefinitions = doc2SecuritySchemes } - result.Security = FromV3SecurityRequirements(swagger.Security) - return result, nil + doc2.Security = FromV3SecurityRequirements(doc3.Security) + return doc2, nil } func consumesToArray(consumes map[string]struct{}) []string { @@ -662,7 +659,7 @@ func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, c return } - //Only select one formData or request body for an individual requesstBody as swagger 2 does not support multiples + //Only select one formData or request body for an individual requesstBody as OpenAPI 2 does not support multiples if requestBodyRef.Value != nil { for contentType, mediaType := range requestBodyRef.Value.Content { if consumes == nil { @@ -789,20 +786,20 @@ func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) open return result } -func FromV3PathItem(swagger *openapi3.Swagger, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) { +func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) { stripNonCustomExtensions(pathItem.Extensions) result := &openapi2.PathItem{ ExtensionProps: pathItem.ExtensionProps, } for method, operation := range pathItem.Operations() { - r, err := FromV3Operation(swagger, operation) + r, err := FromV3Operation(doc3, operation) if err != nil { return nil, err } result.SetOperation(method, r) } for _, parameter := range pathItem.Parameters { - p, err := FromV3Parameter(parameter, &swagger.Components) + p, err := FromV3Parameter(parameter, &doc3.Components) if err != nil { return nil, err } @@ -875,7 +872,7 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter return parameters } -func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (*openapi2.Operation, error) { +func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2.Operation, error) { if operation == nil { return nil, nil } @@ -892,7 +889,7 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( result.Security = &resultSecurity } for _, parameter := range operation.Parameters { - r, err := FromV3Parameter(parameter, &swagger.Components) + r, err := FromV3Parameter(parameter, &doc3.Components) if err != nil { return nil, err } @@ -905,7 +902,7 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( return nil, errors.New("could not find a name for request body") } - bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, &swagger.Components) + bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, &doc3.Components) if err != nil { return nil, err } @@ -926,7 +923,7 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) ( sort.Sort(result.Parameters) if responses := operation.Responses; responses != nil { - resultResponses, err := FromV3Responses(responses, &swagger.Components) + resultResponses, err := FromV3Responses(responses, &doc3.Components) if err != nil { return nil, err } @@ -1032,7 +1029,7 @@ func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) return result, nil } -func FromV3SecurityScheme(swagger *openapi3.Swagger, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) { +func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) { securityScheme := ref.Value if securityScheme == nil { return nil, nil @@ -1095,11 +1092,11 @@ func stripNonCustomExtensions(extensions map[string]interface{}) { } } -func addPathExtensions(swagger *openapi2.Swagger, path string, extensionProps openapi3.ExtensionProps) { - paths := swagger.Paths +func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.ExtensionProps) { + paths := doc2.Paths if paths == nil { paths = make(map[string]*openapi2.PathItem, 8) - swagger.Paths = paths + doc2.Paths = paths } pathItem := paths[path] if pathItem == nil { diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index f284ba4cc..adb9b0814 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -11,19 +11,19 @@ import ( ) func TestConvOpenAPIV3ToV2(t *testing.T) { - var doc3 openapi3.Swagger + var doc3 openapi3.T err := json.Unmarshal([]byte(exampleV3), &doc3) require.NoError(t, err) { // Refs need resolving before we can Validate - sl := openapi3.NewSwaggerLoader() + sl := openapi3.NewLoader() err = sl.ResolveRefsIn(&doc3, nil) require.NoError(t, err) err = doc3.Validate(context.Background()) require.NoError(t, err) } - spec2, err := FromV3Swagger(&doc3) + spec2, err := FromV3(&doc3) require.NoError(t, err) data, err := json.Marshal(spec2) require.NoError(t, err) @@ -31,11 +31,11 @@ func TestConvOpenAPIV3ToV2(t *testing.T) { } func TestConvOpenAPIV2ToV3(t *testing.T) { - var doc2 openapi2.Swagger + var doc2 openapi2.T err := json.Unmarshal([]byte(exampleV2), &doc2) require.NoError(t, err) - spec3, err := ToV3Swagger(&doc2) + spec3, err := ToV3(&doc2) require.NoError(t, err) err = spec3.Validate(context.Background()) require.NoError(t, err) diff --git a/openapi3/callback.go b/openapi3/callback.go index b216d1a9d..8995e4792 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -26,9 +26,9 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) { // Callback is specified by OpenAPI/Swagger standard version 3.0. type Callback map[string]*PathItem -func (value Callback) Validate(c context.Context) error { +func (value Callback) Validate(ctx context.Context) error { for _, v := range value { - if err := v.Validate(c); err != nil { + if err := v.Validate(ctx); err != nil { return err } } diff --git a/openapi3/components.go b/openapi3/components.go index e01f961d2..7acafabf9 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -34,12 +34,12 @@ func (components *Components) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, components) } -func (components *Components) Validate(c context.Context) (err error) { +func (components *Components) Validate(ctx context.Context) (err error) { for k, v := range components.Schemas { if err = ValidateIdentifier(k); err != nil { return } - if err = v.Validate(c); err != nil { + if err = v.Validate(ctx); err != nil { return } } @@ -48,7 +48,7 @@ func (components *Components) Validate(c context.Context) (err error) { if err = ValidateIdentifier(k); err != nil { return } - if err = v.Validate(c); err != nil { + if err = v.Validate(ctx); err != nil { return } } @@ -57,7 +57,7 @@ func (components *Components) Validate(c context.Context) (err error) { if err = ValidateIdentifier(k); err != nil { return } - if err = v.Validate(c); err != nil { + if err = v.Validate(ctx); err != nil { return } } @@ -66,7 +66,7 @@ func (components *Components) Validate(c context.Context) (err error) { if err = ValidateIdentifier(k); err != nil { return } - if err = v.Validate(c); err != nil { + if err = v.Validate(ctx); err != nil { return } } @@ -75,7 +75,7 @@ func (components *Components) Validate(c context.Context) (err error) { if err = ValidateIdentifier(k); err != nil { return } - if err = v.Validate(c); err != nil { + if err = v.Validate(ctx); err != nil { return } } @@ -84,7 +84,7 @@ func (components *Components) Validate(c context.Context) (err error) { if err = ValidateIdentifier(k); err != nil { return } - if err = v.Validate(c); err != nil { + if err = v.Validate(ctx); err != nil { return } } diff --git a/openapi3/content.go b/openapi3/content.go index abe376e3e..5edb7d3fa 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -104,10 +104,10 @@ func (content Content) Get(mime string) *MediaType { return content["*/*"] } -func (content Content) Validate(c context.Context) error { - for _, v := range content { +func (value Content) Validate(ctx context.Context) error { + for _, v := range value { // Validate MediaType - if err := v.Validate(c); err != nil { + if err := v.Validate(ctx); err != nil { return err } } diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index de518d578..82ad7040b 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -21,6 +21,6 @@ func (value *Discriminator) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } -func (value *Discriminator) Validate(c context.Context) error { +func (value *Discriminator) Validate(ctx context.Context) error { return nil } diff --git a/openapi3/discriminator_test.go b/openapi3/discriminator_test.go index c85548122..7c16992cf 100644 --- a/openapi3/discriminator_test.go +++ b/openapi3/discriminator_test.go @@ -45,8 +45,8 @@ func TestParsingDiscriminator(t *testing.T) { } ` - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData([]byte(spec)) + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(loader.Context) diff --git a/openapi3/doc.go b/openapi3/doc.go index efe8b4a0c..fc2735cb7 100644 --- a/openapi3/doc.go +++ b/openapi3/doc.go @@ -1,5 +1,4 @@ -// Package openapi3 parses and writes OpenAPI 3 specifications. +// Package openapi3 parses and writes OpenAPI 3 specification documents. // -// The OpenAPI 3.0 specification can be found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md +// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md package openapi3 diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 4bf657f70..ad48b9160 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -61,21 +61,21 @@ func (encoding *Encoding) SerializationMethod() *SerializationMethod { return sm } -func (encoding *Encoding) Validate(c context.Context) error { - if encoding == nil { +func (value *Encoding) Validate(ctx context.Context) error { + if value == nil { return nil } - for k, v := range encoding.Headers { + for k, v := range value.Headers { if err := ValidateIdentifier(k); err != nil { return nil } - if err := v.Validate(c); err != nil { + if err := v.Validate(ctx); err != nil { return nil } } // Validate a media types's serialization method. - sm := encoding.SerializationMethod() + sm := value.SerializationMethod() switch { case sm.Style == SerializationForm && sm.Explode, sm.Style == SerializationForm && !sm.Explode, diff --git a/openapi3/examples.go b/openapi3/examples.go index 98f79b884..f7f90ce54 100644 --- a/openapi3/examples.go +++ b/openapi3/examples.go @@ -1,6 +1,7 @@ package openapi3 import ( + "context" "fmt" "github.com/getkin/kin-openapi/jsoninfo" @@ -46,3 +47,7 @@ func (example *Example) MarshalJSON() ([]byte, error) { func (example *Example) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, example) } + +func (value *Example) Validate(ctx context.Context) error { + return nil // TODO +} diff --git a/openapi3/extension_test.go b/openapi3/extension_test.go index 22ed6af8e..9d009024e 100644 --- a/openapi3/extension_test.go +++ b/openapi3/extension_test.go @@ -10,9 +10,9 @@ import ( ) func ExampleExtensionProps_DecodeWith() { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - spec, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.json") + spec, err := loader.LoadFromFile("testdata/testref.openapi.json") if err != nil { panic(err) } diff --git a/openapi3/header.go b/openapi3/header.go index bab30a412..7cf61f8c6 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -43,9 +43,9 @@ func (value *Header) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } -func (value *Header) Validate(c context.Context) error { +func (value *Header) Validate(ctx context.Context) error { if v := value.Schema; v != nil { - if err := v.Validate(c); err != nil { + if err := v.Validate(ctx); err != nil { return err } } diff --git a/openapi3/info.go b/openapi3/info.go index 386ae861c..2adffff1a 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -26,15 +26,15 @@ func (value *Info) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } -func (value *Info) Validate(c context.Context) error { +func (value *Info) Validate(ctx context.Context) error { if contact := value.Contact; contact != nil { - if err := contact.Validate(c); err != nil { + if err := contact.Validate(ctx); err != nil { return err } } if license := value.License; license != nil { - if err := license.Validate(c); err != nil { + if err := license.Validate(ctx); err != nil { return err } } @@ -66,7 +66,7 @@ func (value *Contact) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } -func (value *Contact) Validate(c context.Context) error { +func (value *Contact) Validate(ctx context.Context) error { return nil } @@ -85,7 +85,7 @@ func (value *License) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } -func (value *License) Validate(c context.Context) error { +func (value *License) Validate(ctx context.Context) error { if value.Name == "" { return errors.New("value of license name must be a non-empty string") } diff --git a/openapi3/issue301_test.go b/openapi3/issue301_test.go index 15298a569..a0225fdb8 100644 --- a/openapi3/issue301_test.go +++ b/openapi3/issue301_test.go @@ -7,10 +7,10 @@ import ( ) func TestIssue301(t *testing.T) { - sl := NewSwaggerLoader() + sl := NewLoader() sl.IsExternalRefsAllowed = true - doc, err := sl.LoadSwaggerFromFile("testdata/callbacks.yml") + doc, err := sl.LoadFromFile("testdata/callbacks.yml") require.NoError(t, err) err = doc.Validate(sl.Context) diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go index c026d6d4f..93364d0e8 100644 --- a/openapi3/issue341_test.go +++ b/openapi3/issue341_test.go @@ -7,9 +7,9 @@ import ( ) func TestIssue341(t *testing.T) { - sl := NewSwaggerLoader() + sl := NewLoader() sl.IsExternalRefsAllowed = true - doc, err := sl.LoadSwaggerFromFile("testdata/main.yaml") + doc, err := sl.LoadFromFile("testdata/main.yaml") require.NoError(t, err) err = doc.Validate(sl.Context) diff --git a/openapi3/issue344_test.go b/openapi3/issue344_test.go index 8a53394c5..44ba2b7f5 100644 --- a/openapi3/issue344_test.go +++ b/openapi3/issue344_test.go @@ -7,10 +7,10 @@ import ( ) func TestIssue344(t *testing.T) { - sl := NewSwaggerLoader() + sl := NewLoader() sl.IsExternalRefsAllowed = true - doc, err := sl.LoadSwaggerFromFile("testdata/spec.yaml") + doc, err := sl.LoadFromFile("testdata/spec.yaml") require.NoError(t, err) err = doc.Validate(sl.Context) diff --git a/openapi3/link.go b/openapi3/link.go index 722c166d2..7d627b8bc 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -44,7 +44,7 @@ func (value *Link) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } -func (value *Link) Validate(c context.Context) error { +func (value *Link) Validate(ctx context.Context) error { if value.OperationID == "" && value.OperationRef == "" { return errors.New("missing operationId or operationRef on link") } diff --git a/openapi3/swagger_loader.go b/openapi3/loader.go similarity index 54% rename from openapi3/swagger_loader.go rename to openapi3/loader.go index d97f8034b..ff931825f 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/loader.go @@ -25,19 +25,19 @@ func failedToResolveRefFragmentPart(value, what string) error { return fmt.Errorf("failed to resolve %q in fragment in URI: %q", what, value) } -// SwaggerLoader helps deserialize a Swagger object -type SwaggerLoader struct { +// Loader helps deserialize an OpenAPIv3 document +type Loader struct { // IsExternalRefsAllowed enables visiting other files IsExternalRefsAllowed bool // ReadFromURIFunc allows overriding the any file/URL reading func - ReadFromURIFunc func(loader *SwaggerLoader, url *url.URL) ([]byte, error) + ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error) Context context.Context visitedPathItemRefs map[string]struct{} - visitedDocuments map[string]*Swagger + visitedDocuments map[string]*T visitedExample map[*Example]struct{} visitedHeader map[*Header]struct{} @@ -49,44 +49,44 @@ type SwaggerLoader struct { visitedSecurityScheme map[*SecurityScheme]struct{} } -// NewSwaggerLoader returns an empty SwaggerLoader -func NewSwaggerLoader() *SwaggerLoader { - return &SwaggerLoader{} +// NewLoader returns an empty Loader +func NewLoader() *Loader { + return &Loader{} } -func (swaggerLoader *SwaggerLoader) resetVisitedPathItemRefs() { - swaggerLoader.visitedPathItemRefs = make(map[string]struct{}) +func (loader *Loader) resetVisitedPathItemRefs() { + loader.visitedPathItemRefs = make(map[string]struct{}) } -// LoadSwaggerFromURI loads a spec from a remote URL -func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swagger, error) { - swaggerLoader.resetVisitedPathItemRefs() - return swaggerLoader.loadSwaggerFromURIInternal(location) +// LoadFromURI loads a spec from a remote URL +func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) { + loader.resetVisitedPathItemRefs() + return loader.loadFromURIInternal(location) } -// LoadSwaggerFromFile loads a spec from a local file path -func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(location string) (*Swagger, error) { - return swaggerLoader.LoadSwaggerFromURI(&url.URL{Path: filepath.ToSlash(location)}) +// LoadFromFile loads a spec from a local file path +func (loader *Loader) LoadFromFile(location string) (*T, error) { + return loader.LoadFromURI(&url.URL{Path: filepath.ToSlash(location)}) } -func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL) (*Swagger, error) { - data, err := swaggerLoader.readURL(location) +func (loader *Loader) loadFromURIInternal(location *url.URL) (*T, error) { + data, err := loader.readURL(location) if err != nil { return nil, err } - return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location) + return loader.loadFromDataWithPathInternal(data, location) } -func (swaggerLoader *SwaggerLoader) allowsExternalRefs(ref string) (err error) { - if !swaggerLoader.IsExternalRefsAllowed { +func (loader *Loader) allowsExternalRefs(ref string) (err error) { + if !loader.IsExternalRefsAllowed { err = fmt.Errorf("encountered disallowed external reference: %q", ref) } return } // loadSingleElementFromURI reads the data from ref and unmarshals to the passed element. -func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element interface{}) (*url.URL, error) { - if err := swaggerLoader.allowsExternalRefs(ref); err != nil { +func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, element interface{}) (*url.URL, error) { + if err := loader.allowsExternalRefs(ref); err != nil { return nil, err } @@ -103,7 +103,7 @@ func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPat return nil, fmt.Errorf("could not resolve path: %v", err) } - data, err := swaggerLoader.readURL(resolvedPath) + data, err := loader.readURL(resolvedPath) if err != nil { return nil, err } @@ -114,9 +114,9 @@ func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPat return resolvedPath, nil } -func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { - if f := swaggerLoader.ReadFromURIFunc; f != nil { - return f(swaggerLoader, location) +func (loader *Loader) readURL(location *url.URL) ([]byte, error) { + if f := loader.ReadFromURIFunc; f != nil { + return f(loader, location) } if location.Scheme != "" && location.Host != "" { @@ -136,103 +136,103 @@ func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { return ioutil.ReadFile(location.Path) } -// LoadSwaggerFromData loads a spec from a byte array -func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, error) { - swaggerLoader.resetVisitedPathItemRefs() - doc := &Swagger{} +// LoadFromData loads a spec from a byte array +func (loader *Loader) LoadFromData(data []byte) (*T, error) { + loader.resetVisitedPathItemRefs() + doc := &T{} if err := yaml.Unmarshal(data, doc); err != nil { return nil, err } - if err := swaggerLoader.ResolveRefsIn(doc, nil); err != nil { + if err := loader.ResolveRefsIn(doc, nil); err != nil { return nil, err } return doc, nil } -// LoadSwaggerFromDataWithPath takes the OpenApi spec data in bytes and a path where the resolver can find referred -// elements and returns a *Swagger with all resolved data or an error if unable to load data or resolve refs. -func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, location *url.URL) (*Swagger, error) { - swaggerLoader.resetVisitedPathItemRefs() - return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location) +// LoadFromDataWithPath takes the OpenAPI document data in bytes and a path where the resolver can find referred +// elements and returns a *T with all resolved data or an error if unable to load data or resolve refs. +func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T, error) { + loader.resetVisitedPathItemRefs() + return loader.loadFromDataWithPathInternal(data, location) } -func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []byte, location *url.URL) (*Swagger, error) { - if swaggerLoader.visitedDocuments == nil { - swaggerLoader.visitedDocuments = make(map[string]*Swagger) +func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.URL) (*T, error) { + if loader.visitedDocuments == nil { + loader.visitedDocuments = make(map[string]*T) } uri := location.String() - if doc, ok := swaggerLoader.visitedDocuments[uri]; ok { + if doc, ok := loader.visitedDocuments[uri]; ok { return doc, nil } - swagger := &Swagger{} - swaggerLoader.visitedDocuments[uri] = swagger + doc := &T{} + loader.visitedDocuments[uri] = doc - if err := yaml.Unmarshal(data, swagger); err != nil { + if err := yaml.Unmarshal(data, doc); err != nil { return nil, err } - if err := swaggerLoader.ResolveRefsIn(swagger, location); err != nil { + if err := loader.ResolveRefsIn(doc, location); err != nil { return nil, err } - return swagger, nil + return doc, nil } // ResolveRefsIn expands references if for instance spec was just unmarshalled -func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, location *url.URL) (err error) { - if swaggerLoader.visitedPathItemRefs == nil { - swaggerLoader.resetVisitedPathItemRefs() +func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { + if loader.visitedPathItemRefs == nil { + loader.resetVisitedPathItemRefs() } // Visit all components - components := swagger.Components + components := doc.Components for _, component := range components.Headers { - if err = swaggerLoader.resolveHeaderRef(swagger, component, location); err != nil { + if err = loader.resolveHeaderRef(doc, component, location); err != nil { return } } for _, component := range components.Parameters { - if err = swaggerLoader.resolveParameterRef(swagger, component, location); err != nil { + if err = loader.resolveParameterRef(doc, component, location); err != nil { return } } for _, component := range components.RequestBodies { - if err = swaggerLoader.resolveRequestBodyRef(swagger, component, location); err != nil { + if err = loader.resolveRequestBodyRef(doc, component, location); err != nil { return } } for _, component := range components.Responses { - if err = swaggerLoader.resolveResponseRef(swagger, component, location); err != nil { + if err = loader.resolveResponseRef(doc, component, location); err != nil { return } } for _, component := range components.Schemas { - if err = swaggerLoader.resolveSchemaRef(swagger, component, location); err != nil { + if err = loader.resolveSchemaRef(doc, component, location); err != nil { return } } for _, component := range components.SecuritySchemes { - if err = swaggerLoader.resolveSecuritySchemeRef(swagger, component, location); err != nil { + if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil { return } } for _, component := range components.Examples { - if err = swaggerLoader.resolveExampleRef(swagger, component, location); err != nil { + if err = loader.resolveExampleRef(doc, component, location); err != nil { return } } for _, component := range components.Callbacks { - if err = swaggerLoader.resolveCallbackRef(swagger, component, location); err != nil { + if err = loader.resolveCallbackRef(doc, component, location); err != nil { return } } // Visit all operations - for entrypoint, pathItem := range swagger.Paths { + for entrypoint, pathItem := range doc.Paths { if pathItem == nil { continue } - if err = swaggerLoader.resolvePathItemRef(swagger, entrypoint, pathItem, location); err != nil { + if err = loader.resolvePathItemRef(doc, entrypoint, pathItem, location); err != nil { return } } @@ -267,8 +267,8 @@ func isSingleRefElement(ref string) bool { return !strings.Contains(ref, "#") } -func (swaggerLoader *SwaggerLoader) resolveComponent( - swagger *Swagger, +func (loader *Loader) resolveComponent( + doc *T, ref string, path *url.URL, resolved interface{}, @@ -276,7 +276,7 @@ func (swaggerLoader *SwaggerLoader) resolveComponent( componentPath *url.URL, err error, ) { - if swagger, ref, componentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, path); err != nil { + if doc, ref, componentPath, err = loader.resolveRef(doc, ref, path); err != nil { return nil, err } @@ -293,7 +293,7 @@ func (swaggerLoader *SwaggerLoader) resolveComponent( for _, pathPart := range strings.Split(fragment[1:], "/") { pathPart = unescapeRefString(pathPart) - if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil { + if cursor, err = drillIntoField(cursor, pathPart); err != nil { e := failedToResolveRefFragmentPart(ref, pathPart) return nil, fmt.Errorf("%s: %s", e.Error(), err.Error()) } @@ -304,9 +304,9 @@ func (swaggerLoader *SwaggerLoader) resolveComponent( return cursor, nil } var cursor interface{} - if cursor, err = drill(swagger); err != nil { + if cursor, err = drill(doc); err != nil { var err2 error - data, err2 := swaggerLoader.readURL(path) + data, err2 := loader.readURL(path) if err2 != nil { return nil, err } @@ -345,7 +345,7 @@ func (swaggerLoader *SwaggerLoader) resolveComponent( } } -func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, error) { +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)) @@ -379,7 +379,7 @@ func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, e // if cursor is a "ref wrapper" struct (e.g. RequestBodyRef), if _, ok := val.Type().FieldByName("Value"); ok { // try digging into its Value field - return drillIntoSwaggerField(val.FieldByName("Value").Interface(), fieldName) + return drillIntoField(val.FieldByName("Value").Interface(), fieldName) } if hasFields { if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "ExtensionProps" { @@ -400,12 +400,12 @@ func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, e } } -func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref string, path *url.URL) (*Swagger, string, *url.URL, error) { +func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { if ref != "" && ref[0] == '#' { - return swagger, ref, path, nil + return doc, ref, path, nil } - if err := swaggerLoader.allowsExternalRefs(ref); err != nil { + if err := loader.allowsExternalRefs(ref); err != nil { return nil, "", nil, err } @@ -421,22 +421,22 @@ func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref stri return nil, "", nil, fmt.Errorf("error resolving path: %v", err) } - if swagger, err = swaggerLoader.loadSwaggerFromURIInternal(resolvedPath); err != nil { + if doc, err = loader.loadFromURIInternal(resolvedPath); err != nil { return nil, "", nil, fmt.Errorf("error resolving reference %q: %v", ref, err) } - return swagger, "#" + fragment, resolvedPath, nil + return doc, "#" + fragment, resolvedPath, nil } -func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { - if swaggerLoader.visitedHeader == nil { - swaggerLoader.visitedHeader = make(map[*Header]struct{}) + if loader.visitedHeader == nil { + loader.visitedHeader = make(map[*Header]struct{}) } - if _, ok := swaggerLoader.visitedHeader[component.Value]; ok { + if _, ok := loader.visitedHeader[component.Value]; ok { return nil } - swaggerLoader.visitedHeader[component.Value] = struct{}{} + loader.visitedHeader[component.Value] = struct{}{} } if component == nil { @@ -445,17 +445,17 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var header Header - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { return err } component.Value = &header } else { var resolved HeaderRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := swaggerLoader.resolveHeaderRef(swagger, &resolved, componentPath); err != nil { + if err := loader.resolveHeaderRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -467,22 +467,22 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component } if schema := value.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } } return nil } -func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { - if swaggerLoader.visitedParameter == nil { - swaggerLoader.visitedParameter = make(map[*Parameter]struct{}) + if loader.visitedParameter == nil { + loader.visitedParameter = make(map[*Parameter]struct{}) } - if _, ok := swaggerLoader.visitedParameter[component.Value]; ok { + if _, ok := loader.visitedParameter[component.Value]; ok { return nil } - swaggerLoader.visitedParameter[component.Value] = struct{}{} + loader.visitedParameter[component.Value] = struct{}{} } if component == nil { @@ -492,17 +492,17 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon if ref != "" { if isSingleRefElement(ref) { var param Parameter - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { return err } component.Value = ¶m } else { var resolved ParameterRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := swaggerLoader.resolveParameterRef(swagger, &resolved, componentPath); err != nil { + if err := loader.resolveParameterRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -518,28 +518,28 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon } for _, contentType := range value.Content { if schema := contentType.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } } } if schema := value.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } } return nil } -func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { - if swaggerLoader.visitedRequestBody == nil { - swaggerLoader.visitedRequestBody = make(map[*RequestBody]struct{}) + if loader.visitedRequestBody == nil { + loader.visitedRequestBody = make(map[*RequestBody]struct{}) } - if _, ok := swaggerLoader.visitedRequestBody[component.Value]; ok { + if _, ok := loader.visitedRequestBody[component.Value]; ok { return nil } - swaggerLoader.visitedRequestBody[component.Value] = struct{}{} + loader.visitedRequestBody[component.Value] = struct{}{} } if component == nil { @@ -548,17 +548,17 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var requestBody RequestBody - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { return err } component.Value = &requestBody } else { var resolved RequestBodyRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err = swaggerLoader.resolveRequestBodyRef(swagger, &resolved, componentPath); err != nil { + if err = loader.resolveRequestBodyRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -571,13 +571,13 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp for _, contentType := range value.Content { for name, example := range contentType.Examples { - if err := swaggerLoader.resolveExampleRef(swagger, example, documentPath); err != nil { + if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } } @@ -585,15 +585,15 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp return nil } -func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { - if swaggerLoader.visitedResponse == nil { - swaggerLoader.visitedResponse = make(map[*Response]struct{}) + if loader.visitedResponse == nil { + loader.visitedResponse = make(map[*Response]struct{}) } - if _, ok := swaggerLoader.visitedResponse[component.Value]; ok { + if _, ok := loader.visitedResponse[component.Value]; ok { return nil } - swaggerLoader.visitedResponse[component.Value] = struct{}{} + loader.visitedResponse[component.Value] = struct{}{} } if component == nil { @@ -603,17 +603,17 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone if ref != "" { if isSingleRefElement(ref) { var resp Response - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { return err } component.Value = &resp } else { var resolved ResponseRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := swaggerLoader.resolveResponseRef(swagger, &resolved, componentPath); err != nil { + if err := loader.resolveResponseRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -625,7 +625,7 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone } for _, header := range value.Headers { - if err := swaggerLoader.resolveHeaderRef(swagger, header, documentPath); err != nil { + if err := loader.resolveHeaderRef(doc, header, documentPath); err != nil { return err } } @@ -634,35 +634,35 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone continue } for name, example := range contentType.Examples { - if err := swaggerLoader.resolveExampleRef(swagger, example, documentPath); err != nil { + if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } contentType.Schema = schema } } for _, link := range value.Links { - if err := swaggerLoader.resolveLinkRef(swagger, link, documentPath); err != nil { + if err := loader.resolveLinkRef(doc, link, documentPath); err != nil { return err } } return nil } -func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { - if swaggerLoader.visitedSchema == nil { - swaggerLoader.visitedSchema = make(map[*Schema]struct{}) + if loader.visitedSchema == nil { + loader.visitedSchema = make(map[*Schema]struct{}) } - if _, ok := swaggerLoader.visitedSchema[component.Value]; ok { + if _, ok := loader.visitedSchema[component.Value]; ok { return nil } - swaggerLoader.visitedSchema[component.Value] = struct{}{} + loader.visitedSchema[component.Value] = struct{}{} } if component == nil { @@ -672,17 +672,17 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component if ref != "" { if isSingleRefElement(ref) { var schema Schema - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { return err } component.Value = &schema } else { var resolved SchemaRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := swaggerLoader.resolveSchemaRef(swagger, &resolved, componentPath); err != nil { + if err := loader.resolveSchemaRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -695,52 +695,52 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component // ResolveRefs referred schemas if v := value.Items; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } for _, v := range value.Properties { - if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } if v := value.AdditionalProperties; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } if v := value.Not; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } for _, v := range value.AllOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } for _, v := range value.AnyOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } for _, v := range value.OneOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } return nil } -func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { - if swaggerLoader.visitedSecurityScheme == nil { - swaggerLoader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) + if loader.visitedSecurityScheme == nil { + loader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) } - if _, ok := swaggerLoader.visitedSecurityScheme[component.Value]; ok { + if _, ok := loader.visitedSecurityScheme[component.Value]; ok { return nil } - swaggerLoader.visitedSecurityScheme[component.Value] = struct{}{} + loader.visitedSecurityScheme[component.Value] = struct{}{} } if component == nil { @@ -749,17 +749,17 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var scheme SecurityScheme - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { return err } component.Value = &scheme } else { var resolved SecuritySchemeRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := swaggerLoader.resolveSecuritySchemeRef(swagger, &resolved, componentPath); err != nil { + if err := loader.resolveSecuritySchemeRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -768,15 +768,15 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c return nil } -func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { - if swaggerLoader.visitedExample == nil { - swaggerLoader.visitedExample = make(map[*Example]struct{}) + if loader.visitedExample == nil { + loader.visitedExample = make(map[*Example]struct{}) } - if _, ok := swaggerLoader.visitedExample[component.Value]; ok { + if _, ok := loader.visitedExample[component.Value]; ok { return nil } - swaggerLoader.visitedExample[component.Value] = struct{}{} + loader.visitedExample[component.Value] = struct{}{} } if component == nil { @@ -785,17 +785,17 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var example Example - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { return err } component.Value = &example } else { var resolved ExampleRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := swaggerLoader.resolveExampleRef(swagger, &resolved, componentPath); err != nil { + if err := loader.resolveExampleRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -804,7 +804,7 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen return nil } -func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, component *CallbackRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) { if component == nil { return errors.New("invalid callback: value MUST be an object") @@ -812,17 +812,17 @@ func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, compone if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var resolved Callback - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil { return err } component.Value = &resolved } else { var resolved CallbackRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := swaggerLoader.resolveCallbackRef(swagger, &resolved, componentPath); err != nil { + if err := loader.resolveCallbackRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -841,10 +841,10 @@ func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, compone key = documentPath.EscapedPath() } key += entrypoint - if _, ok := swaggerLoader.visitedPathItemRefs[key]; ok { + if _, ok := loader.visitedPathItemRefs[key]; ok { return nil } - swaggerLoader.visitedPathItemRefs[key] = struct{}{} + loader.visitedPathItemRefs[key] = struct{}{} if pathItem == nil { return errors.New("invalid path item: value MUST be an object") @@ -853,12 +853,12 @@ func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, compone if ref != "" { if isSingleRefElement(ref) { var p PathItem - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { return err } *pathItem = p } else { - if swagger, ref, documentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, documentPath); err != nil { + if doc, ref, documentPath, err = loader.resolveRef(doc, ref, documentPath); err != nil { return } @@ -868,7 +868,7 @@ func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, compone } id := unescapeRefString(rest) - definitions := swagger.Components.Callbacks + definitions := doc.Components.Callbacks if definitions == nil { return failedToResolveRefFragmentPart(ref, "callbacks") } @@ -883,7 +883,7 @@ func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, compone } } } - return swaggerLoader.resolvePathItemRefContinued(swagger, pathItem, documentPath) + return loader.resolvePathItemRefContinued(doc, pathItem, documentPath) }() if err != nil { return err @@ -892,15 +892,15 @@ func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, compone return nil } -func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) (err error) { +func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { - if swaggerLoader.visitedLink == nil { - swaggerLoader.visitedLink = make(map[*Link]struct{}) + if loader.visitedLink == nil { + loader.visitedLink = make(map[*Link]struct{}) } - if _, ok := swaggerLoader.visitedLink[component.Value]; ok { + if _, ok := loader.visitedLink[component.Value]; ok { return nil } - swaggerLoader.visitedLink[component.Value] = struct{}{} + loader.visitedLink[component.Value] = struct{}{} } if component == nil { @@ -909,17 +909,17 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var link Link - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { return err } component.Value = &link } else { var resolved LinkRef - componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved) + componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := swaggerLoader.resolveLinkRef(swagger, &resolved, componentPath); err != nil { + if err := loader.resolveLinkRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -928,16 +928,16 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * return nil } -func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) { +func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) { key := "_" if documentPath != nil { key = documentPath.EscapedPath() } key += entrypoint - if _, ok := swaggerLoader.visitedPathItemRefs[key]; ok { + if _, ok := loader.visitedPathItemRefs[key]; ok { return nil } - swaggerLoader.visitedPathItemRefs[key] = struct{}{} + loader.visitedPathItemRefs[key] = struct{}{} if pathItem == nil { return errors.New("invalid path item: value MUST be an object") @@ -946,12 +946,12 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo if ref != "" { if isSingleRefElement(ref) { var p PathItem - if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { + if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { return err } *pathItem = p } else { - if swagger, ref, documentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, documentPath); err != nil { + if doc, ref, documentPath, err = loader.resolveRef(doc, ref, documentPath); err != nil { return } @@ -961,7 +961,7 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo } id := unescapeRefString(rest) - definitions := swagger.Paths + definitions := doc.Paths if definitions == nil { return failedToResolveRefFragmentPart(ref, "paths") } @@ -973,33 +973,33 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo *pathItem = *resolved } } - return swaggerLoader.resolvePathItemRefContinued(swagger, pathItem, documentPath) + return loader.resolvePathItemRefContinued(doc, pathItem, documentPath) } -func (swaggerLoader *SwaggerLoader) resolvePathItemRefContinued(swagger *Swagger, pathItem *PathItem, documentPath *url.URL) (err error) { +func (loader *Loader) resolvePathItemRefContinued(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) { for _, parameter := range pathItem.Parameters { - if err = swaggerLoader.resolveParameterRef(swagger, parameter, documentPath); err != nil { + if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil { return } } for _, operation := range pathItem.Operations() { for _, parameter := range operation.Parameters { - if err = swaggerLoader.resolveParameterRef(swagger, parameter, documentPath); err != nil { + if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil { return } } if requestBody := operation.RequestBody; requestBody != nil { - if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, documentPath); err != nil { + if err = loader.resolveRequestBodyRef(doc, requestBody, documentPath); err != nil { return } } for _, response := range operation.Responses { - if err = swaggerLoader.resolveResponseRef(swagger, response, documentPath); err != nil { + if err = loader.resolveResponseRef(doc, response, documentPath); err != nil { return } } for _, callback := range operation.Callbacks { - if err = swaggerLoader.resolveCallbackRef(swagger, callback, documentPath); err != nil { + if err = loader.resolveCallbackRef(doc, callback, documentPath); err != nil { return } } diff --git a/openapi3/swagger_loader_empty_response_description_test.go b/openapi3/loader_empty_response_description_test.go similarity index 83% rename from openapi3/swagger_loader_empty_response_description_test.go rename to openapi3/loader_empty_response_description_test.go index c75ad8aae..3c4b6bffd 100644 --- a/openapi3/swagger_loader_empty_response_description_test.go +++ b/openapi3/loader_empty_response_description_test.go @@ -33,8 +33,8 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { { spec := []byte(spec) - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(spec) + loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) got := doc.Paths["/path1"].Get.Responses["200"].Value.Description expected := "" @@ -46,8 +46,8 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { { spec := []byte(strings.Replace(spec, `"description": ""`, `"description": "My response"`, 1)) - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(spec) + loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) got := doc.Paths["/path1"].Get.Responses["200"].Value.Description expected := "My response" @@ -57,9 +57,9 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { require.NoError(t, err) } - noDescriptionIsInvalid := func(data []byte) *Swagger { - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(data) + noDescriptionIsInvalid := func(data []byte) *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) @@ -69,7 +69,7 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { return doc } - var docWithNoResponseDescription *Swagger + var docWithNoResponseDescription *T { spec := []byte(strings.Replace(spec, `"description": ""`, ``, 1)) docWithNoResponseDescription = noDescriptionIsInvalid(spec) diff --git a/openapi3/swagger_loader_http_error_test.go b/openapi3/loader_http_error_test.go similarity index 84% rename from openapi3/swagger_loader_http_error_test.go rename to openapi3/loader_http_error_test.go index ba361f406..5f7f137c8 100644 --- a/openapi3/swagger_loader_http_error_test.go +++ b/openapi3/loader_http_error_test.go @@ -42,15 +42,15 @@ func TestLoadReferenceFromRemoteURLFailsWithHttpError(t *testing.T) { } }`) - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) + doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) - require.Nil(t, swagger) + require.Nil(t, doc) require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL, ts.URL)) - swagger, err = loader.LoadSwaggerFromData(spec) - require.Nil(t, swagger) + doc, err = loader.LoadFromData(spec) + require.Nil(t, doc) require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL, ts.URL)) } @@ -86,14 +86,14 @@ func TestLoadFromRemoteURLFailsWithHttpError(t *testing.T) { } }`) - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) + doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) - require.Nil(t, swagger) + require.Nil(t, doc) require.EqualError(t, err, fmt.Sprintf("error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL)) - swagger, err = loader.LoadSwaggerFromData(spec) - require.Nil(t, swagger) + doc, err = loader.LoadFromData(spec) + require.Nil(t, doc) require.EqualError(t, err, fmt.Sprintf("error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL)) } diff --git a/openapi3/swagger_loader_issue212_test.go b/openapi3/loader_issue212_test.go similarity index 96% rename from openapi3/swagger_loader_issue212_test.go rename to openapi3/loader_issue212_test.go index 1999db4d3..507b37522 100644 --- a/openapi3/swagger_loader_issue212_test.go +++ b/openapi3/loader_issue212_test.go @@ -72,8 +72,8 @@ components: pattern: ^\/images\/[0-9a-f]{64}$ ` - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData([]byte(spec)) + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) diff --git a/openapi3/swagger_loader_issue220_test.go b/openapi3/loader_issue220_test.go similarity index 87% rename from openapi3/swagger_loader_issue220_test.go rename to openapi3/loader_issue220_test.go index 14c4d648f..57a44d5d0 100644 --- a/openapi3/swagger_loader_issue220_test.go +++ b/openapi3/loader_issue220_test.go @@ -14,9 +14,9 @@ func TestIssue220(t *testing.T) { } { t.Logf("specPath: %q", specPath) - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - doc, err := loader.LoadSwaggerFromFile(specPath) + doc, err := loader.LoadFromFile(specPath) require.NoError(t, err) err = doc.Validate(loader.Context) diff --git a/openapi3/swagger_loader_issue235_test.go b/openapi3/loader_issue235_test.go similarity index 69% rename from openapi3/swagger_loader_issue235_test.go rename to openapi3/loader_issue235_test.go index 79515c12d..4cb54eff1 100644 --- a/openapi3/swagger_loader_issue235_test.go +++ b/openapi3/loader_issue235_test.go @@ -7,9 +7,9 @@ import ( ) func TestIssue235OK(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - doc, err := loader.LoadSwaggerFromFile("testdata/issue235.spec0.yml") + doc, err := loader.LoadFromFile("testdata/issue235.spec0.yml") require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) @@ -17,9 +17,9 @@ func TestIssue235OK(t *testing.T) { func TestIssue235CircularDep(t *testing.T) { t.Skip("TODO: return an error on circular dependencies between external files of a spec") - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - doc, err := loader.LoadSwaggerFromFile("testdata/issue235.spec0-typo.yml") + doc, err := loader.LoadFromFile("testdata/issue235.spec0-typo.yml") require.Nil(t, doc) require.Error(t, err) } diff --git a/openapi3/swagger_loader_outside_refs_test.go b/openapi3/loader_outside_refs_test.go similarity index 81% rename from openapi3/swagger_loader_outside_refs_test.go rename to openapi3/loader_outside_refs_test.go index 1a5cb1c62..5cec93452 100644 --- a/openapi3/swagger_loader_outside_refs_test.go +++ b/openapi3/loader_outside_refs_test.go @@ -7,9 +7,9 @@ import ( ) func TestLoadOutsideRefs(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - doc, err := loader.LoadSwaggerFromFile("testdata/303bis/service.yaml") + doc, err := loader.LoadFromFile("testdata/303bis/service.yaml") require.NoError(t, err) require.NotNil(t, doc) diff --git a/openapi3/swagger_loader_paths_test.go b/openapi3/loader_paths_test.go similarity index 84% rename from openapi3/swagger_loader_paths_test.go rename to openapi3/loader_paths_test.go index babd52c25..584f00e85 100644 --- a/openapi3/swagger_loader_paths_test.go +++ b/openapi3/loader_paths_test.go @@ -26,8 +26,8 @@ paths: "foo/bar": "invalid paths: path \"foo/bar\" does not start with a forward slash (/)", "/foo/bar": "", } { - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData([]byte(strings.Replace(spec, "PATH", path, 1))) + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(strings.Replace(spec, "PATH", path, 1))) require.NoError(t, err) err = doc.Validate(loader.Context) if expectedErr != "" { diff --git a/openapi3/swagger_loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go similarity index 75% rename from openapi3/swagger_loader_read_from_uri_func_test.go rename to openapi3/loader_read_from_uri_func_test.go index d63a3be3c..72d4a95a7 100644 --- a/openapi3/swagger_loader_read_from_uri_func_test.go +++ b/openapi3/loader_read_from_uri_func_test.go @@ -11,24 +11,24 @@ import ( ) func TestLoaderReadFromURIFunc(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - loader.ReadFromURIFunc = func(loader *SwaggerLoader, url *url.URL) ([]byte, error) { + loader.ReadFromURIFunc = func(loader *Loader, url *url.URL) ([]byte, error) { return ioutil.ReadFile(filepath.Join("testdata", url.Path)) } - doc, err := loader.LoadSwaggerFromFile("recursiveRef/openapi.yml") + doc, err := loader.LoadFromFile("recursiveRef/openapi.yml") 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["foo"].Value.Properties["bar"].Value.Items.Value.Example) } -type multipleSourceSwaggerLoaderExample struct { +type multipleSourceLoaderExample struct { Sources map[string][]byte } -func (l *multipleSourceSwaggerLoaderExample) LoadSwaggerFromURI( - loader *SwaggerLoader, +func (l *multipleSourceLoaderExample) LoadFromURI( + loader *Loader, location *url.URL, ) ([]byte, error) { source := l.resolveSourceFromURI(location) @@ -38,7 +38,7 @@ func (l *multipleSourceSwaggerLoaderExample) LoadSwaggerFromURI( return source, nil } -func (l *multipleSourceSwaggerLoaderExample) resolveSourceFromURI(location fmt.Stringer) []byte { +func (l *multipleSourceLoaderExample) resolveSourceFromURI(location fmt.Stringer) []byte { return l.Sources[location.String()] } @@ -50,18 +50,18 @@ func TestResolveSchemaExternalRef(t *testing.T) { externalLocation.String(), )) externalSpec := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"External Spec"},"paths":{},"components":{"schemas":{"External":{"type":"string"}}}}`) - multipleSourceLoader := &multipleSourceSwaggerLoaderExample{ + multipleSourceLoader := &multipleSourceLoaderExample{ Sources: map[string][]byte{ rootLocation.String(): rootSpec, externalLocation.String(): externalSpec, }, } - loader := &SwaggerLoader{ + loader := &Loader{ IsExternalRefsAllowed: true, - ReadFromURIFunc: multipleSourceLoader.LoadSwaggerFromURI, + ReadFromURIFunc: multipleSourceLoader.LoadFromURI, } - doc, err := loader.LoadSwaggerFromURI(rootLocation) + doc, err := loader.LoadFromURI(rootLocation) require.NoError(t, err) err = doc.Validate(loader.Context) diff --git a/openapi3/swagger_loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go similarity index 81% rename from openapi3/swagger_loader_recursive_ref_test.go rename to openapi3/loader_recursive_ref_test.go index 45842d8f1..bc1f24b88 100644 --- a/openapi3/swagger_loader_recursive_ref_test.go +++ b/openapi3/loader_recursive_ref_test.go @@ -7,9 +7,9 @@ import ( ) func TestLoaderSupportsRecursiveReference(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - doc, err := loader.LoadSwaggerFromFile("testdata/recursiveRef/openapi.yml") + doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml") require.NoError(t, err) require.NotNil(t, doc) require.NoError(t, doc.Validate(loader.Context)) diff --git a/openapi3/swagger_loader_relative_refs_test.go b/openapi3/loader_relative_refs_test.go similarity index 71% rename from openapi3/swagger_loader_relative_refs_test.go rename to openapi3/loader_relative_refs_test.go index cb5ab3f05..50d2c7d24 100644 --- a/openapi3/swagger_loader_relative_refs_test.go +++ b/openapi3/loader_relative_refs_test.go @@ -11,120 +11,120 @@ import ( type refTestDataEntry struct { name string contentTemplate string - testFunc func(t *testing.T, swagger *Swagger) + testFunc func(t *testing.T, doc *T) } type refTestDataEntryWithErrorMessage struct { name string contentTemplate string errorMessage *string - testFunc func(t *testing.T, swagger *Swagger) + testFunc func(t *testing.T, doc *T) } var refTestDataEntries = []refTestDataEntry{ { name: "SchemaRef", contentTemplate: externalSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.Schemas["TestSchema"].Value.Type) - require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.Schemas["TestSchema"].Value.Type) + require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) }, }, { name: "ResponseRef", contentTemplate: externalResponseRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { + testFunc: func(t *testing.T, doc *T) { desc := "description" - require.Equal(t, &desc, swagger.Components.Responses["TestResponse"].Value.Description) + require.Equal(t, &desc, doc.Components.Responses["TestResponse"].Value.Description) }, }, { name: "ParameterRef", contentTemplate: externalParameterRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.Parameters["TestParameter"].Value.Name) - require.Equal(t, "id", swagger.Components.Parameters["TestParameter"].Value.Name) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.Parameters["TestParameter"].Value.Name) + require.Equal(t, "id", doc.Components.Parameters["TestParameter"].Value.Name) }, }, { name: "ExampleRef", contentTemplate: externalExampleRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.Examples["TestExample"].Value.Description) - require.Equal(t, "description", swagger.Components.Examples["TestExample"].Value.Description) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.Examples["TestExample"].Value.Description) + require.Equal(t, "description", doc.Components.Examples["TestExample"].Value.Description) }, }, { name: "RequestBodyRef", contentTemplate: externalRequestBodyRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.RequestBodies["TestRequestBody"].Value.Content) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.RequestBodies["TestRequestBody"].Value.Content) }, }, { name: "SecuritySchemeRef", contentTemplate: externalSecuritySchemeRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Description) - require.Equal(t, "description", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Description) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Description) + require.Equal(t, "description", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Description) }, }, { name: "ExternalHeaderRef", contentTemplate: externalHeaderRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.Headers["TestHeader"].Value.Description) - require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.Headers["TestHeader"].Value.Description) + require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description) }, }, { name: "PathParameterRef", contentTemplate: externalPathParameterRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test/{id}"].Parameters[0].Value.Name) - require.Equal(t, "id", swagger.Paths["/test/{id}"].Parameters[0].Value.Name) + 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) }, }, { name: "PathOperationParameterRef", contentTemplate: externalPathOperationParameterRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test/{id}"].Get.Parameters[0].Value) - require.Equal(t, "id", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Name) + 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) }, }, { name: "PathOperationRequestBodyRef", contentTemplate: externalPathOperationRequestBodyRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value) - require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content) + 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) }, }, { name: "PathOperationResponseRef", contentTemplate: externalPathOperationResponseRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) desc := "description" - require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description) + require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) }, }, { name: "PathOperationParameterSchemaRef", contentTemplate: externalPathOperationParameterSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value) - require.Equal(t, "string", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value.Type) - require.Equal(t, "id", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Name) + 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) }, }, { name: "PathOperationParameterRefWithContentInQuery", contentTemplate: externalPathOperationParameterWithContentInQueryTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - schemaRef := swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Content["application/json"].Schema + testFunc: func(t *testing.T, doc *T) { + schemaRef := doc.Paths["/test/{id}"].Get.Parameters[0].Value.Content["application/json"].Schema require.NotNil(t, schemaRef.Value) require.Equal(t, "string", schemaRef.Value.Type) }, @@ -133,53 +133,53 @@ var refTestDataEntries = []refTestDataEntry{ { name: "PathOperationRequestBodyExampleRef", contentTemplate: externalPathOperationRequestBodyExampleRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value) - require.Equal(t, "description", swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description) + 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) }, }, { name: "PathOperationReqestBodyContentSchemaRef", contentTemplate: externalPathOperationReqestBodyContentSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value) - require.Equal(t, "string", swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) + 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) }, }, { name: "PathOperationResponseExampleRef", contentTemplate: externalPathOperationResponseExampleRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) desc := "testdescription" - require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description) - require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Examples["application/json"].Value.Description) + 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) }, }, { name: "PathOperationResponseSchemaRef", contentTemplate: externalPathOperationResponseSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) desc := "testdescription" - require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description) - require.Equal(t, "string", swagger.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Schema.Value.Type) + 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) }, }, { name: "ComponentHeaderSchemaRef", contentTemplate: externalComponentHeaderSchemaRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.Headers["TestHeader"].Value) - require.Equal(t, "string", swagger.Components.Headers["TestHeader"].Value.Schema.Value.Type) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.Headers["TestHeader"].Value) + require.Equal(t, "string", doc.Components.Headers["TestHeader"].Value.Schema.Value.Type) }, }, { name: "RequestResponseHeaderRef", contentTemplate: externalRequestResponseHeaderRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) - require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + 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) }, }, } @@ -189,7 +189,7 @@ var refTestDataEntriesResponseError = []refTestDataEntryWithErrorMessage{ name: "CannotContainBothSchemaAndContentInAParameter", contentTemplate: externalCannotContainBothSchemaAndContentInAParameter, errorMessage: &(&struct{ x string }{"cannot contain both schema and content in a parameter"}).x, - testFunc: func(t *testing.T, swagger *Swagger) { + testFunc: func(t *testing.T, doc *T) { }, }, } @@ -199,11 +199,11 @@ func TestLoadFromDataWithExternalRef(t *testing.T) { t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json")) - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) + doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) - td.testFunc(t, swagger) + td.testFunc(t, doc) } } @@ -212,11 +212,11 @@ func TestLoadFromDataWithExternalRefResponseError(t *testing.T) { t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json")) - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) + doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.EqualError(t, err, *td.errorMessage) - td.testFunc(t, swagger) + td.testFunc(t, doc) } } @@ -225,11 +225,11 @@ func TestLoadFromDataWithExternalNestedRef(t *testing.T) { t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "nesteddir/nestedcomponents.openapi.json")) - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) + doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) - td.testFunc(t, swagger) + td.testFunc(t, doc) } } @@ -723,78 +723,78 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "SchemaRef", contentTemplate: relativeSchemaDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.Schemas["TestSchema"].Value.Type) - require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.Schemas["TestSchema"].Value.Type) + require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) }, }, { name: "ResponseRef", contentTemplate: relativeResponseDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { + testFunc: func(t *testing.T, doc *T) { desc := "description" - require.Equal(t, &desc, swagger.Components.Responses["TestResponse"].Value.Description) + require.Equal(t, &desc, doc.Components.Responses["TestResponse"].Value.Description) }, }, { name: "ParameterRef", contentTemplate: relativeParameterDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.Parameters["TestParameter"].Value.Name) - require.Equal(t, "param", swagger.Components.Parameters["TestParameter"].Value.Name) - require.Equal(t, true, swagger.Components.Parameters["TestParameter"].Value.Required) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.Parameters["TestParameter"].Value.Name) + require.Equal(t, "param", doc.Components.Parameters["TestParameter"].Value.Name) + require.Equal(t, true, doc.Components.Parameters["TestParameter"].Value.Required) }, }, { name: "ExampleRef", contentTemplate: relativeExampleDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, "param", swagger.Components.Examples["TestExample"].Value.Summary) - require.NotNil(t, "param", swagger.Components.Examples["TestExample"].Value.Value) - require.Equal(t, "An example", swagger.Components.Examples["TestExample"].Value.Summary) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, "param", doc.Components.Examples["TestExample"].Value.Summary) + require.NotNil(t, "param", doc.Components.Examples["TestExample"].Value.Value) + require.Equal(t, "An example", doc.Components.Examples["TestExample"].Value.Summary) }, }, { name: "RequestRef", contentTemplate: relativeRequestDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, "param", swagger.Components.RequestBodies["TestRequestBody"].Value.Description) - require.Equal(t, "example request", swagger.Components.RequestBodies["TestRequestBody"].Value.Description) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, "param", doc.Components.RequestBodies["TestRequestBody"].Value.Description) + require.Equal(t, "example request", doc.Components.RequestBodies["TestRequestBody"].Value.Description) }, }, { name: "HeaderRef", contentTemplate: relativeHeaderDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, "param", swagger.Components.Headers["TestHeader"].Value.Description) - require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, "param", doc.Components.Headers["TestHeader"].Value.Description) + require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description) }, }, { name: "HeaderRef", contentTemplate: relativeHeaderDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, "param", swagger.Components.Headers["TestHeader"].Value.Description) - require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, "param", doc.Components.Headers["TestHeader"].Value.Description) + require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description) }, }, { name: "SecuritySchemeRef", contentTemplate: relativeSecuritySchemeDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Type) - require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme) - require.Equal(t, "http", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Type) - require.Equal(t, "basic", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme) + testFunc: func(t *testing.T, doc *T) { + require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Type) + require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme) + require.Equal(t, "http", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Type) + require.Equal(t, "basic", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme) }, }, { name: "PathRef", contentTemplate: relativePathDocsRefTemplate, - testFunc: func(t *testing.T, swagger *Swagger) { - require.NotNil(t, swagger.Paths["/pets"]) - require.NotNil(t, swagger.Paths["/pets"].Get.Responses["200"]) - require.NotNil(t, swagger.Paths["/pets"].Get.Responses["200"].Value.Content["application/json"]) + 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"]) }, }, } @@ -804,11 +804,11 @@ func TestLoadSpecWithRelativeDocumentRefs(t *testing.T) { t.Logf("testcase %q", td.name) spec := []byte(td.contentTemplate) - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/"}) + doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/"}) require.NoError(t, err) - td.testFunc(t, swagger) + td.testFunc(t, doc) } } @@ -906,15 +906,15 @@ paths: ` func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromFile("testdata/relativeDocsUseDocumentPath/openapi/openapi.yml") + doc, err := loader.LoadFromFile("testdata/relativeDocsUseDocumentPath/openapi/openapi.yml") require.NoError(t, err) // path in nested directory // check parameter - nestedDirPath := swagger.Paths["/pets/{id}"] + nestedDirPath := doc.Paths["/pets/{id}"] require.Equal(t, "param", nestedDirPath.Patch.Parameters[0].Value.Name) require.Equal(t, "path", nestedDirPath.Patch.Parameters[0].Value.In) require.Equal(t, true, nestedDirPath.Patch.Parameters[0].Value.Required) @@ -934,7 +934,7 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // path in more nested directory // check parameter - moreNestedDirPath := swagger.Paths["/pets/{id}/{city}"] + moreNestedDirPath := doc.Paths["/pets/{id}/{city}"] require.Equal(t, "param", moreNestedDirPath.Patch.Parameters[0].Value.Name) require.Equal(t, "path", moreNestedDirPath.Patch.Parameters[0].Value.In) require.Equal(t, true, moreNestedDirPath.Patch.Parameters[0].Value.Required) diff --git a/openapi3/swagger_loader_test.go b/openapi3/loader_test.go similarity index 77% rename from openapi3/swagger_loader_test.go rename to openapi3/loader_test.go index 0662f16c7..4bc4ce432 100644 --- a/openapi3/swagger_loader_test.go +++ b/openapi3/loader_test.go @@ -55,8 +55,8 @@ paths: $ref: '#/components/schemas/ErrorModel' `) - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(spec) + loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) require.Equal(t, "An API", doc.Info.Title) require.Equal(t, 2, len(doc.Components.Schemas)) @@ -68,20 +68,20 @@ paths: require.NoError(t, err) } -func ExampleSwaggerLoader() { +func ExampleLoader() { const source = `{"info":{"description":"An API"}}` - swagger, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(source)) + doc, err := NewLoader().LoadFromData([]byte(source)) if err != nil { panic(err) } - fmt.Print(swagger.Info.Description) + fmt.Print(doc.Info.Description) // Output: An API } func TestResolveSchemaRef(t *testing.T) { source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1",description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`) - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(source) + loader := NewLoader() + doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) @@ -93,8 +93,8 @@ func TestResolveSchemaRef(t *testing.T) { func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) { source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{"/foo":{"post":{"requestBody":{"content":{"application/json":{"schema":null}}}}}}}`) - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(source) + loader := NewLoader() + doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) require.EqualError(t, err, `invalid paths: found unresolved ref: ""`) @@ -122,8 +122,8 @@ paths: examples: test: $ref: '#/components/examples/test'`) - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(source) + loader := NewLoader() + doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) @@ -160,8 +160,8 @@ paths: $ref: '#/components/schemas/Thing' `) - loader := NewSwaggerLoader() - _, err := loader.LoadSwaggerFromData(spec) + loader := NewLoader() + _, err := loader.LoadFromData(spec) require.Error(t, err) } @@ -188,11 +188,11 @@ paths: description: Test call. `) - loader := NewSwaggerLoader() - swagger, err := loader.LoadSwaggerFromData(spec) + loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) - require.NotNil(t, swagger.Paths["/"].Parameters[0].Value) + require.NotNil(t, doc.Paths["/"].Parameters[0].Value) } func TestLoadRequestExampleRef(t *testing.T) { @@ -220,11 +220,11 @@ paths: description: Test call. `) - loader := NewSwaggerLoader() - swagger, err := loader.LoadSwaggerFromData(spec) + loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) - require.NotNil(t, swagger.Paths["/"].Post.RequestBody.Value.Content.Get("application/json").Examples["test"]) + require.NotNil(t, doc.Paths["/"].Post.RequestBody.Value.Content.Get("application/json").Examples["test"]) } func createTestServer(handler http.Handler) *httptest.Server { @@ -242,21 +242,21 @@ func TestLoadFromRemoteURL(t *testing.T) { ts.Start() defer ts.Close() - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true url, err := url.Parse("http://" + addr + "/test.openapi.json") require.NoError(t, err) - swagger, err := loader.LoadSwaggerFromURI(url) + doc, err := loader.LoadFromURI(url) require.NoError(t, err) - require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type) + require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) } func TestLoadWithReferenceInReference(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - doc, err := loader.LoadSwaggerFromFile("testdata/refInRef/openapi.json") + doc, err := loader.LoadFromFile("testdata/refInRef/openapi.json") require.NoError(t, err) require.NotNil(t, doc) err = doc.Validate(loader.Context) @@ -265,22 +265,22 @@ func TestLoadWithReferenceInReference(t *testing.T) { } func TestLoadFileWithExternalSchemaRef(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.json") + doc, err := loader.LoadFromFile("testdata/testref.openapi.json") require.NoError(t, err) - require.NotNil(t, swagger.Components.Schemas["AnotherTestSchema"].Value.Type) + require.NotNil(t, doc.Components.Schemas["AnotherTestSchema"].Value.Type) } func TestLoadFileWithExternalSchemaRefSingleComponent(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromFile("testdata/testrefsinglecomponent.openapi.json") + doc, err := loader.LoadFromFile("testdata/testrefsinglecomponent.openapi.json") require.NoError(t, err) - require.NotNil(t, swagger.Components.Responses["SomeResponse"]) + require.NotNil(t, doc.Components.Responses["SomeResponse"]) desc := "this is a single response definition" - require.Equal(t, &desc, swagger.Components.Responses["SomeResponse"].Value.Description) + require.Equal(t, &desc, doc.Components.Responses["SomeResponse"].Value.Description) } func TestLoadRequestResponseHeaderRef(t *testing.T) { @@ -316,12 +316,12 @@ func TestLoadRequestResponseHeaderRef(t *testing.T) { } }`) - loader := NewSwaggerLoader() - swagger, err := loader.LoadSwaggerFromData(spec) + loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) - require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) - require.Equal(t, "testheader", swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + 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) } func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) { @@ -355,41 +355,41 @@ func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) { ts.Start() defer ts.Close() - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) + doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) - require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) - require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) + 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) } func TestLoadYamlFile(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromFile("testdata/test.openapi.yml") + doc, err := loader.LoadFromFile("testdata/test.openapi.yml") require.NoError(t, err) - require.Equal(t, "OAI Specification in YAML", swagger.Info.Title) + require.Equal(t, "OAI Specification in YAML", doc.Info.Title) } func TestLoadYamlFileWithExternalSchemaRef(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.yml") + doc, err := loader.LoadFromFile("testdata/testref.openapi.yml") require.NoError(t, err) - require.NotNil(t, swagger.Components.Schemas["AnotherTestSchema"].Value.Type) + require.NotNil(t, doc.Components.Schemas["AnotherTestSchema"].Value.Type) } func TestLoadYamlFileWithExternalPathRef(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() loader.IsExternalRefsAllowed = true - swagger, err := loader.LoadSwaggerFromFile("testdata/pathref.openapi.yml") + doc, err := loader.LoadFromFile("testdata/pathref.openapi.yml") require.NoError(t, err) - require.NotNil(t, swagger.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) - require.Equal(t, "string", swagger.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) + 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) } func TestResolveResponseLinkRef(t *testing.T) { @@ -423,8 +423,8 @@ paths: father: $ref: '#/components/links/Father' `) - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(source) + loader := NewLoader() + doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) @@ -492,8 +492,8 @@ paths: $ref: '#/components/schemas/ErrorModel' `) - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData(spec) + loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) @@ -521,8 +521,8 @@ servers: `{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), } { t.Run(value, func(t *testing.T) { - loader := NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData([]byte(strings.Replace(spec, "@@@", value, 1))) + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(strings.Replace(spec, "@@@", value, 1))) require.NoError(t, err) err = doc.Validate(loader.Context) require.Equal(t, expected, err) diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 6d2f2cb7a..2dd0842f6 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -67,12 +67,12 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, mediaType) } -func (mediaType *MediaType) Validate(c context.Context) error { - if mediaType == nil { +func (value *MediaType) Validate(ctx context.Context) error { + if value == nil { return nil } - if schema := mediaType.Schema; schema != nil { - if err := schema.Validate(c); err != nil { + if schema := value.Schema; schema != nil { + if err := schema.Validate(ctx); err != nil { return err } } diff --git a/openapi3/swagger.go b/openapi3/openapi3.go similarity index 66% rename from openapi3/swagger.go rename to openapi3/openapi3.go index 64f76c232..ee6887727 100644 --- a/openapi3/swagger.go +++ b/openapi3/openapi3.go @@ -8,7 +8,8 @@ import ( "github.com/getkin/kin-openapi/jsoninfo" ) -type Swagger struct { +// T is the root of an OpenAPI v3 document +type T struct { ExtensionProps OpenAPI string `json:"openapi" yaml:"openapi"` // Required Components Components `json:"components,omitempty" yaml:"components,omitempty"` @@ -20,19 +21,19 @@ type Swagger struct { ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } -func (swagger *Swagger) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(swagger) +func (doc *T) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(doc) } -func (swagger *Swagger) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, swagger) +func (doc *T) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, doc) } -func (swagger *Swagger) AddOperation(path string, method string, operation *Operation) { - paths := swagger.Paths +func (doc *T) AddOperation(path string, method string, operation *Operation) { + paths := doc.Paths if paths == nil { paths = make(Paths) - swagger.Paths = paths + doc.Paths = paths } pathItem := paths[path] if pathItem == nil { @@ -42,12 +43,12 @@ func (swagger *Swagger) AddOperation(path string, method string, operation *Oper pathItem.SetOperation(method, operation) } -func (swagger *Swagger) AddServer(server *Server) { - swagger.Servers = append(swagger.Servers, server) +func (doc *T) AddServer(server *Server) { + doc.Servers = append(doc.Servers, server) } -func (swagger *Swagger) Validate(c context.Context) error { - if swagger.OpenAPI == "" { +func (value *T) Validate(ctx context.Context) error { + if value.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") } @@ -55,15 +56,15 @@ func (swagger *Swagger) Validate(c context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid components: %v", e) } - if err := swagger.Components.Validate(c); err != nil { + if err := value.Components.Validate(ctx); err != nil { return wrap(err) } } { wrap := func(e error) error { return fmt.Errorf("invalid info: %v", e) } - if v := swagger.Info; v != nil { - if err := v.Validate(c); err != nil { + if v := value.Info; v != nil { + if err := v.Validate(ctx); err != nil { return wrap(err) } } else { @@ -73,8 +74,8 @@ func (swagger *Swagger) Validate(c context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid paths: %v", e) } - if v := swagger.Paths; v != nil { - if err := v.Validate(c); err != nil { + if v := value.Paths; v != nil { + if err := v.Validate(ctx); err != nil { return wrap(err) } } else { @@ -84,8 +85,8 @@ func (swagger *Swagger) Validate(c context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid security: %v", e) } - if v := swagger.Security; v != nil { - if err := v.Validate(c); err != nil { + if v := value.Security; v != nil { + if err := v.Validate(ctx); err != nil { return wrap(err) } } @@ -93,8 +94,8 @@ func (swagger *Swagger) Validate(c context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid servers: %v", e) } - if v := swagger.Servers; v != nil { - if err := v.Validate(c); err != nil { + if v := value.Servers; v != nil { + if err := v.Validate(ctx); err != nil { return wrap(err) } } diff --git a/openapi3/swagger_test.go b/openapi3/openapi3_test.go similarity index 93% rename from openapi3/swagger_test.go rename to openapi3/openapi3_test.go index ded2500b2..4c6bfe3ca 100644 --- a/openapi3/swagger_test.go +++ b/openapi3/openapi3_test.go @@ -11,28 +11,28 @@ import ( ) func TestRefsJSON(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() - t.Log("Marshal *Swagger to JSON") + t.Log("Marshal *T to JSON") data, err := json.Marshal(spec()) require.NoError(t, err) require.NotEmpty(t, data) - t.Log("Unmarshal *Swagger from JSON") - docA := &Swagger{} + t.Log("Unmarshal *T from JSON") + docA := &T{} err = json.Unmarshal(specJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) - t.Log("Resolve refs in unmarshalled *Swagger") + t.Log("Resolve refs in unmarshalled *T") err = loader.ResolveRefsIn(docA, nil) require.NoError(t, err) - t.Log("Resolve refs in marshalled *Swagger") - docB, err := loader.LoadSwaggerFromData(data) + t.Log("Resolve refs in marshalled *T") + docB, err := loader.LoadFromData(data) require.NoError(t, err) require.NotEmpty(t, docB) - t.Log("Validate *Swagger") + t.Log("Validate *T") err = docA.Validate(loader.Context) require.NoError(t, err) err = docB.Validate(loader.Context) @@ -49,28 +49,28 @@ func TestRefsJSON(t *testing.T) { } func TestRefsYAML(t *testing.T) { - loader := NewSwaggerLoader() + loader := NewLoader() - t.Log("Marshal *Swagger to YAML") + t.Log("Marshal *T to YAML") data, err := yaml.Marshal(spec()) require.NoError(t, err) require.NotEmpty(t, data) - t.Log("Unmarshal *Swagger from YAML") - docA := &Swagger{} + t.Log("Unmarshal *T from YAML") + docA := &T{} err = yaml.Unmarshal(specYAML, &docA) require.NoError(t, err) require.NotEmpty(t, data) - t.Log("Resolve refs in unmarshalled *Swagger") + t.Log("Resolve refs in unmarshalled *T") err = loader.ResolveRefsIn(docA, nil) require.NoError(t, err) - t.Log("Resolve refs in marshalled *Swagger") - docB, err := loader.LoadSwaggerFromData(data) + t.Log("Resolve refs in marshalled *T") + docB, err := loader.LoadFromData(data) require.NoError(t, err) require.NotEmpty(t, docB) - t.Log("Validate *Swagger") + t.Log("Validate *T") err = docA.Validate(loader.Context) require.NoError(t, err) err = docB.Validate(loader.Context) @@ -237,7 +237,7 @@ var specJSON = []byte(` } `) -func spec() *Swagger { +func spec() *T { parameter := &Parameter{ Description: "Some parameter", Name: "example", @@ -257,7 +257,7 @@ func spec() *Swagger { Description: "Some schema", } example := map[string]string{"name": "Some example"} - return &Swagger{ + return &T{ OpenAPI: "3.0", Info: &Info{ Title: "MyAPI", @@ -401,7 +401,7 @@ components: for spec, expectedErr := range tests { t.Run(expectedErr, func(t *testing.T) { - doc := &Swagger{} + doc := &T{} err := yaml.Unmarshal([]byte(spec), &doc) require.NoError(t, err) diff --git a/openapi3/operation.go b/openapi3/operation.go index f7ff93fe5..0de7c421a 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -120,19 +120,19 @@ func (operation *Operation) AddResponse(status int, response *Response) { } } -func (operation *Operation) Validate(c context.Context) error { - if v := operation.Parameters; v != nil { - if err := v.Validate(c); err != nil { +func (value *Operation) Validate(ctx context.Context) error { + if v := value.Parameters; v != nil { + if err := v.Validate(ctx); err != nil { return err } } - if v := operation.RequestBody; v != nil { - if err := v.Validate(c); err != nil { + if v := value.RequestBody; v != nil { + if err := v.Validate(ctx); err != nil { return err } } - if v := operation.Responses; v != nil { - if err := v.Validate(c); err != nil { + if v := value.Responses; v != nil { + if err := v.Validate(ctx); err != nil { return err } } else { diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 76e5f7f1d..f4b91adf0 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -64,9 +64,9 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter { return nil } -func (parameters Parameters) Validate(c context.Context) error { +func (value Parameters) Validate(ctx context.Context) error { dupes := make(map[string]struct{}) - for _, item := range parameters { + for _, item := range value { if v := item.Value; v != nil { key := v.In + ":" + v.Name if _, ok := dupes[key]; ok { @@ -75,7 +75,7 @@ func (parameters Parameters) Validate(c context.Context) error { dupes[key] = struct{}{} } - if err := item.Validate(c); err != nil { + if err := item.Validate(ctx); err != nil { return err } } @@ -236,11 +236,11 @@ func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error) } } -func (parameter *Parameter) Validate(c context.Context) error { - if parameter.Name == "" { +func (value *Parameter) Validate(ctx context.Context) error { + if value.Name == "" { return errors.New("parameter name can't be blank") } - in := parameter.In + in := value.In switch in { case ParameterInPath, @@ -248,55 +248,55 @@ func (parameter *Parameter) Validate(c context.Context) error { ParameterInHeader, ParameterInCookie: default: - return fmt.Errorf("parameter can't have 'in' value %q", parameter.In) + return fmt.Errorf("parameter can't have 'in' value %q", value.In) } // Validate a parameter's serialization method. - sm, err := parameter.SerializationMethod() + sm, err := value.SerializationMethod() if err != nil { return err } var smSupported bool switch { - case parameter.In == ParameterInPath && sm.Style == SerializationSimple && !sm.Explode, - parameter.In == ParameterInPath && sm.Style == SerializationSimple && sm.Explode, - parameter.In == ParameterInPath && sm.Style == SerializationLabel && !sm.Explode, - parameter.In == ParameterInPath && sm.Style == SerializationLabel && sm.Explode, - parameter.In == ParameterInPath && sm.Style == SerializationMatrix && !sm.Explode, - parameter.In == ParameterInPath && sm.Style == SerializationMatrix && sm.Explode, - - parameter.In == ParameterInQuery && sm.Style == SerializationForm && sm.Explode, - parameter.In == ParameterInQuery && sm.Style == SerializationForm && !sm.Explode, - parameter.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && sm.Explode, - parameter.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && !sm.Explode, - parameter.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && sm.Explode, - parameter.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && !sm.Explode, - parameter.In == ParameterInQuery && sm.Style == SerializationDeepObject && sm.Explode, - - parameter.In == ParameterInHeader && sm.Style == SerializationSimple && !sm.Explode, - parameter.In == ParameterInHeader && sm.Style == SerializationSimple && sm.Explode, - - parameter.In == ParameterInCookie && sm.Style == SerializationForm && !sm.Explode, - parameter.In == ParameterInCookie && sm.Style == SerializationForm && sm.Explode: + case value.In == ParameterInPath && sm.Style == SerializationSimple && !sm.Explode, + value.In == ParameterInPath && sm.Style == SerializationSimple && sm.Explode, + value.In == ParameterInPath && sm.Style == SerializationLabel && !sm.Explode, + value.In == ParameterInPath && sm.Style == SerializationLabel && sm.Explode, + value.In == ParameterInPath && sm.Style == SerializationMatrix && !sm.Explode, + value.In == ParameterInPath && sm.Style == SerializationMatrix && sm.Explode, + + value.In == ParameterInQuery && sm.Style == SerializationForm && sm.Explode, + value.In == ParameterInQuery && sm.Style == SerializationForm && !sm.Explode, + value.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && sm.Explode, + value.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && !sm.Explode, + value.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && sm.Explode, + value.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && !sm.Explode, + value.In == ParameterInQuery && sm.Style == SerializationDeepObject && sm.Explode, + + value.In == ParameterInHeader && sm.Style == SerializationSimple && !sm.Explode, + value.In == ParameterInHeader && sm.Style == SerializationSimple && sm.Explode, + + value.In == ParameterInCookie && sm.Style == SerializationForm && !sm.Explode, + value.In == ParameterInCookie && sm.Style == SerializationForm && sm.Explode: smSupported = true } if !smSupported { e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in) - return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) + return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, e) } - if (parameter.Schema == nil) == (parameter.Content == nil) { + if (value.Schema == nil) == (value.Content == nil) { e := errors.New("parameter must contain exactly one of content and schema") - return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) + return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, e) } - if schema := parameter.Schema; schema != nil { - if err := schema.Validate(c); err != nil { - return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err) + if schema := value.Schema; schema != nil { + if err := schema.Validate(ctx); err != nil { + return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, err) } } - if content := parameter.Content; content != nil { - if err := content.Validate(c); err != nil { - return fmt.Errorf("parameter %q content is invalid: %v", parameter.Name, err) + if content := value.Content; content != nil { + if err := content.Validate(ctx); err != nil { + return fmt.Errorf("parameter %q content is invalid: %v", value.Name, err) } } return nil diff --git a/openapi3/parameter_issue223_test.go b/openapi3/parameter_issue223_test.go index ae1ddd2e9..336d42fb1 100644 --- a/openapi3/parameter_issue223_test.go +++ b/openapi3/parameter_issue223_test.go @@ -109,7 +109,7 @@ components: type: string ` - doc, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + doc, err := NewLoader().LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(context.Background()) require.EqualError(t, err, `invalid paths: operation GET /pets/{petId} must define exactly all path parameters`) diff --git a/openapi3/path_item.go b/openapi3/path_item.go index f7cf1d989..a66502046 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -116,9 +116,9 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { } } -func (pathItem *PathItem) Validate(c context.Context) error { - for _, operation := range pathItem.Operations() { - if err := operation.Validate(c); err != nil { +func (value *PathItem) Validate(ctx context.Context) error { + for _, operation := range value.Operations() { + if err := operation.Validate(ctx); err != nil { return err } } diff --git a/openapi3/paths.go b/openapi3/paths.go index baafaaabc..c6ddbf3bf 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -9,16 +9,16 @@ import ( // Paths is specified by OpenAPI/Swagger standard version 3.0. type Paths map[string]*PathItem -func (paths Paths) Validate(c context.Context) error { +func (value Paths) Validate(ctx context.Context) error { normalizedPaths := make(map[string]string) - for path, pathItem := range paths { + for path, pathItem := range value { if path == "" || path[0] != '/' { return fmt.Errorf("path %q does not start with a forward slash (/)", path) } if pathItem == nil { - paths[path] = &PathItem{} - pathItem = paths[path] + value[path] = &PathItem{} + pathItem = value[path] } normalizedPath, pathParamsCount := normalizeTemplatedPath(path) @@ -49,7 +49,7 @@ func (paths Paths) Validate(c context.Context) error { } } - if err := pathItem.Validate(c); err != nil { + if err := pathItem.Validate(ctx); err != nil { return err } } diff --git a/openapi3/paths_test.go b/openapi3/paths_test.go index cc8d9306c..402288b67 100644 --- a/openapi3/paths_test.go +++ b/openapi3/paths_test.go @@ -21,8 +21,8 @@ paths: ` func TestPathValidate(t *testing.T) { - swagger, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(emptyPathSpec)) + doc, err := NewLoader().LoadFromData([]byte(emptyPathSpec)) require.NoError(t, err) - err = swagger.Paths.Validate(context.Background()) + err = doc.Paths.Validate(context.Background()) require.NoError(t, err) } diff --git a/openapi3/refs.go b/openapi3/refs.go index a086e367e..4b64035f8 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -27,12 +27,11 @@ func (value *CallbackRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *CallbackRef) Validate(c context.Context) error { - v := value.Value - if v == nil { - return foundUnresolvedRef(value.Ref) +func (value *CallbackRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) } - return v.Validate(c) + return foundUnresolvedRef(value.Ref) } func (value CallbackRef) JSONLookup(token string) (interface{}, error) { @@ -59,8 +58,11 @@ func (value *ExampleRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *ExampleRef) Validate(c context.Context) error { - return nil +func (value *ExampleRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) + } + return foundUnresolvedRef(value.Ref) } func (value ExampleRef) JSONLookup(token string) (interface{}, error) { @@ -87,13 +89,13 @@ func (value *HeaderRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *HeaderRef) Validate(c context.Context) error { - v := value.Value - if v == nil { - return foundUnresolvedRef(value.Ref) +func (value *HeaderRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) } - return v.Validate(c) + return foundUnresolvedRef(value.Ref) } + func (value HeaderRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil @@ -116,12 +118,11 @@ func (value *LinkRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *LinkRef) Validate(c context.Context) error { - v := value.Value - if v == nil { - return foundUnresolvedRef(value.Ref) +func (value *LinkRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) } - return v.Validate(c) + return foundUnresolvedRef(value.Ref) } type ParameterRef struct { @@ -139,12 +140,11 @@ func (value *ParameterRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *ParameterRef) Validate(c context.Context) error { - v := value.Value - if v == nil { - return foundUnresolvedRef(value.Ref) +func (value *ParameterRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) } - return v.Validate(c) + return foundUnresolvedRef(value.Ref) } func (value ParameterRef) JSONLookup(token string) (interface{}, error) { @@ -171,12 +171,11 @@ func (value *ResponseRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *ResponseRef) Validate(c context.Context) error { - v := value.Value - if v == nil { - return foundUnresolvedRef(value.Ref) +func (value *ResponseRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) } - return v.Validate(c) + return foundUnresolvedRef(value.Ref) } func (value ResponseRef) JSONLookup(token string) (interface{}, error) { @@ -203,12 +202,11 @@ func (value *RequestBodyRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *RequestBodyRef) Validate(c context.Context) error { - v := value.Value - if v == nil { - return foundUnresolvedRef(value.Ref) +func (value *RequestBodyRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) } - return v.Validate(c) + return foundUnresolvedRef(value.Ref) } func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { @@ -242,12 +240,11 @@ func (value *SchemaRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *SchemaRef) Validate(c context.Context) error { - v := value.Value - if v == nil { - return foundUnresolvedRef(value.Ref) +func (value *SchemaRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) } - return v.Validate(c) + return foundUnresolvedRef(value.Ref) } func (value SchemaRef) JSONLookup(token string) (interface{}, error) { @@ -274,12 +271,11 @@ func (value *SecuritySchemeRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } -func (value *SecuritySchemeRef) Validate(c context.Context) error { - v := value.Value - if v == nil { - return foundUnresolvedRef(value.Ref) +func (value *SecuritySchemeRef) Validate(ctx context.Context) error { + if v := value.Value; v != nil { + return v.Validate(ctx) } - return v.Validate(c) + return foundUnresolvedRef(value.Ref) } func (value SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index 7de298ca4..e714da455 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -108,7 +108,7 @@ components: type: string ` - _, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + _, err := NewLoader().LoadFromData([]byte(spec)) require.EqualError(t, err, `invalid response: value MUST be an object`) } @@ -213,7 +213,7 @@ components: - type: integer format: int32 ` - root, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + root, err := NewLoader().LoadFromData([]byte(spec)) require.NoError(t, err) ptr, err := jsonpointer.New("/paths/~1pet/put/responses/200/content") diff --git a/openapi3/request_body.go b/openapi3/request_body.go index ad871e8fd..66b512fa0 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -97,9 +97,9 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, requestBody) } -func (requestBody *RequestBody) Validate(c context.Context) error { - if v := requestBody.Content; v != nil { - if err := v.Validate(c); err != nil { +func (value *RequestBody) Validate(ctx context.Context) error { + if v := value.Content; v != nil { + if err := v.Validate(ctx); err != nil { return err } } diff --git a/openapi3/response.go b/openapi3/response.go index 7c4da1dc2..2ab33aca2 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -29,12 +29,12 @@ func (responses Responses) Get(status int) *ResponseRef { return responses[strconv.FormatInt(int64(status), 10)] } -func (responses Responses) Validate(c context.Context) error { - if len(responses) == 0 { +func (value Responses) Validate(ctx context.Context) error { + if len(value) == 0 { return errors.New("the responses object MUST contain at least one response code") } - for _, v := range responses { - if err := v.Validate(c); err != nil { + for _, v := range value { + if err := v.Validate(ctx); err != nil { return err } } @@ -94,13 +94,13 @@ func (response *Response) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, response) } -func (response *Response) Validate(c context.Context) error { - if response.Description == nil { +func (value *Response) Validate(ctx context.Context) error { + if value.Description == nil { return errors.New("a short description of the response is required") } - if content := response.Content; content != nil { - if err := content.Validate(c); err != nil { + if content := value.Content; content != nil { + if err := content.Validate(ctx); err != nil { return err } } diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go index 2a175808e..b456d8832 100644 --- a/openapi3/response_issue224_test.go +++ b/openapi3/response_issue224_test.go @@ -453,7 +453,7 @@ func TestEmptyResponsesAreInvalid(t *testing.T) { } ` - doc, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + doc, err := NewLoader().LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(context.Background()) require.EqualError(t, err, `invalid paths: the responses object MUST contain at least one response code`) diff --git a/openapi3/schema.go b/openapi3/schema.go index 22cb84210..8c59f3c04 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -579,11 +579,11 @@ func (schema *Schema) IsEmpty() bool { return true } -func (schema *Schema) Validate(c context.Context) error { - return schema.validate(c, []*Schema{}) +func (value *Schema) Validate(ctx context.Context) error { + return value.validate(ctx, []*Schema{}) } -func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { +func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { for _, existing := range stack { if existing == schema { return @@ -600,7 +600,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(c, stack); err == nil { + if err = v.validate(ctx, stack); err == nil { return } } @@ -610,7 +610,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(c, stack); err != nil { + if err = v.validate(ctx, stack); err != nil { return } } @@ -620,7 +620,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(c, stack); err != nil { + if err = v.validate(ctx, stack); err != nil { return } } @@ -630,7 +630,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(c, stack); err != nil { + if err = v.validate(ctx, stack); err != nil { return } } @@ -691,7 +691,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(c, stack); err != nil { + if err = v.validate(ctx, stack); err != nil { return } } @@ -701,7 +701,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(c, stack); err != nil { + if err = v.validate(ctx, stack); err != nil { return } } @@ -711,7 +711,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) { if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(c, stack); err != nil { + if err = v.validate(ctx, stack); err != nil { return } } diff --git a/openapi3/schema_issue289_test.go b/openapi3/schema_issue289_test.go index e4e4aad36..6ab6b63d5 100644 --- a/openapi3/schema_issue289_test.go +++ b/openapi3/schema_issue289_test.go @@ -29,7 +29,7 @@ func TestIssue289(t *testing.T) { openapi: "3.0.1" `) - s, err := NewSwaggerLoader().LoadSwaggerFromData(spec) + s, err := NewLoader().LoadFromData(spec) require.NoError(t, err) err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ "name": "kin-openapi", diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 91747d84b..f621ca7c9 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1211,7 +1211,7 @@ components: "name": "kin-openapi", "ownerName": true, } - s, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(api)) + s, err := NewLoader().LoadFromData([]byte(api)) require.NoError(t, err) require.NotNil(t, s) err = s.Components.Schemas["Test"].Value.VisitJSON(data) diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index 1d2c745f7..ce6fcc6f1 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -15,9 +15,9 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) * return srs } -func (srs SecurityRequirements) Validate(c context.Context) error { - for _, item := range srs { - if err := item.Validate(c); err != nil { +func (value SecurityRequirements) Validate(ctx context.Context) error { + for _, item := range value { + if err := item.Validate(ctx); err != nil { return err } } @@ -38,6 +38,6 @@ func (security SecurityRequirement) Authenticate(provider string, scopes ...stri return security } -func (security SecurityRequirement) Validate(c context.Context) error { +func (value SecurityRequirement) Validate(ctx context.Context) error { return nil } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index d1f665cd9..990f258d4 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -103,15 +103,15 @@ func (ss *SecurityScheme) WithBearerFormat(value string) *SecurityScheme { return ss } -func (ss *SecurityScheme) Validate(c context.Context) error { +func (value *SecurityScheme) Validate(ctx context.Context) error { hasIn := false hasBearerFormat := false hasFlow := false - switch ss.Type { + switch value.Type { case "apiKey": hasIn = true case "http": - scheme := ss.Scheme + scheme := value.Scheme switch scheme { case "bearer": hasBearerFormat = true @@ -122,46 +122,46 @@ func (ss *SecurityScheme) Validate(c context.Context) error { case "oauth2": hasFlow = true case "openIdConnect": - if ss.OpenIdConnectUrl == "" { - return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", ss.Name) + if value.OpenIdConnectUrl == "" { + return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", value.Name) } default: - return fmt.Errorf("security scheme 'type' can't be %q", ss.Type) + return fmt.Errorf("security scheme 'type' can't be %q", value.Type) } // Validate "in" and "name" if hasIn { - switch ss.In { + switch value.In { case "query", "header", "cookie": default: - return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", ss.In) + return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", value.In) } - if ss.Name == "" { + if value.Name == "" { return errors.New("security scheme of type 'apiKey' should have 'name'") } - } else if len(ss.In) > 0 { - return fmt.Errorf("security scheme of type %q can't have 'in'", ss.Type) - } else if len(ss.Name) > 0 { + } else if len(value.In) > 0 { + return fmt.Errorf("security scheme of type %q can't have 'in'", value.Type) + } else if len(value.Name) > 0 { return errors.New("security scheme of type 'apiKey' can't have 'name'") } // Validate "format" // "bearerFormat" is an arbitrary string so we only check if the scheme supports it - if !hasBearerFormat && len(ss.BearerFormat) > 0 { - return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", ss.Type) + if !hasBearerFormat && len(value.BearerFormat) > 0 { + return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", value.Type) } // Validate "flow" if hasFlow { - flow := ss.Flows + flow := value.Flows if flow == nil { - return fmt.Errorf("security scheme of type %q should have 'flows'", ss.Type) + return fmt.Errorf("security scheme of type %q should have 'flows'", value.Type) } - if err := flow.Validate(c); err != nil { + if err := flow.Validate(ctx); err != nil { return fmt.Errorf("security scheme 'flow' is invalid: %v", err) } - } else if ss.Flows != nil { - return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type) + } else if value.Flows != nil { + return fmt.Errorf("security scheme of type %q can't have 'flows'", value.Type) } return nil } @@ -191,18 +191,18 @@ func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flows) } -func (flows *OAuthFlows) Validate(c context.Context) error { +func (flows *OAuthFlows) Validate(ctx context.Context) error { if v := flows.Implicit; v != nil { - return v.Validate(c, oAuthFlowTypeImplicit) + return v.Validate(ctx, oAuthFlowTypeImplicit) } if v := flows.Password; v != nil { - return v.Validate(c, oAuthFlowTypePassword) + return v.Validate(ctx, oAuthFlowTypePassword) } if v := flows.ClientCredentials; v != nil { - return v.Validate(c, oAuthFlowTypeClientCredentials) + return v.Validate(ctx, oAuthFlowTypeClientCredentials) } if v := flows.AuthorizationCode; v != nil { - return v.Validate(c, oAuthFlowAuthorizationCode) + return v.Validate(ctx, oAuthFlowAuthorizationCode) } return errors.New("no OAuth flow is defined") } @@ -223,7 +223,7 @@ func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flow) } -func (flow *OAuthFlow) Validate(c context.Context, typ oAuthFlowType) error { +func (flow *OAuthFlow) Validate(ctx context.Context, typ oAuthFlowType) error { if typ == oAuthFlowAuthorizationCode || typ == oAuthFlowTypeImplicit { if v := flow.AuthorizationURL; v == "" { return errors.New("an OAuth flow is missing 'authorizationUrl in authorizationCode or implicit '") diff --git a/openapi3/server.go b/openapi3/server.go index 6cdeb4afd..4415bd08f 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -15,9 +15,9 @@ import ( type Servers []*Server // Validate ensures servers are per the OpenAPIv3 specification. -func (servers Servers) Validate(c context.Context) error { - for _, v := range servers { - if err := v.Validate(c); err != nil { +func (value Servers) Validate(ctx context.Context) error { + for _, v := range value { + if err := v.Validate(ctx); err != nil { return err } } @@ -125,22 +125,22 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) { return params, input, true } -func (server *Server) Validate(c context.Context) (err error) { - if server.URL == "" { +func (value *Server) Validate(ctx context.Context) (err error) { + if value.URL == "" { return errors.New("value of url must be a non-empty string") } - opening, closing := strings.Count(server.URL, "{"), strings.Count(server.URL, "}") + opening, closing := strings.Count(value.URL, "{"), strings.Count(value.URL, "}") if opening != closing { return errors.New("server URL has mismatched { and }") } - if opening != len(server.Variables) { + if opening != len(value.Variables) { return errors.New("server has undeclared variables") } - for name, v := range server.Variables { - if !strings.Contains(server.URL, fmt.Sprintf("{%s}", name)) { + for name, v := range value.Variables { + if !strings.Contains(value.URL, fmt.Sprintf("{%s}", name)) { return errors.New("server has undeclared variables") } - if err = v.Validate(c); err != nil { + if err = v.Validate(ctx); err != nil { return } } @@ -163,9 +163,9 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, serverVariable) } -func (serverVariable *ServerVariable) Validate(c context.Context) error { - if serverVariable.Default == "" { - data, err := serverVariable.MarshalJSON() +func (value *ServerVariable) Validate(ctx context.Context) error { + if value.Default == "" { + data, err := value.MarshalJSON() if err != nil { return err } diff --git a/openapi3/testdata/load_with_go_embed_test.go b/openapi3/testdata/load_with_go_embed_test.go index ffcf35fcb..9cdace562 100644 --- a/openapi3/testdata/load_with_go_embed_test.go +++ b/openapi3/testdata/load_with_go_embed_test.go @@ -14,13 +14,13 @@ import ( var fs embed.FS func Example() { - loader := openapi3.NewSwaggerLoader() + loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = true - loader.ReadFromURIFunc = func(loader *openapi3.SwaggerLoader, uri *url.URL) ([]byte, error) { + loader.ReadFromURIFunc = func(loader *openapi3.Loader, uri *url.URL) ([]byte, error) { return fs.ReadFile(uri.Path) } - doc, err := loader.LoadSwaggerFromFile("recursiveRef/openapi.yml") + doc, err := loader.LoadFromFile("recursiveRef/openapi.yml") if err != nil { panic(err) } diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index c461da94f..397c9e9b1 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -914,7 +914,7 @@ func TestDecodeParameter(t *testing.T) { Title: "MyAPI", Version: "0.1", } - spec := &openapi3.Swagger{OpenAPI: "3.0.0", Info: info} + spec := &openapi3.T{OpenAPI: "3.0.0", Info: info} op := &openapi3.Operation{ OperationID: "test", Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go index cefadf77e..454a927e9 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -65,8 +65,8 @@ func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { ID string `json:"_id"` } - sl := openapi3.NewSwaggerLoader() - doc, err := sl.LoadSwaggerFromData([]byte(spec)) + sl := openapi3.NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(sl.Context) require.NoError(t, err) diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index f1e7bf977..2f9a5f14c 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -25,7 +25,7 @@ var ErrInvalidRequired = errors.New("value is required but missing") // // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker -func ValidateRequest(c context.Context, input *RequestValidationInput) error { +func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { var ( err error me openapi3.MultiError @@ -49,7 +49,7 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { } } - if err = ValidateParameter(c, input, parameter); err != nil && !options.MultiError { + if err = ValidateParameter(ctx, input, parameter); err != nil && !options.MultiError { return err } @@ -60,7 +60,7 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { // For each parameter of the Operation for _, parameter := range operationParameters { - if err = ValidateParameter(c, input, parameter.Value); err != nil && !options.MultiError { + if err = ValidateParameter(ctx, input, parameter.Value); err != nil && !options.MultiError { return err } @@ -72,7 +72,7 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { // RequestBody requestBody := operation.RequestBody if requestBody != nil && !options.ExcludeRequestBody { - if err = ValidateRequestBody(c, input, requestBody.Value); err != nil && !options.MultiError { + if err = ValidateRequestBody(ctx, input, requestBody.Value); err != nil && !options.MultiError { return err } @@ -86,10 +86,10 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { // If there aren't any security requirements for the operation if security == nil { // Use the global security requirements. - security = &route.Swagger.Security + security = &route.Spec.Security } if security != nil { - if err = ValidateSecurityRequirements(c, input, *security); err != nil && !options.MultiError { + if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError { return err } @@ -109,10 +109,10 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error { // The function returns RequestError with a ParseError cause when unable to parse a value. // The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined. // The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. -func ValidateParameter(c context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error { +func ValidateParameter(ctx context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error { if parameter.Schema == nil && parameter.Content == nil { // We have no schema for the parameter. Assume that everything passes - // a schema-less check, but this could also be an error. The Swagger + // a schema-less check, but this could also be an error. The OpenAPI // validation allows this to happen. return nil } @@ -166,7 +166,7 @@ const prefixInvalidCT = "header Content-Type has unexpected value" // // The function returns RequestError with ErrInvalidRequired cause when a value is required but not defined. // The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. -func ValidateRequestBody(c context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error { +func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error { var ( req = input.Request data []byte @@ -252,13 +252,13 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque // ValidateSecurityRequirements goes through multiple OpenAPI 3 security // requirements in order and returns nil on the first valid requirement. // If no requirement is met, errors are returned in order. -func ValidateSecurityRequirements(c context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error { +func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error { if len(srs) == 0 { return nil } var errs []error for _, sr := range srs { - if err := validateSecurityRequirement(c, input, sr); err != nil { + if err := validateSecurityRequirement(ctx, input, sr); err != nil { if len(errs) == 0 { errs = make([]error, 0, len(srs)) } @@ -274,9 +274,9 @@ func ValidateSecurityRequirements(c context.Context, input *RequestValidationInp } // validateSecurityRequirement validates a single OpenAPI 3 security requirement -func validateSecurityRequirement(c context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error { - swagger := input.Route.Swagger - securitySchemes := swagger.Components.SecuritySchemes +func validateSecurityRequirement(ctx context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error { + doc := input.Route.Spec + securitySchemes := doc.Components.SecuritySchemes // Ensure deterministic order names := make([]string, 0, len(securityRequirement)) @@ -310,7 +310,7 @@ func validateSecurityRequirement(c context.Context, input *RequestValidationInpu } } scopes := securityRequirement[name] - if err := f(c, &AuthenticationInput{ + if err := f(ctx, &AuthenticationInput{ RequestValidationInput: input, SecuritySchemeName: name, SecurityScheme: securityScheme, diff --git a/openapi3filter/validate_request_input.go b/openapi3filter/validate_request_input.go index 14c661bbb..91dd102b6 100644 --- a/openapi3filter/validate_request_input.go +++ b/openapi3filter/validate_request_input.go @@ -8,7 +8,7 @@ import ( "github.com/getkin/kin-openapi/routers" ) -// A ContentParameterDecoder takes a parameter definition from the swagger spec, +// A ContentParameterDecoder takes a parameter definition from the OpenAPI spec, // and the value which we received for it. It is expected to return the // value unmarshaled into an interface which can be traversed for // validation, it should also return the schema to be used for validating the diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index 2c1c0b5cc..60e3f2eba 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -64,8 +64,8 @@ components: ` func Example() { - loader := openapi3.NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData([]byte(spec)) + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) if err != nil { panic(err) } diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index 9575c4c1e..b70938e7c 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -17,7 +17,7 @@ import ( // // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker -func ValidateResponse(c context.Context, input *ResponseValidationInput) error { +func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error { req := input.RequestValidationInput.Request switch req.Method { case "HEAD": diff --git a/openapi3filter/validation_discriminator_test.go b/openapi3filter/validation_discriminator_test.go index a3eb8a8c7..c7d614403 100644 --- a/openapi3filter/validation_discriminator_test.go +++ b/openapi3filter/validation_discriminator_test.go @@ -75,8 +75,8 @@ components: type: integer ` - loader := openapi3.NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromData([]byte(spec)) + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) require.NoError(t, err) router, err := legacyrouter.NewRouter(doc) diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index d03fb7dbf..d539c3f7e 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -28,7 +28,7 @@ func newPetstoreRequest(t *testing.T, method, path string, body io.Reader) *http type validationFields struct { Handler http.Handler - SwaggerFile string + File string ErrorEncoder ErrorEncoder } type validationArgs struct { @@ -543,12 +543,12 @@ func TestValidationErrorEncoder(t *testing.T) { } func buildValidationHandler(tt *validationTest) (*ValidationHandler, error) { - if tt.fields.SwaggerFile == "" { - tt.fields.SwaggerFile = "fixtures/petstore.json" + if tt.fields.File == "" { + tt.fields.File = "fixtures/petstore.json" } h := &ValidationHandler{ Handler: tt.fields.Handler, - SwaggerFile: tt.fields.SwaggerFile, + File: tt.fields.File, ErrorEncoder: tt.fields.ErrorEncoder, } tt.wantErr = tt.wantErr || @@ -588,7 +588,7 @@ func runTest_ServeHTTP(t *testing.T, handler http.Handler, encoder ErrorEncoder, h := &ValidationHandler{ Handler: handler, ErrorEncoder: encoder, - SwaggerFile: "fixtures/petstore.json", + File: "fixtures/petstore.json", } err := h.Load() require.NoError(t, err) @@ -600,7 +600,7 @@ func runTest_ServeHTTP(t *testing.T, handler http.Handler, encoder ErrorEncoder, func runTest_Middleware(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response { h := &ValidationHandler{ ErrorEncoder: encoder, - SwaggerFile: "fixtures/petstore.json", + File: "fixtures/petstore.json", } err := h.Load() require.NoError(t, err) diff --git a/openapi3filter/validation_handler.go b/openapi3filter/validation_handler.go index 111ece745..eeb1ca1ea 100644 --- a/openapi3filter/validation_handler.go +++ b/openapi3filter/validation_handler.go @@ -18,14 +18,14 @@ var _ AuthenticationFunc = NoopAuthenticationFunc type ValidationHandler struct { Handler http.Handler AuthenticationFunc AuthenticationFunc - SwaggerFile string + File string ErrorEncoder ErrorEncoder router routers.Router } func (h *ValidationHandler) Load() error { - loader := openapi3.NewSwaggerLoader() - doc, err := loader.LoadSwaggerFromFile(h.SwaggerFile) + loader := openapi3.NewLoader() + doc, err := loader.LoadFromFile(h.File) if err != nil { return err } diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index 9db76748a..d4d0a12f1 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -45,7 +45,7 @@ func TestFilter(t *testing.T) { complexArgSchema.Required = []string{"name", "id"} // Declare router - doc := &openapi3.Swagger{ + doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -528,7 +528,7 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test }, } - doc := &openapi3.Swagger{ + doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -596,7 +596,7 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test Request: httpReq, Route: route, Options: &Options{ - AuthenticationFunc: func(c context.Context, input *AuthenticationInput) error { + AuthenticationFunc: func(ctx context.Context, input *AuthenticationInput) error { if schemesValidated != nil { if validated, ok := (*schemesValidated)[input.SecurityScheme]; ok { if validated { @@ -662,7 +662,7 @@ func TestAnySecurityRequirementMet(t *testing.T) { }, } - doc := openapi3.Swagger{ + doc := openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -759,7 +759,7 @@ func TestAllSchemesMet(t *testing.T) { }, } - doc := openapi3.Swagger{ + doc := openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", @@ -840,8 +840,8 @@ func TestAllSchemesMet(t *testing.T) { // makeAuthFunc creates an authentication function that accepts the given valid schemes. // If an invalid or unknown scheme is encountered, an error is returned by the returned function. // Otherwise the return value of the returned function is nil. -func makeAuthFunc(schemes map[string]bool) func(c context.Context, input *AuthenticationInput) error { - return func(c context.Context, input *AuthenticationInput) error { +func makeAuthFunc(schemes map[string]bool) func(ctx context.Context, input *AuthenticationInput) error { + return func(ctx context.Context, input *AuthenticationInput) error { // If the scheme is valid and present in the schemes valid, present := schemes[input.SecuritySchemeName] if valid && present { diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 896f5fa47..4011932db 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -26,7 +26,7 @@ type Router struct { // NewRouter creates a gorilla/mux router. // Assumes spec is .Validate()d // TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request)) -func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { +func NewRouter(doc *openapi3.T) (routers.Router, error) { type srv struct { schemes []string host, base string @@ -81,7 +81,7 @@ func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { } r.muxes = append(r.muxes, muxRoute) r.routes = append(r.routes, &routers.Route{ - Swagger: doc, + Spec: doc, Server: s.server, Path: path, PathItem: pathItem, @@ -103,7 +103,7 @@ func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string } route := r.routes[i] route.Method = req.Method - route.Operation = route.Swagger.Paths[route.Path].GetOperation(route.Method) + route.Operation = route.Spec.Paths[route.Path].GetOperation(route.Method) return route, match.Vars, nil } switch match.MatchErr { diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index 1a3d1123e..d9ad2c9c6 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -24,7 +24,7 @@ func TestRouter(t *testing.T) { paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()} booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()} - doc := &openapi3.Swagger{ + doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", diff --git a/routers/legacy/router.go b/routers/legacy/router.go index 1cb1426b6..ecaae1348 100644 --- a/routers/legacy/router.go +++ b/routers/legacy/router.go @@ -50,15 +50,15 @@ func (rs Routers) FindRoute(req *http.Request) (routers.Router, *routers.Route, // Router maps a HTTP request to an OpenAPI operation. type Router struct { - doc *openapi3.Swagger + doc *openapi3.T pathNode *pathpattern.Node } // NewRouter creates a new router. // -// If the given Swagger has servers, router will use them. -// All operations of the Swagger will be added to the router. -func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { +// If the given OpenAPIv3 document has servers, router will use them. +// All operations of the document will be added to the router. +func NewRouter(doc *openapi3.T) (routers.Router, error) { if err := doc.Validate(context.Background()); err != nil { return nil, fmt.Errorf("validating OpenAPI failed: %v", err) } @@ -68,7 +68,7 @@ func NewRouter(doc *openapi3.Swagger) (routers.Router, error) { for method, operation := range pathItem.Operations() { method = strings.ToUpper(method) if err := root.Add(method+" "+path, &routers.Route{ - Swagger: doc, + Spec: doc, Path: path, PathItem: pathItem, Method: method, diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go index 082eab571..bfc7e11e5 100644 --- a/routers/legacy/router_test.go +++ b/routers/legacy/router_test.go @@ -24,7 +24,7 @@ func TestRouter(t *testing.T) { paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()} booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()} - doc := &openapi3.Swagger{ + doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", diff --git a/routers/types.go b/routers/types.go index b15b3ba94..3cdbee32c 100644 --- a/routers/types.go +++ b/routers/types.go @@ -13,7 +13,7 @@ type Router interface { // Route describes the operation an http.Request can match type Route struct { - Swagger *openapi3.Swagger + Spec *openapi3.T Server *openapi3.Server Path string PathItem *openapi3.PathItem From abfd78fd09dd5b67a11cc16af1a5fb0dcc12dd32 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 26 Apr 2021 20:28:38 +0200 Subject: [PATCH 070/260] Fix CI (#352) Signed-off-by: Pierre Fenoll --- .github/workflows/go.yml | 6 +++++- openapi3/testdata/go.mod | 2 +- openapi3/testdata/go.sum | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c770eaff6..6233f67e8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -65,7 +65,11 @@ jobs: run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - run: go test ./... - - run: cd openapi3/testdata && go test -tags with_embed ./... && cd - + - run: | + cd openapi3/testdata + go get -u -v github.com/getkin/kin-openapi + go test -tags with_embed ./... + cd - if: matrix.go != '1.14' - if: runner.os == 'Linux' run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] diff --git a/openapi3/testdata/go.mod b/openapi3/testdata/go.mod index b58d02338..2d875e74e 100644 --- a/openapi3/testdata/go.mod +++ b/openapi3/testdata/go.mod @@ -2,4 +2,4 @@ module github.com/getkin/kin-openapi/openapi3/testdata.test go 1.16 -require github.com/getkin/kin-openapi v0.52.0 // indirect +require github.com/getkin/kin-openapi v0.61.0 diff --git a/openapi3/testdata/go.sum b/openapi3/testdata/go.sum index 0739f618e..0418aff2b 100644 --- a/openapi3/testdata/go.sum +++ b/openapi3/testdata/go.sum @@ -1,7 +1,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M= github.com/getkin/kin-openapi v0.52.0/go.mod h1:fRpo2Nw4Czgy0QnrIesRrEXs5+15N1F9mGZLP/aIomE= +github.com/getkin/kin-openapi v0.61.0 h1:6awGqF5nG5zkVpMsAih1QH4VgzS8phTxECUWIFo7zko= +github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= @@ -9,17 +12,22 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34 github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= From 070c62845c61e34c38c9f2e53e8593ec5e0b57c4 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 29 Apr 2021 14:24:58 +0200 Subject: [PATCH 071/260] cannot reproduce #353 (#354) Signed-off-by: Pierre Fenoll --- routers/gorillamux/example_test.go | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 routers/gorillamux/example_test.go diff --git a/routers/gorillamux/example_test.go b/routers/gorillamux/example_test.go new file mode 100644 index 000000000..33ce98e6a --- /dev/null +++ b/routers/gorillamux/example_test.go @@ -0,0 +1,64 @@ +package gorillamux_test + +import ( + "context" + "fmt" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func Example() { + ctx := context.Background() + loader := &openapi3.Loader{Context: ctx, IsExternalRefsAllowed: true} + doc, err := loader.LoadFromFile("../../openapi3/testdata/pathref.openapi.yml") + if err != nil { + panic(err) + } + if err = doc.Validate(ctx); err != nil { + panic(err) + } + router, err := gorillamux.NewRouter(doc) + if err != nil { + panic(err) + } + httpReq, err := http.NewRequest(http.MethodGet, "/test", nil) + if err != nil { + panic(err) + } + + route, pathParams, err := router.FindRoute(httpReq) + if err != nil { + panic(err) + } + + requestValidationInput := &openapi3filter.RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } + if err := openapi3filter.ValidateRequest(ctx, requestValidationInput); err != nil { + panic(err) + } + + responseValidationInput := &openapi3filter.ResponseValidationInput{ + RequestValidationInput: requestValidationInput, + Status: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + } + responseValidationInput.SetBodyBytes([]byte(`{}`)) + + err = openapi3filter.ValidateResponse(ctx, responseValidationInput) + fmt.Println(err) + // Output: + // response body doesn't match the schema: Field must be set to string or not be present + // Schema: + // { + // "type": "string" + // } + // + // Value: + // "object" +} From 7be9302a58ed4c9fcf9de9a437b49f98e747c358 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 6 May 2021 14:21:55 +0200 Subject: [PATCH 072/260] add example usage of request validation with gorilla/mux router (#359) Signed-off-by: Pierre Fenoll --- openapi3filter/testdata/petstore.yaml | 100 ++++++++++ openapi3filter/unpack_errors_test.go | 173 ++++++++++++++++++ .../legacy}/validate_request_test.go | 6 +- 3 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 openapi3filter/testdata/petstore.yaml create mode 100644 openapi3filter/unpack_errors_test.go rename {openapi3filter => routers/legacy}/validate_request_test.go (95%) diff --git a/openapi3filter/testdata/petstore.yaml b/openapi3filter/testdata/petstore.yaml new file mode 100644 index 000000000..e3b61bff9 --- /dev/null +++ b/openapi3filter/testdata/petstore.yaml @@ -0,0 +1,100 @@ +openapi: "3.0.0" +info: + description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters." + version: "1.0.0" + title: "Swagger Petstore" + termsOfService: "http://swagger.io/terms/" + contact: + email: "apiteam@swagger.io" + license: + name: "Apache 2.0" + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: +- name: "pet" + description: "Everything about your Pets" + externalDocs: + description: "Find out more" + url: "http://swagger.io" +- name: "store" + description: "Access to Petstore orders" +- name: "user" + description: "Operations about user" + externalDocs: + description: "Find out more about our store" + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - "pet" + summary: "Add a new pet to the store" + description: "" + operationId: "addPet" + requestBody: + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/Pet' + responses: + "405": + description: "Invalid input" +components: + schemas: + Category: + type: "object" + properties: + id: + type: "integer" + format: "int64" + name: + type: "string" + xml: + name: "Category" + Tag: + type: "object" + properties: + id: + type: "integer" + format: "int64" + name: + type: "string" + xml: + name: "Tag" + Pet: + type: "object" + required: + - "name" + - "photoUrls" + properties: + id: + type: "integer" + format: "int64" + category: + $ref: "#/components/schemas/Category" + name: + type: "string" + example: "doggie" + photoUrls: + type: "array" + xml: + name: "photoUrl" + wrapped: true + items: + type: "string" + tags: + type: "array" + xml: + name: "tag" + wrapped: true + items: + $ref: "#/components/schemas/Tag" + status: + type: "string" + description: "pet status in the store" + enum: + - "available" + - "pending" + - "sold" + xml: + name: "Pet" diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go new file mode 100644 index 000000000..4242177f9 --- /dev/null +++ b/openapi3filter/unpack_errors_test.go @@ -0,0 +1,173 @@ +package openapi3filter_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "sort" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func Example() { + doc, err := openapi3.NewLoader().LoadFromFile("./testdata/petstore.yaml") + if err != nil { + panic(err) + } + + router, err := gorillamux.NewRouter(doc) + if err != nil { + panic(err) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route, pathParams, err := router.FindRoute(r) + if err != nil { + fmt.Println(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{ + Request: r, + PathParams: pathParams, + Route: route, + Options: &openapi3filter.Options{ + MultiError: true, + }, + }) + switch err := err.(type) { + case nil: + case openapi3.MultiError: + issues := convertError(err) + names := make([]string, 0, len(issues)) + for k := range issues { + names = append(names, k) + } + sort.Strings(names) + for _, k := range names { + msgs := issues[k] + fmt.Println("===== Start New Error =====") + fmt.Println(k + ":") + for _, msg := range msgs { + fmt.Printf("\t%s\n", msg) + } + } + w.WriteHeader(http.StatusBadRequest) + default: + fmt.Println(err.Error()) + w.WriteHeader(http.StatusBadRequest) + } + })) + defer ts.Close() + + // (note invalid type for name and invalid status) + body := strings.NewReader(`{"name": 100, "photoUrls": [], "status": "invalidStatus"}`) + req, err := http.NewRequest("POST", ts.URL+"/pet", body) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + fmt.Printf("response: %d %s\n", resp.StatusCode, resp.Body) + + // Output: + // ===== Start New Error ===== + // @body.name: + // Error at "/name": Field must be set to string or not be present + // Schema: + // { + // "example": "doggie", + // "type": "string" + // } + // + // Value: + // "number, integer" + // + // ===== Start New Error ===== + // @body.status: + // Error at "/status": value is not one of the allowed values + // Schema: + // { + // "description": "pet status in the store", + // "enum": [ + // "available", + // "pending", + // "sold" + // ], + // "type": "string" + // } + // + // Value: + // "invalidStatus" + // + // response: 400 {} +} + +const ( + prefixBody = "@body" + unknown = "@unknown" +) + +func convertError(me openapi3.MultiError) map[string][]string { + issues := make(map[string][]string) + for _, err := range me { + switch err := err.(type) { + case *openapi3.SchemaError: + // Can inspect schema validation errors here, e.g. err.Value + field := prefixBody + if path := err.JSONPointer(); len(path) > 0 { + field = fmt.Sprintf("%s.%s", field, strings.Join(path, ".")) + } + if _, ok := issues[field]; !ok { + issues[field] = make([]string, 0, 3) + } + issues[field] = append(issues[field], err.Error()) + case *openapi3filter.RequestError: // possible there were multiple issues that failed validation + if err, ok := err.Err.(openapi3.MultiError); ok { + for k, v := range convertError(err) { + if _, ok := issues[k]; !ok { + issues[k] = make([]string, 0, 3) + } + issues[k] = append(issues[k], v...) + } + continue + } + + // check if invalid HTTP parameter + if err.Parameter != nil { + prefix := err.Parameter.In + name := fmt.Sprintf("%s.%s", prefix, err.Parameter.Name) + if _, ok := issues[name]; !ok { + issues[name] = make([]string, 0, 3) + } + issues[name] = append(issues[name], err.Error()) + continue + } + + // check if requestBody + if err.RequestBody != nil { + if _, ok := issues[prefixBody]; !ok { + issues[prefixBody] = make([]string, 0, 3) + } + issues[prefixBody] = append(issues[prefixBody], err.Error()) + continue + } + default: + reasons, ok := issues[unknown] + if !ok { + reasons = make([]string, 0, 3) + } + reasons = append(reasons, err.Error()) + issues[unknown] = reasons + } + } + return issues +} diff --git a/openapi3filter/validate_request_test.go b/routers/legacy/validate_request_test.go similarity index 95% rename from openapi3filter/validate_request_test.go rename to routers/legacy/validate_request_test.go index 60e3f2eba..7737f5028 100644 --- a/openapi3filter/validate_request_test.go +++ b/routers/legacy/validate_request_test.go @@ -1,4 +1,4 @@ -package openapi3filter_test +package legacy_test import ( "bytes" @@ -8,7 +8,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" - legacyrouter "github.com/getkin/kin-openapi/routers/legacy" + "github.com/getkin/kin-openapi/routers/legacy" ) const spec = ` @@ -73,7 +73,7 @@ func Example() { panic(err) } - router, err := legacyrouter.NewRouter(doc) + router, err := legacy.NewRouter(doc) if err != nil { panic(err) } From 56338d2b64168461d42bd904e82cd6b788a9ef87 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 20 May 2021 12:38:15 +0200 Subject: [PATCH 073/260] Have Header Object follow the structure of the Parameter Object (#355) --- openapi3/header.go | 74 ++++++++++++++++++++++++++++++++------- openapi3/openapi3_test.go | 11 ++++-- openapi3/parameter.go | 38 ++++++++++---------- 3 files changed, 89 insertions(+), 34 deletions(-) diff --git a/openapi3/header.go b/openapi3/header.go index 7cf61f8c6..5fdc31771 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -2,6 +2,7 @@ package openapi3 import ( "context" + "errors" "fmt" "github.com/getkin/kin-openapi/jsoninfo" @@ -24,17 +25,10 @@ func (h Headers) JSONLookup(token string) (interface{}, error) { return ref.Value, nil } +// Header is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#headerObject type Header struct { - ExtensionProps - - // Optional description. Should use CommonMark syntax. - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Parameter } var _ jsonpointer.JSONPointable = (*Header)(nil) @@ -43,10 +37,52 @@ func (value *Header) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } +// SerializationMethod returns a header's serialization method. +func (value *Header) SerializationMethod() (*SerializationMethod, error) { + style := value.Style + if style == "" { + style = SerializationSimple + } + explode := false + if value.Explode != nil { + explode = *value.Explode + } + return &SerializationMethod{Style: style, Explode: explode}, nil +} + func (value *Header) Validate(ctx context.Context) error { - if v := value.Schema; v != nil { - if err := v.Validate(ctx); err != nil { - return err + if value.Name != "" { + return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map") + } + if value.In != "" { + return errors.New("header 'in' MUST NOT be specified, it is implicitly in header") + } + + // Validate a parameter's serialization method. + sm, err := value.SerializationMethod() + if err != nil { + return err + } + if smSupported := false || + sm.Style == SerializationSimple && !sm.Explode || + sm.Style == SerializationSimple && sm.Explode; !smSupported { + e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a header parameter", sm.Style, sm.Explode) + return fmt.Errorf("header schema is invalid: %v", e) + } + + if (value.Schema == nil) == (value.Content == nil) { + e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", value) + return fmt.Errorf("header schema is invalid: %v", e) + } + if schema := value.Schema; schema != nil { + if err := schema.Validate(ctx); err != nil { + return fmt.Errorf("header schema is invalid: %v", err) + } + } + + if content := value.Content; content != nil { + if err := content.Validate(ctx); err != nil { + return fmt.Errorf("header content is invalid: %v", err) } } return nil @@ -61,8 +97,20 @@ func (value Header) JSONLookup(token string) (interface{}, error) { } return value.Schema.Value, nil } + case "name": + return value.Name, nil + case "in": + return value.In, nil case "description": return value.Description, nil + case "style": + return value.Style, nil + case "explode": + return value.Explode, nil + case "allowEmptyValue": + return value.AllowEmptyValue, nil + case "allowReserved": + return value.AllowReserved, nil case "deprecated": return value.Deprecated, nil case "required": diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 4c6bfe3ca..38d488d4e 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -130,7 +130,8 @@ components: someSchema: description: Some schema headers: - otherHeader: {} + otherHeader: + schema: {type: string} someHeader: "$ref": "#/components/headers/otherHeader" examples: @@ -207,7 +208,11 @@ var specJSON = []byte(` } }, "headers": { - "otherHeader": {}, + "otherHeader": { + "schema": { + "type": "string" + } + }, "someHeader": { "$ref": "#/components/headers/otherHeader" } @@ -317,7 +322,7 @@ func spec() *T { Ref: "#/components/headers/otherHeader", }, "otherHeader": { - Value: &Header{}, + Value: &Header{Parameter{Schema: &SchemaRef{Value: NewStringSchema()}}}, }, }, Examples: map[string]*ExampleRef{ diff --git a/openapi3/parameter.go b/openapi3/parameter.go index f4b91adf0..2081e4e1d 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -83,6 +83,7 @@ func (value Parameters) Validate(ctx context.Context) error { } // Parameter is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#parameterObject type Parameter struct { ExtensionProps Name string `json:"name,omitempty" yaml:"name,omitempty"` @@ -167,42 +168,42 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, parameter) } -func (parameter Parameter) JSONLookup(token string) (interface{}, error) { +func (value Parameter) JSONLookup(token string) (interface{}, error) { switch token { case "schema": - if parameter.Schema != nil { - if parameter.Schema.Ref != "" { - return &Ref{Ref: parameter.Schema.Ref}, nil + if value.Schema != nil { + if value.Schema.Ref != "" { + return &Ref{Ref: value.Schema.Ref}, nil } - return parameter.Schema.Value, nil + return value.Schema.Value, nil } case "name": - return parameter.Name, nil + return value.Name, nil case "in": - return parameter.In, nil + return value.In, nil case "description": - return parameter.Description, nil + return value.Description, nil case "style": - return parameter.Style, nil + return value.Style, nil case "explode": - return parameter.Explode, nil + return value.Explode, nil case "allowEmptyValue": - return parameter.AllowEmptyValue, nil + return value.AllowEmptyValue, nil case "allowReserved": - return parameter.AllowReserved, nil + return value.AllowReserved, nil case "deprecated": - return parameter.Deprecated, nil + return value.Deprecated, nil case "required": - return parameter.Required, nil + return value.Required, nil case "example": - return parameter.Example, nil + return value.Example, nil case "examples": - return parameter.Examples, nil + return value.Examples, nil case "content": - return parameter.Content, nil + return value.Content, nil } - v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token) return v, err } @@ -294,6 +295,7 @@ func (value *Parameter) Validate(ctx context.Context) error { return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, err) } } + if content := value.Content; content != nil { if err := content.Validate(ctx); err != nil { return fmt.Errorf("parameter %q content is invalid: %v", value.Name, err) From 93b779808793a8a6b54ffc1f87ba17d0ffa12b70 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 20 May 2021 13:03:40 +0200 Subject: [PATCH 074/260] CI: fix tests after tag (#363) Signed-off-by: Pierre Fenoll --- openapi3/testdata/go.mod | 2 +- openapi3/testdata/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openapi3/testdata/go.mod b/openapi3/testdata/go.mod index 2d875e74e..75d8c6f6c 100644 --- a/openapi3/testdata/go.mod +++ b/openapi3/testdata/go.mod @@ -2,4 +2,4 @@ module github.com/getkin/kin-openapi/openapi3/testdata.test go 1.16 -require github.com/getkin/kin-openapi v0.61.0 +require github.com/getkin/kin-openapi v0.62.0 diff --git a/openapi3/testdata/go.sum b/openapi3/testdata/go.sum index 0418aff2b..840581548 100644 --- a/openapi3/testdata/go.sum +++ b/openapi3/testdata/go.sum @@ -5,6 +5,8 @@ github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6Gcjss github.com/getkin/kin-openapi v0.52.0/go.mod h1:fRpo2Nw4Czgy0QnrIesRrEXs5+15N1F9mGZLP/aIomE= github.com/getkin/kin-openapi v0.61.0 h1:6awGqF5nG5zkVpMsAih1QH4VgzS8phTxECUWIFo7zko= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getkin/kin-openapi v0.62.0 h1:qDGdXTLo20ANSgflJEYotfNzHGvYvilNogWOryEwRrI= +github.com/getkin/kin-openapi v0.62.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= From 707e4ba1acb2ff3340c197dde6d4527b9e8bcbec Mon Sep 17 00:00:00 2001 From: jasmanx11 <61581398+jasmanx11@users.noreply.github.com> Date: Mon, 7 Jun 2021 22:21:05 +0800 Subject: [PATCH 075/260] Update openapi2_conv.go (#365) Co-authored-by: Pierre Fenoll --- openapi2conv/openapi2_conv.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 05447ac67..6877e88e8 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -232,6 +232,9 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete ExtensionProps: parameter.ExtensionProps, } if parameter.Name != "" { + if result.Extensions == nil { + result.Extensions = make(map[string]interface{}) + } result.Extensions["x-originalParamName"] = parameter.Name } From 9f8b1acb3de3dc51c4dfe01fc396d695b84fc228 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 21 Jun 2021 16:27:52 +0200 Subject: [PATCH 076/260] update that tag again... (#374) --- openapi3/testdata/go.mod | 2 +- openapi3/testdata/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openapi3/testdata/go.mod b/openapi3/testdata/go.mod index 75d8c6f6c..21eab28a6 100644 --- a/openapi3/testdata/go.mod +++ b/openapi3/testdata/go.mod @@ -2,4 +2,4 @@ module github.com/getkin/kin-openapi/openapi3/testdata.test go 1.16 -require github.com/getkin/kin-openapi v0.62.0 +require github.com/getkin/kin-openapi v0.63.0 diff --git a/openapi3/testdata/go.sum b/openapi3/testdata/go.sum index 840581548..2795d3cbc 100644 --- a/openapi3/testdata/go.sum +++ b/openapi3/testdata/go.sum @@ -7,6 +7,8 @@ github.com/getkin/kin-openapi v0.61.0 h1:6awGqF5nG5zkVpMsAih1QH4VgzS8phTxECUWIFo github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.62.0 h1:qDGdXTLo20ANSgflJEYotfNzHGvYvilNogWOryEwRrI= github.com/getkin/kin-openapi v0.62.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getkin/kin-openapi v0.63.0 h1:27zYoKAuHDSquDYRpfmgsDu9TKC5z5G4vlu/XmIdsa8= +github.com/getkin/kin-openapi v0.63.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= From 5ffbbe3711120676edb99bfd7b8f02eeaff52e72 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 23 Jun 2021 10:56:33 +0200 Subject: [PATCH 077/260] fix drilling down struct looking for additionalProperties (#377) --- openapi3/issue376_test.go | 42 +++++++++++++++++++++++++++++++++++++++ openapi3/loader.go | 15 ++++++++++++++ openapi3/schema.go | 4 ++-- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 openapi3/issue376_test.go diff --git a/openapi3/issue376_test.go b/openapi3/issue376_test.go new file mode 100644 index 000000000..c23ac24ab --- /dev/null +++ b/openapi3/issue376_test.go @@ -0,0 +1,42 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue376(t *testing.T) { + spec := []byte(` +openapi: 3.0.0 +components: + schemas: + schema1: + type: object + additionalProperties: + type: string + schema2: + type: object + properties: + prop: + $ref: '#/components/schemas/schema1/additionalProperties' +paths: {} +info: + title: An API + version: 1.2.3.4 +`) + + loader := NewLoader() + + doc, err := loader.LoadFromData(spec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + 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, "string", doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index ff931825f..95c6f1525 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -305,6 +305,9 @@ func (loader *Loader) resolveComponent( } var cursor interface{} if cursor, err = drill(doc); err != nil { + if path == nil { + return nil, err + } var err2 error data, err2 := loader.readURL(path) if err2 != nil { @@ -346,6 +349,14 @@ func (loader *Loader) resolveComponent( } 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; ap != nil { + return ap, nil + } + return s.Value.AdditionalPropertiesAllowed, nil + } + switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { case reflect.Map: elementValue := val.MapIndex(reflect.ValueOf(fieldName)) @@ -372,6 +383,10 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { field := val.Type().Field(i) tagValue := field.Tag.Get("yaml") yamlKey := strings.Split(tagValue, ",")[0] + if yamlKey == "-" { + tagValue := field.Tag.Get("multijson") + yamlKey = strings.Split(tagValue, ",")[0] + } if yamlKey == fieldName { return val.Field(i).Interface(), nil } diff --git a/openapi3/schema.go b/openapi3/schema.go index 8c59f3c04..fe6098f92 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -110,7 +110,7 @@ type Schema struct { ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` // Object-related, here for struct compactness - AdditionalPropertiesAllowed *bool `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"` + AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // Array-related, here for struct compactness UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` // Number-related, here for struct compactness @@ -145,7 +145,7 @@ type Schema struct { Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - AdditionalProperties *SchemaRef `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"` + AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } From d30837871e4548501ae7e3fc9ed0f74e35caa53d Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 23 Jun 2021 13:45:59 +0200 Subject: [PATCH 078/260] fix drilling down additionalProperties in the boolean case (#378) --- .github/workflows/go.yml | 1 + openapi3/issue376_test.go | 60 +++++++++++++++++++++++++++++++++++++++ openapi3/loader.go | 6 ++-- openapi3/schema.go | 15 +++++----- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6233f67e8..1639c89a3 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -69,6 +69,7 @@ jobs: cd openapi3/testdata go get -u -v github.com/getkin/kin-openapi go test -tags with_embed ./... + git --no-pager diff && git checkout -- . cd - if: matrix.go != '1.14' - if: runner.os == 'Linux' diff --git a/openapi3/issue376_test.go b/openapi3/issue376_test.go index c23ac24ab..22aa7fb40 100644 --- a/openapi3/issue376_test.go +++ b/openapi3/issue376_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -40,3 +41,62 @@ info: require.Equal(t, "string", doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type) } + +func TestMultijsonTagSerialization(t *testing.T) { + spec := []byte(` +openapi: 3.0.0 +components: + schemas: + unset: + type: number + #empty-object: + # TODO additionalProperties: {} + object: + additionalProperties: {type: string} + boolean: + additionalProperties: false +paths: {} +info: + title: An API + version: 1.2.3.4 +`) + + loader := NewLoader() + + doc, err := loader.LoadFromData(spec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + for propName, propSchema := range doc.Components.Schemas { + ap := propSchema.Value.AdditionalProperties + apa := propSchema.Value.AdditionalPropertiesAllowed + + encoded, err := propSchema.MarshalJSON() + require.NoError(t, err) + require.Equal(t, string(encoded), map[string]string{ + "unset": `{"type":"number"}`, + // TODO: "empty-object":`{"additionalProperties":{}}`, + "object": `{"additionalProperties":{"type":"string"}}`, + "boolean": `{"additionalProperties":false}`, + }[propName]) + + if propName == "unset" { + require.True(t, ap == nil && apa == nil) + continue + } + + apStr := "" + if ap != nil { + apStr = fmt.Sprintf("{Ref:%s Value.Type:%v}", (*ap).Ref, (*ap).Value.Type) + } + apaStr := "" + if apa != nil { + apaStr = fmt.Sprintf("%v", *apa) + } + + require.Truef(t, (ap != nil && apa == nil) || (ap == nil && apa != nil), + "%s: isnil(%s) xor isnil(%s)", propName, apaStr, apStr) + } +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 95c6f1525..da3479770 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -351,10 +351,10 @@ func (loader *Loader) resolveComponent( 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; ap != nil { - return ap, nil + if ap := s.Value.AdditionalPropertiesAllowed; ap != nil { + return *ap, nil } - return s.Value.AdditionalPropertiesAllowed, nil + return s.Value.AdditionalProperties, nil } switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { diff --git a/openapi3/schema.go b/openapi3/schema.go index fe6098f92..0d1ad4237 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -109,8 +109,6 @@ type Schema struct { Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - // Object-related, here for struct compactness - AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // Array-related, here for struct compactness UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` // Number-related, here for struct compactness @@ -141,12 +139,13 @@ type Schema struct { Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` // Object - Required []string `json:"required,omitempty" yaml:"required,omitempty"` - Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` - MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` - MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` + MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // In this order... + AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // ...for multijson + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } var _ jsonpointer.JSONPointable = (*Schema)(nil) From 19ba1f805a9885f57f111d46cb6cc1b76cfb06cc Mon Sep 17 00:00:00 2001 From: Alexander Bolgov <49677698+alexanderbolgov-ef@users.noreply.github.com> Date: Thu, 24 Jun 2021 12:16:38 +0200 Subject: [PATCH 079/260] Compile pattern on validate (#375) Co-authored-by: Pierre Fenoll --- .github/workflows/go.yml | 3 +++ openapi3/race_test.go | 27 +++++++++++++++++++++++++++ openapi3/schema.go | 26 ++++++++++++++++++-------- openapi3/schema_test.go | 10 ++++++++++ 4 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 openapi3/race_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1639c89a3..be19f943a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -65,6 +65,9 @@ jobs: run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - run: go test ./... + - run: go test -v -run TestRaceyPatternSchema -race ./... + env: + CGO_ENABLED: '1' - run: | cd openapi3/testdata go get -u -v github.com/getkin/kin-openapi diff --git a/openapi3/race_test.go b/openapi3/race_test.go new file mode 100644 index 000000000..4ac31c38e --- /dev/null +++ b/openapi3/race_test.go @@ -0,0 +1,27 @@ +package openapi3_test + +import ( + "context" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func TestRaceyPatternSchema(t *testing.T) { + schema := openapi3.Schema{ + Pattern: "^test|for|race|condition$", + Type: "string", + } + + err := schema.Validate(context.Background()) + require.NoError(t, err) + + visit := func() { + err := schema.VisitJSONString("test") + require.NoError(t, err) + } + + go visit() + visit() +} diff --git a/openapi3/schema.go b/openapi3/schema.go index 0d1ad4237..001062760 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -676,6 +676,11 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } } + if schema.Pattern != "" { + if err = schema.compilePattern(); err != nil { + return err + } + } case "array": if schema.Items == nil { return errors.New("when schema type is 'array', schema 'items' must be non-null") @@ -1138,15 +1143,9 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } // "pattern" - if pattern := schema.Pattern; pattern != "" && schema.compiledPattern == nil { + if schema.Pattern != "" && schema.compiledPattern == nil { var err error - if schema.compiledPattern, err = regexp.Compile(pattern); err != nil { - err = &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "pattern", - Reason: fmt.Sprintf("cannot compile pattern %q: %v", pattern, err), - } + if err = schema.compilePattern(); err != nil { if !settings.multiError { return err } @@ -1460,6 +1459,17 @@ func (schema *Schema) expectedType(settings *schemaValidationSettings, typ strin } } +func (schema *Schema) compilePattern() (err error) { + if schema.compiledPattern, err = regexp.Compile(schema.Pattern); err != nil { + return &SchemaError{ + Schema: schema, + SchemaField: "pattern", + Reason: fmt.Sprintf("cannot compile pattern %q: %v", schema.Pattern, err), + } + } + return nil +} + type SchemaError struct { Value interface{} reversePath []string diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index f621ca7c9..f724f08e2 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1219,3 +1219,13 @@ components: require.NotEqual(t, errSchema, err) require.Contains(t, err.Error(), `Error at "/ownerName": Doesn't match schema "not"`) } + +func TestValidationFailsOnInvalidPattern(t *testing.T) { + schema := Schema{ + Pattern: "[", + Type: "string", + } + + var err = schema.Validate(context.Background()) + require.Error(t, err) +} From 6b4444becdebac4ea5658cbe205bac8c27a7ec41 Mon Sep 17 00:00:00 2001 From: bianca rosa Date: Mon, 28 Jun 2021 11:21:02 -0300 Subject: [PATCH 080/260] Add uint type to openapi3gen (#379) --- openapi3gen/openapi3gen.go | 3 +++ openapi3gen/openapi3gen_test.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index f7041334d..960698324 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -144,6 +144,9 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec case reflect.Int64: schema.Type = "integer" schema.Format = "int64" + case reflect.Uint: + schema.Type = "integer" + schema.Min = &zeroInt case reflect.Uint8: schema.Type = "integer" schema.Min = &zeroInt diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index a975b2a7a..777bb9dad 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -39,3 +39,17 @@ func TestExportedNonTagged(t *testing.T) { "even_a_yaml": {Value: &openapi3.Schema{Type: "string"}}, }}}, schemaRef) } + +func TestExportUint(t *testing.T) { + type UnsignedIntStruct struct { + UnsignedInt uint `json:"uint"` + } + + schemaRef, _, err := NewSchemaRefForValue(&UnsignedIntStruct{}, UseAllExportedFields()) + require.NoError(t, err) + require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ + Type: "object", + Properties: map[string]*openapi3.SchemaRef{ + "uint": {Value: &openapi3.Schema{Type: "integer", Min: &zeroInt}}, + }}}, schemaRef) +} From bde5325550db1fa371168738ec29331e4f646795 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 23 Jul 2021 14:08:13 +0200 Subject: [PATCH 081/260] reproduce and fix issue #382 (#383) --- openapi3/issue382_test.go | 15 +++++ openapi3/parameter_issue223_test.go | 2 +- openapi3/paths.go | 77 +++++++++++++++++------ openapi3/testdata/Test_param_override.yml | 40 ++++++++++++ 4 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 openapi3/issue382_test.go create mode 100644 openapi3/testdata/Test_param_override.yml diff --git a/openapi3/issue382_test.go b/openapi3/issue382_test.go new file mode 100644 index 000000000..c29b7e981 --- /dev/null +++ b/openapi3/issue382_test.go @@ -0,0 +1,15 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOverridingGlobalParametersValidation(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromFile("testdata/Test_param_override.yml") + require.NoError(t, err) + err = doc.Validate(loader.Context) + require.NoError(t, err) +} diff --git a/openapi3/parameter_issue223_test.go b/openapi3/parameter_issue223_test.go index 336d42fb1..9b86954f4 100644 --- a/openapi3/parameter_issue223_test.go +++ b/openapi3/parameter_issue223_test.go @@ -112,5 +112,5 @@ components: doc, err := NewLoader().LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(context.Background()) - require.EqualError(t, err, `invalid paths: operation GET /pets/{petId} must define exactly all path parameters`) + require.EqualError(t, err, `invalid paths: operation GET /pets/{petId} must define exactly all path parameters (missing: [petId])`) } diff --git a/openapi3/paths.go b/openapi3/paths.go index c6ddbf3bf..bdb87ae7d 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -21,31 +21,60 @@ func (value Paths) Validate(ctx context.Context) error { pathItem = value[path] } - normalizedPath, pathParamsCount := normalizeTemplatedPath(path) + normalizedPath, _, varsInPath := normalizeTemplatedPath(path) if oldPath, ok := normalizedPaths[normalizedPath]; ok { return fmt.Errorf("conflicting paths %q and %q", path, oldPath) } normalizedPaths[path] = path - var globalCount uint + var commonParams []string for _, parameterRef := range pathItem.Parameters { if parameterRef != nil { if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath { - globalCount++ + commonParams = append(commonParams, parameter.Name) } } } for method, operation := range pathItem.Operations() { - var count uint + var setParams []string for _, parameterRef := range operation.Parameters { if parameterRef != nil { if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath { - count++ + setParams = append(setParams, parameter.Name) } } } - if count+globalCount != pathParamsCount { - return fmt.Errorf("operation %s %s must define exactly all path parameters", method, path) + if expected := len(setParams) + len(commonParams); expected != len(varsInPath) { + expected -= len(varsInPath) + if expected < 0 { + expected *= -1 + } + missing := make(map[string]struct{}, expected) + definedParams := append(setParams, commonParams...) + for _, name := range definedParams { + if _, ok := varsInPath[name]; !ok { + missing[name] = struct{}{} + } + } + for name := range varsInPath { + got := false + for _, othername := range definedParams { + if othername == name { + got = true + break + } + } + if !got { + missing[name] = struct{}{} + } + } + if len(missing) != 0 { + missings := make([]string, 0, len(missing)) + for name := range missing { + missings = append(missings, name) + } + return fmt.Errorf("operation %s %s must define exactly all path parameters (missing: %v)", method, path, missings) + } } } @@ -75,9 +104,9 @@ func (paths Paths) Find(key string) *PathItem { return pathItem } - normalizedPath, expected := normalizeTemplatedPath(key) + normalizedPath, expected, _ := normalizeTemplatedPath(key) for path, pathItem := range paths { - pathNormalized, got := normalizeTemplatedPath(path) + pathNormalized, got, _ := normalizeTemplatedPath(path) if got == expected && pathNormalized == normalizedPath { return pathItem } @@ -85,43 +114,51 @@ func (paths Paths) Find(key string) *PathItem { return nil } -func normalizeTemplatedPath(path string) (string, uint) { +func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) { if strings.IndexByte(path, '{') < 0 { - return path, 0 + return path, 0, nil } - var buf strings.Builder - buf.Grow(len(path)) + var buffTpl strings.Builder + buffTpl.Grow(len(path)) var ( cc rune count uint isVariable bool + vars = make(map[string]struct{}) + buffVar strings.Builder ) for i, c := range path { if isVariable { if c == '}' { - // End path variables + // End path variable + isVariable = false + + vars[buffVar.String()] = struct{}{} + buffVar = strings.Builder{} + // First append possible '*' before this character // The character '}' will be appended if i > 0 && cc == '*' { - buf.WriteRune(cc) + buffTpl.WriteRune(cc) } - isVariable = false } else { - // Skip this character + buffVar.WriteRune(c) continue } + } else if c == '{' { // Begin path variable - // The character '{' will be appended isVariable = true + + // The character '{' will be appended count++ } // Append the character - buf.WriteRune(c) + buffTpl.WriteRune(c) cc = c } - return buf.String(), count + return buffTpl.String(), count, vars } diff --git a/openapi3/testdata/Test_param_override.yml b/openapi3/testdata/Test_param_override.yml new file mode 100644 index 000000000..d6982414a --- /dev/null +++ b/openapi3/testdata/Test_param_override.yml @@ -0,0 +1,40 @@ +openapi: 3.0.0 +info: + title: customer + version: '1.0' +servers: + - url: 'httpbin.kwaf-demo.test' +paths: + '/customers/{customer_id}': + parameters: + - schema: + type: integer + name: customer_id + in: path + required: true + get: + parameters: + - schema: + type: integer + maximum: 100 + name: customer_id + in: path + required: true + summary: customer + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + customer_id: + type: integer + customer_name: + type: string + operationId: get-customers-customer_id + description: Retrieve a specific customer by ID +components: + schemas: {} From 7ee16379f8c189dc3e49c6f5442847a9ce03a520 Mon Sep 17 00:00:00 2001 From: Derek Strickland <1111455+DerekStrickland@users.noreply.github.com> Date: Thu, 29 Jul 2021 06:53:50 -0400 Subject: [PATCH 082/260] Detect if a field is anonymous and handle the indirection (#386) --- openapi3gen/openapi3gen.go | 18 +++++++++++++++--- openapi3gen/openapi3gen_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 960698324..84d1f998d 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -217,9 +217,21 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec // If asked, try to use yaml tag name, fType := fieldInfo.JSONName, fieldInfo.Type if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields { - ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) - if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { - name, fType = tag, ff.Type + // Handle anonymous fields/embedded structs + if t.Field(fieldInfo.Index[0]).Anonymous { + ref, err := g.generateSchemaRefFor(parents, fType) + if err != nil { + return nil, err + } + if ref != nil { + g.SchemaRefs[ref]++ + schema.WithPropertyRef(name, ref) + } + } else { + ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) + if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { + name, fType = tag, ff.Type + } } } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 777bb9dad..f8e2430e2 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -1,6 +1,7 @@ package openapi3gen import ( + "reflect" "testing" "github.com/getkin/kin-openapi/openapi3" @@ -53,3 +54,33 @@ func TestExportUint(t *testing.T) { "uint": {Value: &openapi3.Schema{Type: "integer", Min: &zeroInt}}, }}}, schemaRef) } + +func TestEmbeddedStructs(t *testing.T) { + type EmbeddedStruct struct { + ID string + } + + type ContainerStruct struct { + Name string + EmbeddedStruct + } + + instance := &ContainerStruct{ + Name: "Container", + EmbeddedStruct: EmbeddedStruct{ + ID: "Embedded", + }, + } + + generator := NewGenerator(UseAllExportedFields()) + + schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) + require.NoError(t, err) + + var ok bool + _, ok = schemaRef.Value.Properties["Name"] + require.Equal(t, true, ok) + + _, ok = schemaRef.Value.Properties["ID"] + require.Equal(t, true, ok) +} From ed98f50ff5afcacd39585f52ef4cd2b0c3b0752a Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 29 Jul 2021 12:55:47 +0200 Subject: [PATCH 083/260] Add missing yaml tags in marshaling openapi2.T (#391) --- openapi2/openapi2.go | 150 ++++++++++++++--------------- openapi2/openapi2_test.go | 26 ++++- openapi3/response_issue224_test.go | 1 + 3 files changed, 97 insertions(+), 80 deletions(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 937cc0831..4d2714d56 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -12,20 +12,20 @@ import ( // T is the root of an OpenAPI v2 document type T struct { openapi3.ExtensionProps - Swagger string `json:"swagger"` - Info openapi3.Info `json:"info"` - ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"` - Schemes []string `json:"schemes,omitempty"` - Consumes []string `json:"consumes,omitempty"` - Host string `json:"host,omitempty"` - BasePath string `json:"basePath,omitempty"` - Paths map[string]*PathItem `json:"paths,omitempty"` - Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty,noref"` - Parameters map[string]*Parameter `json:"parameters,omitempty,noref"` - Responses map[string]*Response `json:"responses,omitempty,noref"` - SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty"` - Security SecurityRequirements `json:"security,omitempty"` - Tags openapi3.Tags `json:"tags,omitempty"` + Swagger string `json:"swagger" yaml:"swagger"` + Info openapi3.Info `json:"info" yaml:"info"` + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` + Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` + Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` + Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty,noref" yaml:"definitions,omitempty,noref"` + Parameters map[string]*Parameter `json:"parameters,omitempty,noref" yaml:"parameters,omitempty,noref"` + Responses map[string]*Response `json:"responses,omitempty,noref" yaml:"responses,omitempty,noref"` + SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` + Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` + Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } func (doc *T) MarshalJSON() ([]byte, error) { @@ -52,15 +52,15 @@ func (doc *T) AddOperation(path string, method string, operation *Operation) { type PathItem struct { openapi3.ExtensionProps - Ref string `json:"$ref,omitempty"` - Delete *Operation `json:"delete,omitempty"` - Get *Operation `json:"get,omitempty"` - Head *Operation `json:"head,omitempty"` - Options *Operation `json:"options,omitempty"` - Patch *Operation `json:"patch,omitempty"` - Post *Operation `json:"post,omitempty"` - Put *Operation `json:"put,omitempty"` - Parameters Parameters `json:"parameters,omitempty"` + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` + Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` + Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` + Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` + Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` + Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` + Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` + Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` } func (pathItem *PathItem) MarshalJSON() ([]byte, error) { @@ -141,16 +141,16 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { type Operation struct { openapi3.ExtensionProps - Summary string `json:"summary,omitempty"` - Description string `json:"description,omitempty"` - ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"` - Tags []string `json:"tags,omitempty"` - OperationID string `json:"operationId,omitempty"` - Parameters Parameters `json:"parameters,omitempty"` - Responses map[string]*Response `json:"responses"` - Consumes []string `json:"consumes,omitempty"` - Produces []string `json:"produces,omitempty"` - Security *SecurityRequirements `json:"security,omitempty"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` + OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Responses map[string]*Response `json:"responses" yaml:"responses"` + Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` + Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` + Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` } func (operation *Operation) MarshalJSON() ([]byte, error) { @@ -179,30 +179,30 @@ func (ps Parameters) Less(i, j int) bool { type Parameter struct { openapi3.ExtensionProps - Ref string `json:"$ref,omitempty"` - In string `json:"in,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - CollectionFormat string `json:"collectionFormat,omitempty"` - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Pattern string `json:"pattern,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty"` - Required bool `json:"required,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - ExclusiveMin bool `json:"exclusiveMinimum,omitempty"` - ExclusiveMax bool `json:"exclusiveMaximum,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty"` - Items *openapi3.SchemaRef `json:"items,omitempty"` - Enum []interface{} `json:"enum,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty"` - Minimum *float64 `json:"minimum,omitempty"` - Maximum *float64 `json:"maximum,omitempty"` - MaxLength *uint64 `json:"maxLength,omitempty"` - MaxItems *uint64 `json:"maxItems,omitempty"` - MinLength uint64 `json:"minLength,omitempty"` - MinItems uint64 `json:"minItems,omitempty"` - Default interface{} `json:"default,omitempty"` + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` } func (parameter *Parameter) MarshalJSON() ([]byte, error) { @@ -215,11 +215,11 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error { type Response struct { openapi3.ExtensionProps - Ref string `json:"$ref,omitempty"` - Description string `json:"description,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty"` - Headers map[string]*Header `json:"headers,omitempty"` - Examples map[string]interface{} `json:"examples,omitempty"` + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` } func (response *Response) MarshalJSON() ([]byte, error) { @@ -232,9 +232,9 @@ func (response *Response) UnmarshalJSON(data []byte) error { type Header struct { openapi3.ExtensionProps - Ref string `json:"$ref,omitempty"` - Description string `json:"description,omitempty"` - Type string `json:"type,omitempty"` + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` } func (header *Header) MarshalJSON() ([]byte, error) { @@ -249,16 +249,16 @@ type SecurityRequirements []map[string][]string type SecurityScheme struct { openapi3.ExtensionProps - Ref string `json:"$ref,omitempty"` - Description string `json:"description,omitempty"` - Type string `json:"type,omitempty"` - In string `json:"in,omitempty"` - Name string `json:"name,omitempty"` - Flow string `json:"flow,omitempty"` - AuthorizationURL string `json:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty"` - Scopes map[string]string `json:"scopes,omitempty"` - Tags openapi3.Tags `json:"tags,omitempty"` + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Flow string `json:"flow,omitempty" yaml:"flow,omitempty"` + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` + Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } func (securityScheme *SecurityScheme) MarshalJSON() ([]byte, error) { diff --git a/openapi2/openapi2_test.go b/openapi2/openapi2_test.go index 87bd5ce41..8d3efdf8a 100644 --- a/openapi2/openapi2_test.go +++ b/openapi2/openapi2_test.go @@ -7,6 +7,7 @@ import ( "reflect" "github.com/getkin/kin-openapi/openapi2" + "github.com/ghodss/yaml" ) func Example() { @@ -19,18 +20,33 @@ func Example() { if err = json.Unmarshal(input, &doc); err != nil { panic(err) } + if doc.ExternalDocs.Description != "Find out more about Swagger" { + panic(`doc.ExternalDocs was parsed incorrectly!`) + } - output, err := json.Marshal(doc) + outputJSON, err := json.Marshal(doc) if err != nil { panic(err) } + var docAgainFromJSON openapi2.T + if err = json.Unmarshal(outputJSON, &docAgainFromJSON); err != nil { + panic(err) + } + if !reflect.DeepEqual(doc, docAgainFromJSON) { + fmt.Println("objects doc & docAgainFromJSON should be the same") + } - var docAgain openapi2.T - if err = json.Unmarshal(output, &docAgain); err != nil { + outputYAML, err := yaml.Marshal(doc) + if err != nil { + panic(err) + } + var docAgainFromYAML openapi2.T + if err = yaml.Unmarshal(outputYAML, &docAgainFromYAML); err != nil { panic(err) } - if !reflect.DeepEqual(doc, docAgain) { - fmt.Println("objects doc & docAgain should be the same") + if !reflect.DeepEqual(doc, docAgainFromYAML) { + fmt.Println("objects doc & docAgainFromYAML should be the same") } + // Output: } diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go index b456d8832..97c7e6b20 100644 --- a/openapi3/response_issue224_test.go +++ b/openapi3/response_issue224_test.go @@ -455,6 +455,7 @@ func TestEmptyResponsesAreInvalid(t *testing.T) { doc, err := NewLoader().LoadFromData([]byte(spec)) require.NoError(t, err) + require.Equal(t, doc.ExternalDocs.Description, "See AsyncAPI example") err = doc.Validate(context.Background()) require.EqualError(t, err, `invalid paths: the responses object MUST contain at least one response code`) } From 7fd2ca16cdda3afc5f9a8b2dab3456a8fa2f662e Mon Sep 17 00:00:00 2001 From: Derek Strickland <1111455+DerekStrickland@users.noreply.github.com> Date: Tue, 3 Aug 2021 03:27:22 -0400 Subject: [PATCH 084/260] Support reference cycles (#393) --- openapi3gen/openapi3gen.go | 60 ++++++++++++++++++++++++++++++--- openapi3gen/openapi3gen_test.go | 32 +++++++++++++++++- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 84d1f998d..7c321fe7a 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -3,6 +3,7 @@ package openapi3gen import ( "encoding/json" + "fmt" "math" "reflect" "strings" @@ -22,6 +23,7 @@ type Option func(*generatorOpt) type generatorOpt struct { useAllExportedFields bool + throwErrorOnCycle bool } // UseAllExportedFields changes the default behavior of only @@ -30,6 +32,12 @@ func UseAllExportedFields() Option { return func(x *generatorOpt) { x.useAllExportedFields = true } } +// ThrowErrorOnCycle changes the default behavior of creating cycle +// refs to instead error if a cycle is detected. +func ThrowErrorOnCycle() Option { + return func(x *generatorOpt) { x.throwErrorOnCycle = true } +} + // NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef. func NewSchemaRefForValue(value interface{}, opts ...Option) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) { g := NewGenerator(opts...) @@ -104,6 +112,10 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec if a && b { vs, err := g.generateSchemaRefFor(parents, v.Type) if err != nil { + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { + g.SchemaRefs[vs]++ + return vs, nil + } return nil, err } refSchemaRef := RefSchemaRef @@ -185,7 +197,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Type = "array" items, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { - return nil, err + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { + items = g.generateCycleSchemaRef(t.Elem(), schema) + } else { + return nil, err + } } if items != nil { g.SchemaRefs[items]++ @@ -197,7 +213,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Type = "object" additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { - return nil, err + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { + additionalProperties = g.generateCycleSchemaRef(t.Elem(), schema) + } else { + return nil, err + } } if additionalProperties != nil { g.SchemaRefs[additionalProperties]++ @@ -221,7 +241,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec if t.Field(fieldInfo.Index[0]).Anonymous { ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { - return nil, err + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { + ref = g.generateCycleSchemaRef(fType, schema) + } else { + return nil, err + } } if ref != nil { g.SchemaRefs[ref]++ @@ -237,7 +261,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { - return nil, err + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { + ref = g.generateCycleSchemaRef(fType, schema) + } else { + return nil, err + } } if ref != nil { g.SchemaRefs[ref]++ @@ -255,6 +283,30 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec return openapi3.NewSchemaRef(t.Name(), schema), nil } +func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef { + var typeName string + switch t.Kind() { + case reflect.Ptr: + return g.generateCycleSchemaRef(t.Elem(), schema) + case reflect.Slice: + ref := g.generateCycleSchemaRef(t.Elem(), schema) + sliceSchema := openapi3.NewSchema() + sliceSchema.Type = "array" + sliceSchema.Items = ref + return openapi3.NewSchemaRef("", sliceSchema) + case reflect.Map: + ref := g.generateCycleSchemaRef(t.Elem(), schema) + mapSchema := openapi3.NewSchema() + mapSchema.Type = "object" + mapSchema.AdditionalProperties = ref + return openapi3.NewSchemaRef("", mapSchema) + default: + typeName = t.Name() + } + + return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema) +} + var RefSchemaRef = openapi3.NewSchemaRef("Ref", openapi3.NewObjectSchema().WithProperty("$ref", openapi3.NewStringSchema().WithMinLength(1))) diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index f8e2430e2..0062e2e5f 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -16,7 +16,7 @@ type CyclicType1 struct { } func TestCyclic(t *testing.T) { - schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{}) + schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{}, ThrowErrorOnCycle()) require.IsType(t, &CycleError{}, err) require.Nil(t, schemaRef) require.Empty(t, refsMap) @@ -84,3 +84,33 @@ func TestEmbeddedStructs(t *testing.T) { _, ok = schemaRef.Value.Properties["ID"] require.Equal(t, true, ok) } + +func TestCyclicReferences(t *testing.T) { + type ObjectDiff struct { + FieldCycle *ObjectDiff + SliceCycle []*ObjectDiff + MapCycle map[*ObjectDiff]*ObjectDiff + } + + instance := &ObjectDiff{ + FieldCycle: nil, + SliceCycle: nil, + MapCycle: nil, + } + + generator := NewGenerator(UseAllExportedFields()) + + schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) + require.NoError(t, err) + + require.NotNil(t, schemaRef.Value.Properties["FieldCycle"]) + require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["FieldCycle"].Ref) + + require.NotNil(t, schemaRef.Value.Properties["SliceCycle"]) + require.Equal(t, "array", schemaRef.Value.Properties["SliceCycle"].Value.Type) + require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["SliceCycle"].Value.Items.Ref) + + require.NotNil(t, schemaRef.Value.Properties["MapCycle"]) + require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) + require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) +} From fba0a14e84c1efe61f72dd9d5806fc5605fc6f2e Mon Sep 17 00:00:00 2001 From: Derek Strickland <1111455+DerekStrickland@users.noreply.github.com> Date: Sat, 7 Aug 2021 04:30:38 -0400 Subject: [PATCH 085/260] Add support for embedded struct pointers (#396) --- jsoninfo/field_info.go | 3 +++ openapi3gen/openapi3gen_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/jsoninfo/field_info.go b/jsoninfo/field_info.go index d2ad505bd..2382b731c 100644 --- a/jsoninfo/field_info.go +++ b/jsoninfo/field_info.go @@ -21,6 +21,9 @@ type FieldInfo struct { } func AppendFields(fields []FieldInfo, parentIndex []int, t reflect.Type) []FieldInfo { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } // For each field numField := t.NumField() iteration: diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 0062e2e5f..0b7fde9e6 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -85,6 +85,36 @@ func TestEmbeddedStructs(t *testing.T) { require.Equal(t, true, ok) } +func TestEmbeddedPointerStructs(t *testing.T) { + type EmbeddedStruct struct { + ID string + } + + type ContainerStruct struct { + Name string + *EmbeddedStruct + } + + instance := &ContainerStruct{ + Name: "Container", + EmbeddedStruct: &EmbeddedStruct{ + ID: "Embedded", + }, + } + + generator := NewGenerator(UseAllExportedFields()) + + schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) + require.NoError(t, err) + + var ok bool + _, ok = schemaRef.Value.Properties["Name"] + require.Equal(t, true, ok) + + _, ok = schemaRef.Value.Properties["ID"] + require.Equal(t, true, ok) +} + func TestCyclicReferences(t *testing.T) { type ObjectDiff struct { FieldCycle *ObjectDiff From b35b144093fb1c8a02890f979bdc276eaa628f46 Mon Sep 17 00:00:00 2001 From: Rodrigo Fernandes Date: Tue, 10 Aug 2021 18:10:33 +0100 Subject: [PATCH 086/260] fix: Allow encoded path parameters with slashes (#400) --- routers/gorillamux/router.go | 2 +- routers/gorillamux/router_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 4011932db..a47c762e7 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -56,7 +56,7 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { if len(servers) == 0 { servers = append(servers, srv{}) } - muxRouter := mux.NewRouter() /*.UseEncodedPath()?*/ + muxRouter := mux.NewRouter().UseEncodedPath() r := &Router{} for _, path := range orderedPaths(doc.Paths) { pathItem := doc.Paths[path] diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index d9ad2c9c6..c42d7f835 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -146,7 +146,7 @@ func TestRouter(t *testing.T) { expect(r, http.MethodGet, "/params/a/b/c%2Fd", paramsGET, map[string]string{ "x": "a", "y": "b", - "z": "c/d", + "z": "c%2Fd", }) expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{ "bookid": "War.and.Peace", From f16058d959bd18ecfd8be147bc2b11cbe1cd79d8 Mon Sep 17 00:00:00 2001 From: stakme Date: Wed, 11 Aug 2021 23:54:54 +0900 Subject: [PATCH 087/260] Accept multipart/form-data's part without Content-Type (#399) --- openapi3filter/req_resp_decoder.go | 5 +++++ openapi3filter/req_resp_decoder_test.go | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index b9b0cd8e5..2d2728b24 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -809,6 +809,11 @@ const prefixUnsupportedCT = "unsupported content type" // The function returns ParseError when a body is invalid. func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { contentType := header.Get(headerCT) + if contentType == "" { + if _, ok := body.(*multipart.Part); ok { + contentType = "text/plain" + } + } mediaType := parseMediaType(contentType) decoder, ok := bodyDecoders[mediaType] if !ok { diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 397c9e9b1..7ae863b82 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -974,6 +974,7 @@ func TestDecodeBody(t *testing.T) { {name: "c", contentType: "text/plain", data: strings.NewReader("c2")}, {name: "d", contentType: "application/json", data: bytes.NewReader(d)}, {name: "f", contentType: "application/octet-stream", data: strings.NewReader("foo"), filename: "f1"}, + {name: "g", data: strings.NewReader("g1")}, }) require.NoError(t, err) @@ -987,10 +988,14 @@ func TestDecodeBody(t *testing.T) { {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, {name: "x", contentType: "text/plain", data: strings.NewReader("x1")}, }) + require.NoError(t, err) + multipartAdditionalProps, multipartMimeAdditionalProps, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, {name: "x", contentType: "text/plain", data: strings.NewReader("x1")}, }) + require.NoError(t, err) + multipartAdditionalPropsErr, multipartMimeAdditionalPropsErr, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, {name: "x", contentType: "text/plain", data: strings.NewReader("x1")}, @@ -1075,8 +1080,9 @@ func TestDecodeBody(t *testing.T) { WithProperty("b", openapi3.NewIntegerSchema()). WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())). WithProperty("d", openapi3.NewObjectSchema().WithProperty("d1", openapi3.NewStringSchema())). - WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")), - want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo"}, + WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")). + WithProperty("g", openapi3.NewStringSchema()), + want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"}, }, { name: "multipartExtraPart", From ebccd50e55ee725ed0a0dfa1888188b9fca64b79 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 13 Aug 2021 16:00:22 +0200 Subject: [PATCH 088/260] fix that CI go:embed test forever, again (#405) --- .github/workflows/go.yml | 10 +++--- .gitignore | 2 ++ openapi3/testdata/go.mod | 5 --- openapi3/testdata/go.sum | 38 -------------------- openapi3/testdata/load_with_go_embed_test.go | 6 ++-- 5 files changed, 8 insertions(+), 53 deletions(-) delete mode 100644 openapi3/testdata/go.mod delete mode 100644 openapi3/testdata/go.sum diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index be19f943a..96beea138 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -68,15 +68,13 @@ jobs: - run: go test -v -run TestRaceyPatternSchema -race ./... env: CGO_ENABLED: '1' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - run: | - cd openapi3/testdata - go get -u -v github.com/getkin/kin-openapi - go test -tags with_embed ./... - git --no-pager diff && git checkout -- . - cd - + cp openapi3/testdata/load_with_go_embed_test.go openapi3/ + cat go.mod | sed 's%go 1.14%go 1.16%' >gomod && mv gomod go.mod + go test ./... if: matrix.go != '1.14' - if: runner.os == 'Linux' - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - if: runner.os == 'Linux' diff --git a/.gitignore b/.gitignore index 31ab03fb6..caf95473b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ # IntelliJ / GoLand .idea + +/openapi3/load_with_go_embed_test.go diff --git a/openapi3/testdata/go.mod b/openapi3/testdata/go.mod deleted file mode 100644 index 21eab28a6..000000000 --- a/openapi3/testdata/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/getkin/kin-openapi/openapi3/testdata.test - -go 1.16 - -require github.com/getkin/kin-openapi v0.63.0 diff --git a/openapi3/testdata/go.sum b/openapi3/testdata/go.sum deleted file mode 100644 index 2795d3cbc..000000000 --- a/openapi3/testdata/go.sum +++ /dev/null @@ -1,38 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M= -github.com/getkin/kin-openapi v0.52.0/go.mod h1:fRpo2Nw4Czgy0QnrIesRrEXs5+15N1F9mGZLP/aIomE= -github.com/getkin/kin-openapi v0.61.0 h1:6awGqF5nG5zkVpMsAih1QH4VgzS8phTxECUWIFo7zko= -github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= -github.com/getkin/kin-openapi v0.62.0 h1:qDGdXTLo20ANSgflJEYotfNzHGvYvilNogWOryEwRrI= -github.com/getkin/kin-openapi v0.62.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= -github.com/getkin/kin-openapi v0.63.0 h1:27zYoKAuHDSquDYRpfmgsDu9TKC5z5G4vlu/XmIdsa8= -github.com/getkin/kin-openapi v0.63.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/openapi3/testdata/load_with_go_embed_test.go b/openapi3/testdata/load_with_go_embed_test.go index 9cdace562..a5993dcc8 100644 --- a/openapi3/testdata/load_with_go_embed_test.go +++ b/openapi3/testdata/load_with_go_embed_test.go @@ -1,5 +1,3 @@ -//+build with_embed - package openapi3_test import ( @@ -10,7 +8,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -//go:embed recursiveRef/* +//go:embed testdata/recursiveRef/* var fs embed.FS func Example() { @@ -20,7 +18,7 @@ func Example() { return fs.ReadFile(uri.Path) } - doc, err := loader.LoadFromFile("recursiveRef/openapi.yml") + doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml") if err != nil { panic(err) } From 4a3eb86cb4b2fdf04372d066806d36d6f35c7b8e Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 13 Aug 2021 17:17:32 +0200 Subject: [PATCH 089/260] =?UTF-8?q?fix=20bad=20ci=20script.=20I=20was=20un?= =?UTF-8?q?der=20the=20impression=20this=20was=20working=20when=20I?= =?UTF-8?q?=E2=80=A6=20(#406)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 96beea138..4a5b87c21 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -68,13 +68,13 @@ jobs: - run: go test -v -run TestRaceyPatternSchema -race ./... env: CGO_ENABLED: '1' + - if: runner.os == 'Linux' run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - run: | cp openapi3/testdata/load_with_go_embed_test.go openapi3/ cat go.mod | sed 's%go 1.14%go 1.16%' >gomod && mv gomod go.mod go test ./... if: matrix.go != '1.14' - - if: runner.os == 'Linux' - if: runner.os == 'Linux' From 9b79d5d4792051e59178a323ab0136b545d06136 Mon Sep 17 00:00:00 2001 From: stakme Date: Sun, 15 Aug 2021 00:50:37 +0900 Subject: [PATCH 090/260] Fix handling recursive refs (#403) --- openapi3/loader.go | 20 +++++++++++++++++++ openapi3/loader_read_from_uri_func_test.go | 2 +- openapi3/loader_recursive_ref_test.go | 2 +- openapi3/testdata/load_with_go_embed_test.go | 4 ++-- .../testdata/recursiveRef/components/Foo.yml | 4 +--- .../recursiveRef/components/Foo/Foo2.yml | 4 ++++ openapi3/testdata/recursiveRef/openapi.yml | 2 ++ openapi3/testdata/recursiveRef/paths/foo.yml | 4 ++-- 8 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 openapi3/testdata/recursiveRef/components/Foo/Foo2.yml diff --git a/openapi3/loader.go b/openapi3/loader.go index da3479770..c42d8ba63 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -35,6 +35,8 @@ type Loader struct { Context context.Context + rootDir string + visitedPathItemRefs map[string]struct{} visitedDocuments map[string]*T @@ -66,6 +68,7 @@ func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) { // LoadFromFile loads a spec from a local file path func (loader *Loader) LoadFromFile(location string) (*T, error) { + loader.rootDir = path.Dir(location) return loader.LoadFromURI(&url.URL{Path: filepath.ToSlash(location)}) } @@ -415,6 +418,14 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { } } +func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef string) *url.URL { + if loader.rootDir == "" { + return current + } + return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)} + +} + func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { if ref != "" && ref[0] == '#' { return doc, ref, path, nil @@ -474,6 +485,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value @@ -521,6 +533,7 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value @@ -577,6 +590,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value @@ -632,6 +646,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value @@ -701,6 +716,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value @@ -778,6 +794,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil @@ -814,6 +831,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil @@ -841,6 +859,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value @@ -938,6 +957,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u return err } component.Value = resolved.Value + documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go index 72d4a95a7..8fee2f4c2 100644 --- a/openapi3/loader_read_from_uri_func_test.go +++ b/openapi3/loader_read_from_uri_func_test.go @@ -20,7 +20,7 @@ func TestLoaderReadFromURIFunc(t *testing.T) { require.NoError(t, err) require.NotNil(t, doc) require.NoError(t, doc.Validate(loader.Context)) - require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo"].Value.Properties["bar"].Value.Items.Value.Example) + 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) } type multipleSourceLoaderExample struct { diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go index bc1f24b88..bd6590364 100644 --- a/openapi3/loader_recursive_ref_test.go +++ b/openapi3/loader_recursive_ref_test.go @@ -13,5 +13,5 @@ func TestLoaderSupportsRecursiveReference(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["foo"].Value.Properties["bar"].Value.Items.Value.Example) + 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) } diff --git a/openapi3/testdata/load_with_go_embed_test.go b/openapi3/testdata/load_with_go_embed_test.go index a5993dcc8..56b274c9b 100644 --- a/openapi3/testdata/load_with_go_embed_test.go +++ b/openapi3/testdata/load_with_go_embed_test.go @@ -27,6 +27,6 @@ func Example() { panic(err) } - fmt.Println(doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo"].Value.Properties["bar"].Value.Type) - // Output: array + fmt.Println(doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Type) + // Output: string } diff --git a/openapi3/testdata/recursiveRef/components/Foo.yml b/openapi3/testdata/recursiveRef/components/Foo.yml index 0c0899277..53a233666 100644 --- a/openapi3/testdata/recursiveRef/components/Foo.yml +++ b/openapi3/testdata/recursiveRef/components/Foo.yml @@ -1,6 +1,4 @@ type: object properties: bar: - type: array - items: - $ref: ../openapi.yml#/components/schemas/Bar + $ref: ../openapi.yml#/components/schemas/Bar diff --git a/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml b/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml new file mode 100644 index 000000000..aeac81f48 --- /dev/null +++ b/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml @@ -0,0 +1,4 @@ +type: object +properties: + foo: + $ref: ../../openapi.yml#/components/schemas/Foo diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml index 5dfcfbf7c..3559c8e85 100644 --- a/openapi3/testdata/recursiveRef/openapi.yml +++ b/openapi3/testdata/recursiveRef/openapi.yml @@ -9,5 +9,7 @@ components: schemas: Foo: $ref: ./components/Foo.yml + Foo2: + $ref: ./components/Foo/Foo2.yml Bar: $ref: ./components/Bar.yml diff --git a/openapi3/testdata/recursiveRef/paths/foo.yml b/openapi3/testdata/recursiveRef/paths/foo.yml index dd6c15d0f..1653c7ac7 100644 --- a/openapi3/testdata/recursiveRef/paths/foo.yml +++ b/openapi3/testdata/recursiveRef/paths/foo.yml @@ -7,5 +7,5 @@ get: schema: type: object properties: - foo: - $ref: ../openapi.yml#/components/schemas/Foo + foo2: + $ref: ../openapi.yml#/components/schemas/Foo2 From e2c8b0cb8629e88c0cd7d37d1b2153a10167251a Mon Sep 17 00:00:00 2001 From: NaerChang2 Date: Mon, 16 Aug 2021 05:38:39 -0400 Subject: [PATCH 091/260] fix issue 407, where if server URL has no path it throws exception (#408) Co-authored-by: Naer Chang --- routers/gorillamux/router.go | 2 +- routers/gorillamux/router_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index a47c762e7..31d8dc8fe 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -43,7 +43,7 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { return nil, err } path := bDecode(u.EscapedPath()) - if path[len(path)-1] == '/' { + if len(path) > 0 && path[len(path)-1] == '/' { path = path[:len(path)-1] } servers = append(servers, srv{ diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index c42d7f835..8dc0a2eb1 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -206,3 +206,16 @@ func TestPermuteScheme(t *testing.T) { perms := permutePart(scheme0, server) require.Equal(t, []string{"http", "https"}, perms) } + +func TestServerPath(t *testing.T) { + server := &openapi3.Server{URL: "http://example.com"} + err := server.Validate(context.Background()) + require.NoError(t, err) + + _, err = NewRouter(&openapi3.T{Servers: openapi3.Servers{ + server, + &openapi3.Server{URL: "http://example.com/"}, + &openapi3.Server{URL: "http://example.com/path"}}, + }) + require.NoError(t, err) +} From dc944adc1492febff1977923fc2418f9d71290c8 Mon Sep 17 00:00:00 2001 From: Rodrigo Fernandes Date: Mon, 16 Aug 2021 10:44:09 +0100 Subject: [PATCH 092/260] =?UTF-8?q?feature:=20Add=20more=20discriminator?= =?UTF-8?q?=20error=20messages=20and=20return=20specific=20er=E2=80=A6=20(?= =?UTF-8?q?#394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: Add more discriminator error messages and return specific error when possible * feature: Always show more specific error message * test: Add schema oneOf tests * clean: Add missing word --- openapi3/schema.go | 66 +++++++------ openapi3/schema_oneOf_test.go | 118 ++++++++++++++++++++++++ routers/legacy/validate_request_test.go | 24 +---- 3 files changed, 160 insertions(+), 48 deletions(-) create mode 100644 openapi3/schema_oneOf_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 001062760..6878ad35d 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -824,10 +824,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(ref.Ref) } - var oldfailfast bool - oldfailfast, settings.failfast = settings.failfast, true err := v.visitJSON(settings, value) - settings.failfast = oldfailfast if err == nil { if settings.failfast { return errSchema @@ -841,33 +838,53 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val } if v := schema.OneOf; len(v) > 0 { + var discriminatorRef string + if schema.Discriminator != nil { + pn := schema.Discriminator.PropertyName + if valuemap, okcheck := value.(map[string]interface{}); okcheck { + discriminatorVal, okcheck := valuemap[pn] + if !okcheck { + return errors.New("input does not contain the discriminator property") + } + + if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorVal.(string)]; len(schema.Discriminator.Mapping) > 0 && !okcheck { + return errors.New("input does not contain a valid discriminator value") + } + } + } + ok := 0 + validationErrors := []error{} for _, item := range v { v := item.Value if v == nil { return foundUnresolvedRef(item.Ref) } - var oldfailfast bool - oldfailfast, settings.failfast = settings.failfast, true + + if discriminatorRef != "" && discriminatorRef != item.Ref { + continue + } + err := v.visitJSON(settings, value) - settings.failfast = oldfailfast - if err == nil { - if schema.Discriminator != nil { - pn := schema.Discriminator.PropertyName - if valuemap, okcheck := value.(map[string]interface{}); okcheck { - if discriminatorVal, okcheck := valuemap[pn]; okcheck == true { - mapref, okcheck := schema.Discriminator.Mapping[discriminatorVal.(string)] - if okcheck && mapref == item.Ref { - ok++ - } - } - } - } else { - ok++ - } + if err != nil { + validationErrors = append(validationErrors, err) + continue } + + ok++ } + if ok != 1 { + if len(validationErrors) > 1 { + errorMessage := "" + for _, err := range validationErrors { + if errorMessage != "" { + errorMessage += " Or " + } + errorMessage += err.Error() + } + return errors.New("doesn't match schema due to: " + errorMessage) + } if settings.failfast { return errSchema } @@ -878,7 +895,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val } if ok > 1 { e.Origin = ErrOneOfConflict + } else if len(validationErrors) == 1 { + e.Origin = validationErrors[0] } + return e } } @@ -890,10 +910,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(item.Ref) } - var oldfailfast bool - oldfailfast, settings.failfast = settings.failfast, true err := v.visitJSON(settings, value) - settings.failfast = oldfailfast if err == nil { ok = true break @@ -916,10 +933,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(item.Ref) } - var oldfailfast bool - oldfailfast, settings.failfast = settings.failfast, false err := v.visitJSON(settings, value) - settings.failfast = oldfailfast if err != nil { if settings.failfast { return errSchema diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go new file mode 100644 index 000000000..03fb670b1 --- /dev/null +++ b/openapi3/schema_oneOf_test.go @@ -0,0 +1,118 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +var oneofSpec = []byte(`components: + schemas: + Cat: + type: object + properties: + name: + type: string + scratches: + type: boolean + $type: + type: string + enum: + - cat + required: + - name + - scratches + - $type + Dog: + type: object + properties: + name: + type: string + barks: + type: boolean + $type: + type: string + enum: + - dog + required: + - name + - barks + - $type + Animal: + type: object + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + discriminator: + propertyName: $type + mapping: + cat: "#/components/schemas/Cat" + dog: "#/components/schemas/Dog" +`) + +var oneofNoDiscriminatorSpec = []byte(`components: + schemas: + Cat: + type: object + properties: + name: + type: string + scratches: + type: boolean + required: + - name + - scratches + Dog: + type: object + properties: + name: + type: string + barks: + type: boolean + required: + - name + - barks + Animal: + type: object + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" +`) + +func TestVisitJSON_OneOf_MissingDiscriptorProperty(t *testing.T) { + s, err := NewLoader().LoadFromData(oneofSpec) + require.NoError(t, err) + err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + "name": "snoopy", + }) + require.EqualError(t, err, "input does not contain the discriminator property") +} + +func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) { + s, err := NewLoader().LoadFromData(oneofSpec) + require.NoError(t, err) + err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + "name": "snoopy", + "$type": "snake", + }) + require.EqualError(t, err, "input does not contain a valid discriminator value") +} + +func TestVisitJSON_OneOf_MissingField(t *testing.T) { + s, err := NewLoader().LoadFromData(oneofSpec) + require.NoError(t, err) + err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + "name": "snoopy", + "$type": "dog", + }) + require.EqualError(t, err, "Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"$type\": {\n \"enum\": [\n \"dog\"\n ],\n \"type\": \"string\"\n },\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\",\n \"$type\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"$type\": \"dog\",\n \"name\": \"snoopy\"\n }\n") +} + +func TestVisitJSON_OneOf_NoDiscriptor_MissingField(t *testing.T) { + s, err := NewLoader().LoadFromData(oneofNoDiscriminatorSpec) + require.NoError(t, err) + err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + "name": "snoopy", + }) + require.EqualError(t, err, "doesn't match schema due to: Error at \"/scratches\": property \"scratches\" is missing\nSchema:\n {\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"scratches\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"name\",\n \"scratches\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"name\": \"snoopy\"\n }\n Or Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"name\": \"snoopy\"\n }\n") +} diff --git a/routers/legacy/validate_request_test.go b/routers/legacy/validate_request_test.go index 7737f5028..5b9518c78 100644 --- a/routers/legacy/validate_request_test.go +++ b/routers/legacy/validate_request_test.go @@ -107,26 +107,6 @@ func Example() { fmt.Println(err) } // Output: - // request body has an error: doesn't match the schema: Doesn't match schema "oneOf" - // Schema: - // { - // "discriminator": { - // "propertyName": "pet_type" - // }, - // "oneOf": [ - // { - // "$ref": "#/components/schemas/Cat" - // }, - // { - // "$ref": "#/components/schemas/Dog" - // } - // ] - // } - // - // Value: - // { - // "bark": true, - // "breed": "Dingo", - // "pet_type": "Cat" - // } + // request body has an error: doesn't match the schema: input matches more than one oneOf schemas + } From a70f372a1c499058df979028213cc3e710fac156 Mon Sep 17 00:00:00 2001 From: Derek Strickland <1111455+DerekStrickland@users.noreply.github.com> Date: Fri, 27 Aug 2021 05:42:59 -0400 Subject: [PATCH 093/260] Add nomad to list of projects using kin-openapi in README (#413) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f147fe1be..b5417c4ee 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Here's some projects that depend on _kin-openapi_: * [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs" * [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "...a CLI for interacting with REST-ish HTTP APIs with some nice features built-in" * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Goa is a framework for building micro-services and APIs in Go using a unique design-first approach." + * [github.com/hashicorp/nomad-openapi](https://github.com/hashicorp/nomad-openapi) - "Nomad is an easy-to-use, flexible, and performant workload orchestrator that can deploy a mix of microservice, batch, containerized, and non-containerized applications. Nomad is easy to operate and scale and has native Consul and Vault integrations." * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request) ## Alternatives From 34cafeca29422493b3ac294a217edfba0f490cd4 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sat, 28 Aug 2021 06:27:48 -0400 Subject: [PATCH 094/260] Schema customization plug-point (#411) --- openapi3gen/openapi3gen.go | 55 ++++++++++++++++++------- openapi3gen/openapi3gen_test.go | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 14 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 7c321fe7a..c21b782aa 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -21,9 +21,17 @@ func (err *CycleError) Error() string { return "detected cycle" } // Option allows tweaking SchemaRef generation type Option func(*generatorOpt) +// SchemaCustomizerFn is a callback function, allowing +// the OpenAPI schema definition to be updated with additional +// properties during the generation process, based on the +// name of the field, the Go type, and the struct tags. +// name will be "_root" for the top level object, and tag will be "" +type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error + type generatorOpt struct { useAllExportedFields bool throwErrorOnCycle bool + schemaCustomizer SchemaCustomizerFn } // UseAllExportedFields changes the default behavior of only @@ -38,6 +46,12 @@ func ThrowErrorOnCycle() Option { return func(x *generatorOpt) { x.throwErrorOnCycle = true } } +// SchemaCustomizer allows customization of the schema that is generated +// for a field, for example to support an additional tagging scheme +func SchemaCustomizer(sc SchemaCustomizerFn) Option { + return func(x *generatorOpt) { x.schemaCustomizer = sc } +} + // NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef. func NewSchemaRefForValue(value interface{}, opts ...Option) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) { g := NewGenerator(opts...) @@ -73,15 +87,15 @@ func NewGenerator(opts ...Option) *Generator { func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) { //check generatorOpt consistency here - return g.generateSchemaRefFor(nil, t) + return g.generateSchemaRefFor(nil, t, "_root", "") } -func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) { - if ref := g.Types[t]; ref != nil { +func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { + if ref := g.Types[t]; ref != nil && g.opts.schemaCustomizer == nil { g.SchemaRefs[ref]++ return ref, nil } - ref, err := g.generateWithoutSaving(parents, t) + ref, err := g.generateWithoutSaving(parents, t, name, tag) if ref != nil { g.Types[t] = ref g.SchemaRefs[ref]++ @@ -89,7 +103,7 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect return ref, err } -func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) { +func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { typeInfo := jsoninfo.GetTypeInfo(t) for _, parent := range parents { if parent == typeInfo { @@ -110,7 +124,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec _, a := t.FieldByName("Ref") v, b := t.FieldByName("Value") if a && b { - vs, err := g.generateSchemaRefFor(parents, v.Type) + vs, err := g.generateSchemaRefFor(parents, v.Type, name, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { g.SchemaRefs[vs]++ @@ -195,7 +209,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Format = "byte" } else { schema.Type = "array" - items, err := g.generateSchemaRefFor(parents, t.Elem()) + items, err := g.generateSchemaRefFor(parents, t.Elem(), name, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { items = g.generateCycleSchemaRef(t.Elem(), schema) @@ -211,7 +225,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec case reflect.Map: schema.Type = "object" - additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem()) + additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem(), name, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { additionalProperties = g.generateCycleSchemaRef(t.Elem(), schema) @@ -235,11 +249,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec continue } // If asked, try to use yaml tag - name, fType := fieldInfo.JSONName, fieldInfo.Type + fieldName, fType := fieldInfo.JSONName, fieldInfo.Type if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields { // Handle anonymous fields/embedded structs if t.Field(fieldInfo.Index[0]).Anonymous { - ref, err := g.generateSchemaRefFor(parents, fType) + ref, err := g.generateSchemaRefFor(parents, fType, fieldName, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { ref = g.generateCycleSchemaRef(fType, schema) @@ -249,17 +263,24 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } if ref != nil { g.SchemaRefs[ref]++ - schema.WithPropertyRef(name, ref) + schema.WithPropertyRef(fieldName, ref) } } else { ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { - name, fType = tag, ff.Type + fieldName, fType = tag, ff.Type } } } - ref, err := g.generateSchemaRefFor(parents, fType) + // extract the field tag if we have a customizer + var fieldTag reflect.StructTag + if g.opts.schemaCustomizer != nil { + ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) + fieldTag = ff.Tag + } + + ref, err := g.generateSchemaRefFor(parents, fType, fieldName, fieldTag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { ref = g.generateCycleSchemaRef(fType, schema) @@ -269,7 +290,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } if ref != nil { g.SchemaRefs[ref]++ - schema.WithPropertyRef(name, ref) + schema.WithPropertyRef(fieldName, ref) } } @@ -280,6 +301,12 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } } + if g.opts.schemaCustomizer != nil { + if err := g.opts.schemaCustomizer(name, t, tag, schema); err != nil { + return nil, err + } + } + return openapi3.NewSchemaRef(t.Name(), schema), nil } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 0b7fde9e6..7988acf9e 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -1,7 +1,11 @@ package openapi3gen import ( + "encoding/json" + "fmt" "reflect" + "strconv" + "strings" "testing" "github.com/getkin/kin-openapi/openapi3" @@ -144,3 +148,71 @@ func TestCyclicReferences(t *testing.T) { require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) } + +func TestSchemaCustomizer(t *testing.T) { + type Bla struct { + UntaggedStringField string + AnonStruct struct { + InnerFieldWithoutTag int + InnerFieldWithTag int `mymintag:"-1" mymaxtag:"50"` + } + EnumField string `json:"another" myenumtag:"a,b"` + } + + schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { + t.Logf("Field=%s,Tag=%s", name, tag) + if tag.Get("mymintag") != "" { + minVal, _ := strconv.ParseFloat(tag.Get("mymintag"), 64) + schema.Min = &minVal + } + if tag.Get("mymaxtag") != "" { + maxVal, _ := strconv.ParseFloat(tag.Get("mymaxtag"), 64) + schema.Max = &maxVal + } + if tag.Get("myenumtag") != "" { + for _, s := range strings.Split(tag.Get("myenumtag"), ",") { + schema.Enum = append(schema.Enum, s) + } + } + return nil + })) + require.NoError(t, err) + jsonSchema, err := json.MarshalIndent(schemaRef, "", " ") + require.NoError(t, err) + require.JSONEq(t, `{ + "properties": { + "AnonStruct": { + "properties": { + "InnerFieldWithTag": { + "maximum": 50, + "minimum": -1, + "type": "integer" + }, + "InnerFieldWithoutTag": { + "type": "integer" + } + }, + "type": "object" + }, + "UntaggedStringField": { + "type": "string" + }, + "another": { + "enum": [ + "a", + "b" + ], + "type": "string" + } + }, + "type": "object" +}`, string(jsonSchema)) +} + +func TestSchemaCustomizerError(t *testing.T) { + type Bla struct{} + _, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { + return fmt.Errorf("test error") + })) + require.EqualError(t, err, "test error") +} From c50a458f9ef9f22d70bcebcd2c7c396d447af47f Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Mon, 30 Aug 2021 15:20:57 +0300 Subject: [PATCH 095/260] Update README: remove github.com/getkin/kin (#414) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b5417c4ee..92e7fad89 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ The project has received pull requests from many people. Thanks to everyone! Here's some projects that depend on _kin-openapi_: * [https://github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" - * [github.com/getkin/kin](https://github.com/getkin/kin) - "A configurable backend" * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPIv3 spec document * [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs" From de8fc7e6be1f8a6b7b54d7bc111257fb0e4e66b6 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 31 Aug 2021 14:51:06 +0200 Subject: [PATCH 096/260] fix alters by LGTM.com (#415) --- openapi3/loader.go | 12 ++++++------ openapi3/schema.go | 12 ++++-------- openapi3gen/openapi3gen_test.go | 14 ++++++++++---- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/openapi3/loader.go b/openapi3/loader.go index c42d8ba63..0b8d0e1cc 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -780,7 +780,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var scheme SecurityScheme - if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { + if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { return err } component.Value = &scheme @@ -794,7 +794,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil @@ -817,7 +817,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var example Example - if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { + if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { return err } component.Value = &example @@ -831,7 +831,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil @@ -943,7 +943,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var link Link - if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { + if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { return err } component.Value = &link @@ -957,7 +957,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil diff --git a/openapi3/schema.go b/openapi3/schema.go index 6878ad35d..69aee8d7c 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -824,8 +824,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(ref.Ref) } - err := v.visitJSON(settings, value) - if err == nil { + if err := v.visitJSON(settings, value); err == nil { if settings.failfast { return errSchema } @@ -865,8 +864,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val continue } - err := v.visitJSON(settings, value) - if err != nil { + if err := v.visitJSON(settings, value); err != nil { validationErrors = append(validationErrors, err) continue } @@ -910,8 +908,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(item.Ref) } - err := v.visitJSON(settings, value) - if err == nil { + if err := v.visitJSON(settings, value); err == nil { ok = true break } @@ -933,8 +930,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if v == nil { return foundUnresolvedRef(item.Ref) } - err := v.visitJSON(settings, value) - if err != nil { + if err := v.visitJSON(settings, value); err != nil { if settings.failfast { return errSchema } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 7988acf9e..8659f626c 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -2,7 +2,7 @@ package openapi3gen import ( "encoding/json" - "fmt" + "errors" "reflect" "strconv" "strings" @@ -162,11 +162,17 @@ func TestSchemaCustomizer(t *testing.T) { schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { t.Logf("Field=%s,Tag=%s", name, tag) if tag.Get("mymintag") != "" { - minVal, _ := strconv.ParseFloat(tag.Get("mymintag"), 64) + minVal, err := strconv.ParseFloat(tag.Get("mymintag"), 64) + if err != nil { + return err + } schema.Min = &minVal } if tag.Get("mymaxtag") != "" { - maxVal, _ := strconv.ParseFloat(tag.Get("mymaxtag"), 64) + maxVal, err := strconv.ParseFloat(tag.Get("mymaxtag"), 64) + if err != nil { + return err + } schema.Max = &maxVal } if tag.Get("myenumtag") != "" { @@ -212,7 +218,7 @@ func TestSchemaCustomizer(t *testing.T) { func TestSchemaCustomizerError(t *testing.T) { type Bla struct{} _, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { - return fmt.Errorf("test error") + return errors.New("test error") })) require.EqualError(t, err, "test error") } From f58924543e3f7a5e41383a2513933fa27477a02e Mon Sep 17 00:00:00 2001 From: Guilherme Cardoso Date: Tue, 21 Sep 2021 10:51:44 +0100 Subject: [PATCH 097/260] Add support for "application/x-yaml" (#421) --- go.mod | 2 +- openapi3filter/req_resp_decoder.go | 11 +++++++++++ openapi3filter/req_resp_decoder_test.go | 12 ++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f84f470c1..9da0ae552 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,5 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 github.com/gorilla/mux v1.8.0 github.com/stretchr/testify v1.5.1 - gopkg.in/yaml.v2 v2.3.0 // indirect + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 2d2728b24..2dc392864 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "gopkg.in/yaml.v2" "io" "io/ioutil" "mime" @@ -832,6 +833,8 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, func init() { RegisterBodyDecoder("text/plain", plainBodyDecoder) RegisterBodyDecoder("application/json", jsonBodyDecoder) + RegisterBodyDecoder("application/x-yaml", yamlBodyDecoder) + RegisterBodyDecoder("application/yaml", yamlBodyDecoder) RegisterBodyDecoder("application/problem+json", jsonBodyDecoder) RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder) RegisterBodyDecoder("multipart/form-data", multipartBodyDecoder) @@ -854,6 +857,14 @@ func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema return value, nil } +func yamlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + var value interface{} + if err := yaml.NewDecoder(body).Decode(&value); err != nil { + return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} + } + return value, nil +} + func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { // Validate schema of request body. // By the OpenAPI 3 specification request body's schema must have type "object". diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 7ae863b82..34e63712d 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -1035,6 +1035,18 @@ func TestDecodeBody(t *testing.T) { body: strings.NewReader("\"foo\""), want: "foo", }, + { + name: "x-yaml", + mime: "application/x-yaml", + body: strings.NewReader("foo"), + want: "foo", + }, + { + name: "yaml", + mime: "application/yaml", + body: strings.NewReader("foo"), + want: "foo", + }, { name: "urlencoded form", mime: "application/x-www-form-urlencoded", From 9b46ae7fe3f20d6a42482e1ac952137381cc7b1b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sat, 2 Oct 2021 11:54:13 +0100 Subject: [PATCH 098/260] sort out possible mishandling of ipv4 vs v6 (#431) --- openapi3/schema_formats.go | 18 +++++------ openapi3/schema_formats_test.go | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 openapi3/schema_formats_test.go diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 1eb41509e..095bb2228 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "regexp" + "strings" ) const ( @@ -37,24 +38,23 @@ func DefineStringFormatCallback(name string, callback FormatCallback) { SchemaStringFormats[name] = Format{callback: callback} } -func validateIP(ip string) (*net.IP, error) { +func validateIP(ip string) error { parsed := net.ParseIP(ip) if parsed == nil { - return nil, &SchemaError{ + return &SchemaError{ Value: ip, Reason: "Not an IP address", } } - return &parsed, nil + return nil } func validateIPv4(ip string) error { - parsed, err := validateIP(ip) - if err != nil { + if err := validateIP(ip); err != nil { return err } - if parsed.To4() == nil { + if !(strings.Count(ip, ":") < 2) { return &SchemaError{ Value: ip, Reason: "Not an IPv4 address (it's IPv6)", @@ -62,13 +62,13 @@ func validateIPv4(ip string) error { } return nil } + func validateIPv6(ip string) error { - parsed, err := validateIP(ip) - if err != nil { + if err := validateIP(ip); err != nil { return err } - if parsed.To4() != nil { + if !(strings.Count(ip, ":") >= 2) { return &SchemaError{ Value: ip, Reason: "Not an IPv6 address (it's IPv4)", diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go new file mode 100644 index 000000000..14733c8a1 --- /dev/null +++ b/openapi3/schema_formats_test.go @@ -0,0 +1,57 @@ +package openapi3 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue430(t *testing.T) { + schema := NewOneOfSchema( + NewStringSchema().WithFormat("ipv4"), + NewStringSchema().WithFormat("ipv6"), + ) + + err := schema.Validate(context.Background()) + require.NoError(t, err) + + data := map[string]bool{ + "127.0.1.1": true, + + // https://stackoverflow.com/a/48519490/1418165 + + // v4 + "192.168.0.1": true, + // "192.168.0.1:80" doesn't parse per net.ParseIP() + + // v6 + "::FFFF:C0A8:1": false, + "::FFFF:C0A8:0001": false, + "0000:0000:0000:0000:0000:FFFF:C0A8:1": false, + // "::FFFF:C0A8:1%1" doesn't parse per net.ParseIP() + "::FFFF:192.168.0.1": false, + // "[::FFFF:C0A8:1]:80" doesn't parse per net.ParseIP() + // "[::FFFF:C0A8:1%1]:80" doesn't parse per net.ParseIP() + } + + for datum := range data { + err = schema.VisitJSON(datum) + require.Error(t, err, ErrOneOfConflict.Error()) + } + + DefineIPv4Format() + DefineIPv6Format() + + for datum, isV4 := range data { + err = schema.VisitJSON(datum) + require.NoError(t, err) + if isV4 { + require.Nil(t, validateIPv4(datum), "%q should be IPv4", datum) + require.NotNil(t, validateIPv6(datum), "%q should not be IPv6", datum) + } else { + require.NotNil(t, validateIPv4(datum), "%q should not be IPv4", datum) + require.Nil(t, validateIPv6(datum), "%q should be IPv6", datum) + } + } +} From 47bb0b2707dcdd62e32d1645e349da0e0313c100 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 7 Oct 2021 08:01:19 -0400 Subject: [PATCH 099/260] Panic with customizer and embedded structs (#434) --- openapi3gen/openapi3gen.go | 14 +++++++++++-- openapi3gen/openapi3gen_test.go | 36 +++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index c21b782aa..b4ae7b04c 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -103,6 +103,16 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect return ref, err } +func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.StructField { + var ff reflect.StructField + // fieldInfo.Index is an array of indexes starting from the root of the type + for i := 0; i < len(fieldInfo.Index); i++ { + ff = t.Field(fieldInfo.Index[i]) + t = ff.Type + } + return ff +} + func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { typeInfo := jsoninfo.GetTypeInfo(t) for _, parent := range parents { @@ -266,7 +276,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.WithPropertyRef(fieldName, ref) } } else { - ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) + ff := getStructField(t, fieldInfo) if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { fieldName, fType = tag, ff.Type } @@ -276,7 +286,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec // extract the field tag if we have a customizer var fieldTag reflect.StructTag if g.opts.schemaCustomizer != nil { - ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) + ff := getStructField(t, fieldInfo) fieldTag = ff.Tag } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 8659f626c..6d96db98e 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -150,13 +150,23 @@ func TestCyclicReferences(t *testing.T) { } func TestSchemaCustomizer(t *testing.T) { - type Bla struct { + type NestedInnerBla struct { + Enum1Field string `json:"enum1" myenumtag:"a,b"` + } + + type InnerBla struct { UntaggedStringField string AnonStruct struct { InnerFieldWithoutTag int InnerFieldWithTag int `mymintag:"-1" mymaxtag:"50"` + NestedInnerBla } - EnumField string `json:"another" myenumtag:"a,b"` + Enum2Field string `json:"enum2" myenumtag:"c,d"` + } + + type Bla struct { + InnerBla + EnumField3 string `json:"enum3" myenumtag:"e,f"` } schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { @@ -196,17 +206,31 @@ func TestSchemaCustomizer(t *testing.T) { }, "InnerFieldWithoutTag": { "type": "integer" - } + }, + "enum1": { + "enum": [ + "a", + "b" + ], + "type": "string" + } }, "type": "object" }, "UntaggedStringField": { "type": "string" }, - "another": { + "enum2": { + "enum": [ + "c", + "d" + ], + "type": "string" + }, + "enum3": { "enum": [ - "a", - "b" + "e", + "f" ], "type": "string" } From 5a162f64354ef69fb4b52e4343f45bd224bf6f03 Mon Sep 17 00:00:00 2001 From: Bion <520596+bionoren@users.noreply.github.com> Date: Thu, 7 Oct 2021 07:10:06 -0500 Subject: [PATCH 100/260] Fix #422 added support for error unwrapping for errors with a single sub-error (#433) --- openapi3/schema.go | 6 ++++++ openapi3filter/errors.go | 12 ++++++++++++ openapi3filter/req_resp_decoder.go | 9 ++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 69aee8d7c..de3e5167d 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1489,6 +1489,8 @@ type SchemaError struct { Origin error } +var _ interface{ Unwrap() error } = SchemaError{} + func markSchemaErrorKey(err error, key string) error { if v, ok := err.(*SchemaError); ok { v.reversePath = append(v.reversePath, key) @@ -1564,6 +1566,10 @@ func (err *SchemaError) Error() string { return buf.String() } +func (err SchemaError) Unwrap() error { + return err.Origin +} + func isSliceOfUniqueItems(xs []interface{}) bool { s := len(xs) m := make(map[string]struct{}, s) diff --git a/openapi3filter/errors.go b/openapi3filter/errors.go index ec8ea053c..8454c817f 100644 --- a/openapi3filter/errors.go +++ b/openapi3filter/errors.go @@ -17,6 +17,8 @@ type RequestError struct { Err error } +var _ interface{ Unwrap() error } = RequestError{} + func (err *RequestError) Error() string { reason := err.Reason if e := err.Err; e != nil { @@ -35,6 +37,10 @@ func (err *RequestError) Error() string { } } +func (err RequestError) Unwrap() error { + return err.Err +} + var _ error = &ResponseError{} // ResponseError is returned by ValidateResponse when response does not match OpenAPI spec @@ -44,6 +50,8 @@ type ResponseError struct { Err error } +var _ interface{ Unwrap() error } = ResponseError{} + func (err *ResponseError) Error() string { reason := err.Reason if e := err.Err; e != nil { @@ -56,6 +64,10 @@ func (err *ResponseError) Error() string { return reason } +func (err ResponseError) Unwrap() error { + return err.Err +} + var _ error = &SecurityRequirementsError{} // SecurityRequirementsError is returned by ValidateSecurityRequirements diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 2dc392864..cb58b62b6 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "gopkg.in/yaml.v2" "io" "io/ioutil" "mime" @@ -15,6 +14,8 @@ import ( "strconv" "strings" + "gopkg.in/yaml.v2" + "github.com/getkin/kin-openapi/openapi3" ) @@ -42,6 +43,8 @@ type ParseError struct { path []interface{} } +var _ interface{ Unwrap() error } = ParseError{} + func (e *ParseError) Error() string { var msg []string if p := e.Path(); len(p) > 0 { @@ -81,6 +84,10 @@ func (e *ParseError) RootCause() error { return e.Cause } +func (e ParseError) Unwrap() error { + return e.Cause +} + // Path returns a path to the root cause. func (e *ParseError) Path() []interface{} { var path []interface{} From 12311204341890c3056422d2b71945dea7906d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Mar=C3=ADa=20Mart=C3=ADn=20Luque?= Date: Fri, 8 Oct 2021 07:54:13 +0200 Subject: [PATCH 101/260] Do not escape regular expressions again (getkin#429) (#435) --- openapi3/schema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index de3e5167d..5f1ee60b4 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1167,7 +1167,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "pattern", - Reason: fmt.Sprintf("string doesn't match the regular expression %q", schema.Pattern), + Reason: fmt.Sprintf("string doesn't match the regular expression \"%s\"", schema.Pattern), } if !settings.multiError { return err @@ -1182,7 +1182,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value switch { case f.regexp != nil && f.callback == nil: if cp := f.regexp; !cp.MatchString(value) { - formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression %q)", format, cp.String()) + formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression \"%s\")", format, cp.String()) } case f.regexp == nil && f.callback != nil: if err := f.callback(value); err != nil { From d5022c7c2b1bd9148457c553d40d08200b46871d Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 11 Oct 2021 11:44:45 -0700 Subject: [PATCH 102/260] improve response validation error (#437) --- openapi3filter/validate_response.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index b70938e7c..7cb713ace 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -77,7 +77,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error if contentType == nil { return &ResponseError{ Input: input, - Reason: fmt.Sprintf("input header Content-Type has unexpected value: %q", inputMIME), + Reason: fmt.Sprintf("response header Content-Type has unexpected value: %q", inputMIME), } } From 4cb78ee220815dd565d2ea6910fcd89d2895c009 Mon Sep 17 00:00:00 2001 From: Mansur Marvanov Date: Tue, 12 Oct 2021 17:11:20 +0900 Subject: [PATCH 103/260] Define const schema types (#438) --- openapi3/schema.go | 61 ++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 5f1ee60b4..3793ac19d 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -16,6 +16,15 @@ import ( "github.com/go-openapi/jsonpointer" ) +const ( + TypeArray = "array" + TypeBoolean = "boolean" + TypeInteger = "integer" + TypeNumber = "number" + TypeObject = "object" + TypeString = "string" +) + var ( // SchemaErrorDetailsDisabled disables printing of details about schema errors. SchemaErrorDetailsDisabled = false @@ -297,72 +306,72 @@ func NewAllOfSchema(schemas ...*Schema) *Schema { func NewBoolSchema() *Schema { return &Schema{ - Type: "boolean", + Type: TypeBoolean, } } func NewFloat64Schema() *Schema { return &Schema{ - Type: "number", + Type: TypeNumber, } } func NewIntegerSchema() *Schema { return &Schema{ - Type: "integer", + Type: TypeInteger, } } func NewInt32Schema() *Schema { return &Schema{ - Type: "integer", + Type: TypeInteger, Format: "int32", } } func NewInt64Schema() *Schema { return &Schema{ - Type: "integer", + Type: TypeInteger, Format: "int64", } } func NewStringSchema() *Schema { return &Schema{ - Type: "string", + Type: TypeString, } } func NewDateTimeSchema() *Schema { return &Schema{ - Type: "string", + Type: TypeString, Format: "date-time", } } func NewUUIDSchema() *Schema { return &Schema{ - Type: "string", + Type: TypeString, Format: "uuid", } } func NewBytesSchema() *Schema { return &Schema{ - Type: "string", + Type: TypeString, Format: "byte", } } func NewArraySchema() *Schema { return &Schema{ - Type: "array", + Type: TypeArray, } } func NewObjectSchema() *Schema { return &Schema{ - Type: "object", + Type: TypeObject, Properties: make(map[string]*SchemaRef), } } @@ -637,8 +646,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) schemaType := schema.Type switch schemaType { case "": - case "boolean": - case "number": + case TypeBoolean: + case TypeNumber: if format := schema.Format; len(format) > 0 { switch format { case "float", "double": @@ -648,7 +657,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } } - case "integer": + case TypeInteger: if format := schema.Format; len(format) > 0 { switch format { case "int32", "int64": @@ -658,7 +667,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } } - case "string": + case TypeString: if format := schema.Format; len(format) > 0 { switch format { // Supported by OpenAPIv3.0.1: @@ -681,11 +690,11 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) return err } } - case "array": + case TypeArray: if schema.Items == nil { return errors.New("when schema type is 'array', schema 'items' must be non-null") } - case "object": + case TypeObject: default: return fmt.Errorf("unsupported 'type' value %q", schemaType) } @@ -966,8 +975,8 @@ func (schema *Schema) VisitJSONBoolean(value bool) error { } func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) { - if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" { - return schema.expectedType(settings, "boolean") + if schemaType := schema.Type; schemaType != "" && schemaType != TypeBoolean { + return schema.expectedType(settings, TypeBoolean) } return } @@ -996,7 +1005,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value } me = append(me, err) } - } else if schemaType != "" && schemaType != "number" { + } else if schemaType != "" && schemaType != TypeNumber { return schema.expectedType(settings, "number, integer") } @@ -1101,8 +1110,8 @@ func (schema *Schema) VisitJSONString(value string) error { } func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "string" { - return schema.expectedType(settings, "string") + if schemaType := schema.Type; schemaType != "" && schemaType != TypeString { + return schema.expectedType(settings, TypeString) } var me MultiError @@ -1220,8 +1229,8 @@ func (schema *Schema) VisitJSONArray(value []interface{}) error { } func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "array" { - return schema.expectedType(settings, "array") + if schemaType := schema.Type; schemaType != "" && schemaType != TypeArray { + return schema.expectedType(settings, TypeArray) } var me MultiError @@ -1316,8 +1325,8 @@ func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { } func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "object" { - return schema.expectedType(settings, "object") + if schemaType := schema.Type; schemaType != "" && schemaType != TypeObject { + return schema.expectedType(settings, TypeObject) } var me MultiError From e83ebc0a0df8cdfb5b7f45f2fa2e4b0e45c53cd8 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 12 Oct 2021 16:37:38 +0100 Subject: [PATCH 104/260] reproduce issue #436 (#439) --- openapi3filter/issue436_test.go | 135 ++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 openapi3filter/issue436_test.go diff --git a/openapi3filter/issue436_test.go b/openapi3filter/issue436_test.go new file mode 100644 index 000000000..fa106c5a1 --- /dev/null +++ b/openapi3filter/issue436_test.go @@ -0,0 +1,135 @@ +package openapi3filter_test + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func Example_validateMultipartFormData() { + const spec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: 0.0.1 +paths: + /test: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + categories: + type: array + items: + $ref: "#/components/schemas/Category" + responses: + '200': + description: Created + +components: + schemas: + Category: + type: object + properties: + name: + type: string + required: + - name +` + + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + if err != nil { + panic(err) + } + if err = doc.Validate(loader.Context); err != nil { + panic(err) + } + + router, err := gorillamux.NewRouter(doc) + if err != nil { + panic(err) + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + { // Add a single "categories" item as part data + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="categories"`) + h.Set("Content-Type", "application/json") + fw, err := writer.CreatePart(h) + if err != nil { + panic(err) + } + if _, err = io.Copy(fw, strings.NewReader(`{"name": "foo"}`)); err != nil { + panic(err) + } + } + + { // Add a single "categories" item as part data, again + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="categories"`) + h.Set("Content-Type", "application/json") + fw, err := writer.CreatePart(h) + if err != nil { + panic(err) + } + if _, err = io.Copy(fw, strings.NewReader(`{"name": "bar"}`)); err != nil { + panic(err) + } + } + + { // Add file data + fw, err := writer.CreateFormFile("file", "hello.txt") + if err != nil { + panic(err) + } + if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil { + panic(err) + } + } + + writer.Close() + + req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes())) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + route, pathParams, err := router.FindRoute(req) + if err != nil { + panic(err) + } + + if err = openapi3filter.ValidateRequestBody( + context.Background(), + &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + }, + route.Operation.RequestBody.Value, + ); err != nil { + panic(err) + } + // Output: +} From e9b36dac1a2890474ae44cfcf6446c00393527b5 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 21 Oct 2021 16:42:22 +0100 Subject: [PATCH 105/260] Fix scheme handling in v2->v3 conversion (#441) --- openapi2conv/issue440_test.go | 48 ++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 9 +++++- openapi2conv/openapi2_conv_test.go | 10 +++---- openapi2conv/testdata/swagger.json | 1 + openapi3/schema.go | 4 +-- 5 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 openapi2conv/issue440_test.go create mode 120000 openapi2conv/testdata/swagger.json diff --git a/openapi2conv/issue440_test.go b/openapi2conv/issue440_test.go new file mode 100644 index 000000000..24f7a29e9 --- /dev/null +++ b/openapi2conv/issue440_test.go @@ -0,0 +1,48 @@ +package openapi2conv + +import ( + "context" + "encoding/json" + "os" + "testing" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func TestIssue440(t *testing.T) { + doc2file, err := os.Open("testdata/swagger.json") + require.NoError(t, err) + defer doc2file.Close() + var doc2 openapi2.T + err = json.NewDecoder(doc2file).Decode(&doc2) + require.NoError(t, err) + + doc3, err := ToV3(&doc2) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + require.Equal(t, openapi3.Servers{ + {URL: "https://petstore.swagger.io/v2"}, + {URL: "http://petstore.swagger.io/v2"}, + }, doc3.Servers) + + doc2.Host = "your-bot-domain.de" + doc2.Schemes = nil + doc2.BasePath = "" + doc3, err = ToV3(&doc2) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + require.Equal(t, openapi3.Servers{ + {URL: "https://your-bot-domain.de/"}, + }, doc3.Servers) + + doc2.Host = "https://your-bot-domain.de" + doc2.Schemes = nil + doc2.BasePath = "" + doc3, err = ToV3(&doc2) + require.Error(t, err) + require.Contains(t, err.Error(), `invalid host`) +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 6877e88e8..e9f3164c0 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -26,11 +26,18 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { } if host := doc2.Host; host != "" { + if strings.Contains(host, "/") { + err := fmt.Errorf("invalid host %q. This MUST be the host only and does not include the scheme nor sub-paths.", host) + return nil, err + } schemes := doc2.Schemes if len(schemes) == 0 { - schemes = []string{"https://"} + schemes = []string{"https"} } basePath := doc2.BasePath + if basePath == "" { + basePath = "/" + } for _, scheme := range schemes { u := url.URL{ Scheme: scheme, diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index adb9b0814..a8322698f 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -23,9 +23,9 @@ func TestConvOpenAPIV3ToV2(t *testing.T) { require.NoError(t, err) } - spec2, err := FromV3(&doc3) + doc2, err := FromV3(&doc3) require.NoError(t, err) - data, err := json.Marshal(spec2) + data, err := json.Marshal(doc2) require.NoError(t, err) require.JSONEq(t, exampleV2, string(data)) } @@ -35,11 +35,11 @@ func TestConvOpenAPIV2ToV3(t *testing.T) { err := json.Unmarshal([]byte(exampleV2), &doc2) require.NoError(t, err) - spec3, err := ToV3(&doc2) + doc3, err := ToV3(&doc2) require.NoError(t, err) - err = spec3.Validate(context.Background()) + err = doc3.Validate(context.Background()) require.NoError(t, err) - data, err := json.Marshal(spec3) + data, err := json.Marshal(doc3) require.NoError(t, err) require.JSONEq(t, exampleV3, string(data)) } diff --git a/openapi2conv/testdata/swagger.json b/openapi2conv/testdata/swagger.json new file mode 120000 index 000000000..c211aa245 --- /dev/null +++ b/openapi2conv/testdata/swagger.json @@ -0,0 +1 @@ +../../openapi2/testdata/swagger.json \ No newline at end of file diff --git a/openapi3/schema.go b/openapi3/schema.go index 3793ac19d..443f71980 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1176,7 +1176,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "pattern", - Reason: fmt.Sprintf("string doesn't match the regular expression \"%s\"", schema.Pattern), + Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern), } if !settings.multiError { return err @@ -1191,7 +1191,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value switch { case f.regexp != nil && f.callback == nil: if cp := f.regexp; !cp.MatchString(value) { - formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression \"%s\")", format, cp.String()) + formatErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String()) } case f.regexp == nil && f.callback != nil: if err := f.callback(value); err != nil { From 7aa3c9ef96a384ad58c7ef84e9779c7db16a1dcd Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 8 Nov 2021 09:27:45 +0000 Subject: [PATCH 106/260] reproduce + fix #444: ValidateRequest for application/x-yaml (#445) --- openapi3/schema.go | 21 ++++++++---- routers/legacy/issue444_test.go | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 routers/legacy/issue444_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 443f71980..a0796989a 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -800,13 +800,22 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf return schema.visitJSONArray(settings, value) case map[string]interface{}: return schema.visitJSONObject(settings, value) - default: - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "type", - Reason: fmt.Sprintf("unhandled value of type %T", value), + case map[interface{}]interface{}: // for YAML cf. issue #444 + values := make(map[string]interface{}, len(value)) + for key, v := range value { + if k, ok := key.(string); ok { + values[k] = v + } } + if len(value) == len(values) { + return schema.visitJSONObject(settings, values) + } + } + return &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "type", + Reason: fmt.Sprintf("unhandled value of type %T", value), } } diff --git a/routers/legacy/issue444_test.go b/routers/legacy/issue444_test.go new file mode 100644 index 000000000..222ecbba4 --- /dev/null +++ b/routers/legacy/issue444_test.go @@ -0,0 +1,58 @@ +package legacy_test + +import ( + "bytes" + "context" + "net/http/httptest" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" + "github.com/stretchr/testify/require" +) + +func TestIssue444(t *testing.T) { + loader := openapi3.NewLoader() + oas, err := loader.LoadFromData([]byte(` +openapi: '3.0.0' +info: + title: API + version: 1.0.0 +paths: + '/path': + post: + requestBody: + required: true + content: + application/x-yaml: + schema: + type: object + responses: + '200': + description: x + content: + application/json: + schema: + type: string +`)) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(oas) + require.NoError(t, err) + + r := httptest.NewRequest("POST", "/path", bytes.NewReader([]byte(` +foo: bar +`))) + r.Header.Set("Content-Type", "application/x-yaml") + + openapi3.SchemaErrorDetailsDisabled = true + route, pathParams, err := router.FindRoute(r) + require.NoError(t, err) + reqValidationInput := &openapi3filter.RequestValidationInput{ + Request: r, + PathParams: pathParams, + Route: route, + } + err = openapi3filter.ValidateRequest(context.Background(), reqValidationInput) + require.NoError(t, err) +} From 1cb030336fed9b4499c660bb080eff777f2245dc Mon Sep 17 00:00:00 2001 From: jhwz <52683873+jhwz@users.noreply.github.com> Date: Thu, 11 Nov 2021 19:43:53 +1300 Subject: [PATCH 107/260] Internalize references (#443) --- openapi3/internalize_refs.go | 369 ++++++++++++++++++++++++++++++ openapi3/internalize_refs_test.go | 55 +++++ 2 files changed, 424 insertions(+) create mode 100644 openapi3/internalize_refs.go create mode 100644 openapi3/internalize_refs_test.go diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go new file mode 100644 index 000000000..3a993bfb4 --- /dev/null +++ b/openapi3/internalize_refs.go @@ -0,0 +1,369 @@ +package openapi3 + +import ( + "context" + "path/filepath" + "strings" +) + +type RefNameResolver func(string) string + +// DefaultRefResolver is a default implementation of refNameResolver for the +// InternalizeRefs function. +// +// If a reference points to an element inside a document, it returns the last +// element in the reference using filepath.Base. Otherwise if the reference points +// to a file, it returns the file name trimmed of all extensions. +func DefaultRefNameResolver(ref string) string { + if ref == "" { + return "" + } + split := strings.SplitN(ref, "#", 2) + if len(split) == 2 { + return filepath.Base(split[1]) + } + ref = split[0] + for ext := filepath.Ext(ref); len(ext) > 0; ext = filepath.Ext(ref) { + ref = strings.TrimSuffix(ref, ext) + } + return filepath.Base(ref) +} + +func schemaNames(s Schemas) []string { + out := make([]string, 0, len(s)) + for i := range s { + out = append(out, i) + } + return out +} + +func parametersMapNames(s ParametersMap) []string { + out := make([]string, 0, len(s)) + for i := range s { + out = append(out, i) + } + return out +} + +func isExternalRef(ref string) bool { + return ref != "" && !strings.HasPrefix(ref, "#/components/") +} + +func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver) { + if s == nil || !isExternalRef(s.Ref) { + return + } + + name := refNameResolver(s.Ref) + if _, ok := doc.Components.Schemas[name]; ok { + s.Ref = "#/components/schemas/" + name + return + } + + if doc.Components.Schemas == nil { + doc.Components.Schemas = make(Schemas) + } + doc.Components.Schemas[name] = s.Value.NewRef() + s.Ref = "#/components/schemas/" + name +} + +func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolver) { + if p == nil || !isExternalRef(p.Ref) { + return + } + name := refNameResolver(p.Ref) + if _, ok := doc.Components.Parameters[name]; ok { + p.Ref = "#/components/parameters/" + name + return + } + + if doc.Components.Parameters == nil { + doc.Components.Parameters = make(ParametersMap) + } + doc.Components.Parameters[name] = &ParameterRef{Value: p.Value} + p.Ref = "#/components/parameters/" + name +} + +func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver) { + if h == nil || !isExternalRef(h.Ref) { + return + } + name := refNameResolver(h.Ref) + if _, ok := doc.Components.Headers[name]; ok { + h.Ref = "#/components/headers/" + name + return + } + if doc.Components.Headers == nil { + doc.Components.Headers = make(Headers) + } + doc.Components.Headers[name] = &HeaderRef{Value: h.Value} + h.Ref = "#/components/headers/" + name +} + +func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameResolver) { + if r == nil || !isExternalRef(r.Ref) { + return + } + name := refNameResolver(r.Ref) + if _, ok := doc.Components.RequestBodies[name]; ok { + r.Ref = "#/components/requestBodies/" + name + return + } + if doc.Components.RequestBodies == nil { + doc.Components.RequestBodies = make(RequestBodies) + } + doc.Components.RequestBodies[name] = &RequestBodyRef{Value: r.Value} + r.Ref = "#/components/requestBodies/" + name +} + +func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver) { + if r == nil || !isExternalRef(r.Ref) { + return + } + name := refNameResolver(r.Ref) + if _, ok := doc.Components.Responses[name]; ok { + r.Ref = "#/components/responses/" + name + return + } + if doc.Components.Responses == nil { + doc.Components.Responses = make(Responses) + } + doc.Components.Responses[name] = &ResponseRef{Value: r.Value} + r.Ref = "#/components/responses/" + name + +} + +func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver RefNameResolver) { + if ss == nil || !isExternalRef(ss.Ref) { + return + } + name := refNameResolver(ss.Ref) + if _, ok := doc.Components.SecuritySchemes[name]; ok { + ss.Ref = "#/components/securitySchemes/" + name + return + } + if doc.Components.SecuritySchemes == nil { + doc.Components.SecuritySchemes = make(SecuritySchemes) + } + doc.Components.SecuritySchemes[name] = &SecuritySchemeRef{Value: ss.Value} + ss.Ref = "#/components/securitySchemes/" + name + +} + +func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver) { + if e == nil || !isExternalRef(e.Ref) { + return + } + name := refNameResolver(e.Ref) + if _, ok := doc.Components.Examples[name]; ok { + e.Ref = "#/components/examples/" + name + return + } + if doc.Components.Examples == nil { + doc.Components.Examples = make(Examples) + } + doc.Components.Examples[name] = &ExampleRef{Value: e.Value} + e.Ref = "#/components/examples/" + name + +} + +func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver) { + if l == nil || !isExternalRef(l.Ref) { + return + } + name := refNameResolver(l.Ref) + if _, ok := doc.Components.Links[name]; ok { + l.Ref = "#/components/links/" + name + return + } + if doc.Components.Links == nil { + doc.Components.Links = make(Links) + } + doc.Components.Links[name] = &LinkRef{Value: l.Value} + l.Ref = "#/components/links/" + name + +} + +func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver) { + if c == nil || !isExternalRef(c.Ref) { + return + } + name := refNameResolver(c.Ref) + if _, ok := doc.Components.Callbacks[name]; ok { + c.Ref = "#/components/callbacks/" + name + } + if doc.Components.Callbacks == nil { + doc.Components.Callbacks = make(Callbacks) + } + doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value} + c.Ref = "#/components/callbacks/" + name +} + +func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) { + if s == nil { + return + } + + for _, list := range []SchemaRefs{s.AllOf, s.AnyOf, s.OneOf} { + for _, s2 := range list { + doc.addSchemaToSpec(s2, refNameResolver) + if s2 != nil { + doc.derefSchema(s2.Value, refNameResolver) + } + } + } + for _, s2 := range s.Properties { + doc.addSchemaToSpec(s2, refNameResolver) + if s2 != nil { + doc.derefSchema(s2.Value, refNameResolver) + } + } + for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties, s.Items} { + doc.addSchemaToSpec(ref, refNameResolver) + if ref != nil { + doc.derefSchema(ref.Value, refNameResolver) + } + } +} + +func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver) { + for _, h := range hs { + doc.addHeaderToSpec(h, refNameResolver) + doc.derefParameter(h.Value.Parameter, refNameResolver) + } +} + +func (doc *T) derefExamples(es Examples, refNameResolver RefNameResolver) { + for _, e := range es { + doc.addExampleToSpec(e, refNameResolver) + } +} + +func (doc *T) derefContent(c Content, refNameResolver RefNameResolver) { + for _, mediatype := range c { + doc.addSchemaToSpec(mediatype.Schema, refNameResolver) + if mediatype.Schema != nil { + doc.derefSchema(mediatype.Schema.Value, refNameResolver) + } + doc.derefExamples(mediatype.Examples, refNameResolver) + for _, e := range mediatype.Encoding { + doc.derefHeaders(e.Headers, refNameResolver) + } + } +} + +func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver) { + for _, l := range ls { + doc.addLinkToSpec(l, refNameResolver) + } +} + +func (doc *T) derefResponses(es Responses, refNameResolver RefNameResolver) { + for _, e := range es { + doc.addResponseToSpec(e, refNameResolver) + if e.Value != nil { + doc.derefHeaders(e.Value.Headers, refNameResolver) + doc.derefContent(e.Value.Content, refNameResolver) + doc.derefLinks(e.Value.Links, refNameResolver) + } + } +} + +func (doc *T) derefParameter(p Parameter, refNameResolver RefNameResolver) { + doc.addSchemaToSpec(p.Schema, refNameResolver) + doc.derefContent(p.Content, refNameResolver) + if p.Schema != nil { + doc.derefSchema(p.Schema.Value, refNameResolver) + } +} + +func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver) { + doc.derefContent(r.Content, refNameResolver) +} + +func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver) { + for _, ops := range paths { + // inline full operations + ops.Ref = "" + + for _, op := range ops.Operations() { + doc.addRequestBodyToSpec(op.RequestBody, refNameResolver) + if op.RequestBody != nil && op.RequestBody.Value != nil { + doc.derefRequestBody(*op.RequestBody.Value, refNameResolver) + } + for _, cb := range op.Callbacks { + doc.addCallbackToSpec(cb, refNameResolver) + if cb.Value != nil { + doc.derefPaths(*cb.Value, refNameResolver) + } + } + doc.derefResponses(op.Responses, refNameResolver) + for _, param := range op.Parameters { + doc.addParameterToSpec(param, refNameResolver) + if param.Value != nil { + doc.derefParameter(*param.Value, refNameResolver) + } + } + } + } +} + +// InternalizeRefs removes all references to external files from the spec and moves them +// to the components section. +// +// refNameResolver takes in references to returns a name to store the reference under locally. +// It MUST return a unique name for each reference type. +// A default implementation is provided that will suffice for most use cases. See the function +// documention for more details. +// +// Example: +// +// doc.InternalizeRefs(context.Background(), nil) +func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) { + if refNameResolver == nil { + refNameResolver = DefaultRefNameResolver + } + + // Handle components section + names := schemaNames(doc.Components.Schemas) + for _, name := range names { + schema := doc.Components.Schemas[name] + doc.addSchemaToSpec(schema, refNameResolver) + if schema != nil { + schema.Ref = "" // always dereference the top level + doc.derefSchema(schema.Value, refNameResolver) + } + } + names = parametersMapNames(doc.Components.Parameters) + for _, name := range names { + p := doc.Components.Parameters[name] + doc.addParameterToSpec(p, refNameResolver) + if p != nil && p.Value != nil { + p.Ref = "" // always dereference the top level + doc.derefParameter(*p.Value, refNameResolver) + } + } + doc.derefHeaders(doc.Components.Headers, refNameResolver) + for _, req := range doc.Components.RequestBodies { + doc.addRequestBodyToSpec(req, refNameResolver) + if req != nil && req.Value != nil { + req.Ref = "" // always dereference the top level + doc.derefRequestBody(*req.Value, refNameResolver) + } + } + doc.derefResponses(doc.Components.Responses, refNameResolver) + for _, ss := range doc.Components.SecuritySchemes { + doc.addSecuritySchemeToSpec(ss, refNameResolver) + } + doc.derefExamples(doc.Components.Examples, refNameResolver) + doc.derefLinks(doc.Components.Links, refNameResolver) + for _, cb := range doc.Components.Callbacks { + doc.addCallbackToSpec(cb, refNameResolver) + if cb != nil && cb.Value != nil { + cb.Ref = "" // always dereference the top level + doc.derefPaths(*cb.Value, refNameResolver) + } + } + + doc.derefPaths(doc.Paths, refNameResolver) +} diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go new file mode 100644 index 000000000..d6264d428 --- /dev/null +++ b/openapi3/internalize_refs_test.go @@ -0,0 +1,55 @@ +package openapi3 + +import ( + "context" + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInternalizeRefs(t *testing.T) { + var regexpRef = regexp.MustCompile(`"\$ref":`) + var regexpRefInternal = regexp.MustCompile(`"\$ref":"\#`) + + tests := []struct { + filename string + }{ + {"testdata/testref.openapi.yml"}, + {"testdata/recursiveRef/openapi.yml"}, + {"testdata/spec.yaml"}, + {"testdata/callbacks.yml"}, + } + + for _, test := range tests { + t.Run(test.filename, func(t *testing.T) { + // Load in the reference spec from the testdata + sl := NewLoader() + sl.IsExternalRefsAllowed = true + doc, err := sl.LoadFromFile(test.filename) + require.NoError(t, err, "loading test file") + + // Internalize the references + doc.InternalizeRefs(context.Background(), DefaultRefNameResolver) + + // Validate the internalized spec + err = doc.Validate(context.Background()) + require.Nil(t, err, "validating internalized spec") + + data, err := doc.MarshalJSON() + require.NoError(t, err, "marshalling internalized spec") + + // run a static check over the file, making sure each occurence of a + // reference is followed by a # + numRefs := len(regexpRef.FindAll(data, -1)) + numInternalRefs := len(regexpRefInternal.FindAll(data, -1)) + require.Equal(t, numRefs, numInternalRefs, "checking all references are internal") + + // load from data, but with the path set to the current directory + doc2, err := sl.LoadFromData(data) + require.NoError(t, err, "reloading spec") + err = doc2.Validate(context.Background()) + require.Nil(t, err, "validating reloaded spec") + }) + } +} From 41c2da717f780490b4cc26c55261563346f98851 Mon Sep 17 00:00:00 2001 From: Luukvdm Date: Thu, 18 Nov 2021 17:05:13 +0100 Subject: [PATCH 108/260] ClientCredentials conversion to OpenAPI v2 (#449) --- openapi2conv/issue187_test.go | 24 ++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index 7ca85cace..1c113b708 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -167,3 +167,27 @@ paths: err = doc3.Validate(context.Background()) require.NoError(t, err) } + +func TestPR449(t *testing.T) { + spec := ` +swagger: '2.0' +info: + version: 1.0.0 + title: title + +securityDefinitions: + OAuth2Application: + type: "oauth2" + flow: "application" + tokenUrl: "example.com/oauth2/token" +` + doc3, err := v2v3YAML([]byte(spec)) + require.NoError(t, err) + require.NotNil(t, doc3.Components.SecuritySchemes["OAuth2Application"].Value.Flows.ClientCredentials) + _, err = yaml.Marshal(doc3) + require.NoError(t, err) + + doc2, err := FromV3(doc3) + require.NoError(t, err) + require.Equal(t, doc2.SecurityDefinitions["OAuth2Application"].Flow, "application") +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index e9f3164c0..245702622 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -525,6 +525,8 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu flows.AuthorizationCode = flow case "password": flows.Password = flow + case "application": + flows.ClientCredentials = flow default: return nil, fmt.Errorf("unsupported flow %q", securityScheme.Flow) } @@ -1076,6 +1078,8 @@ func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*o result.Flow = "accessCode" } else if flow = flows.Password; flow != nil { result.Flow = "password" + } else if flow = flows.ClientCredentials; flow != nil { + result.Flow = "application" } else { return nil, nil } From 0db2d4c3583875f4fe65b47ccb51662a20ed32a9 Mon Sep 17 00:00:00 2001 From: Andrey Dyatlov Date: Mon, 22 Nov 2021 12:51:01 +0100 Subject: [PATCH 109/260] Fix issue https://github.com/getkin/kin-openapi/issues/410 (#450) Co-authored-by: Pierre Fenoll --- routers/gorillamux/router.go | 14 ++++++++++---- routers/gorillamux/router_test.go | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 31d8dc8fe..eec1f0122 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -35,10 +35,16 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { servers := make([]srv, 0, len(doc.Servers)) for _, server := range doc.Servers { serverURL := server.URL - scheme0 := strings.Split(serverURL, "://")[0] - schemes := permutePart(scheme0, server) - - u, err := url.Parse(bEncode(strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1))) + var schemes []string + var u *url.URL + var err error + if strings.Contains(serverURL, "://") { + scheme0 := strings.Split(serverURL, "://")[0] + schemes = permutePart(scheme0, server) + u, err = url.Parse(bEncode(strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1))) + } else { + u, err = url.Parse(bEncode(serverURL)) + } if err != nil { return nil, err } diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index 8dc0a2eb1..6c660187e 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -219,3 +219,26 @@ func TestServerPath(t *testing.T) { }) require.NoError(t, err) } + +func TestRelativeURL(t *testing.T) { + helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} + doc := &openapi3.T{ + Servers: openapi3.Servers{ + &openapi3.Server{ + URL: "/api/v1", + }, + }, + Paths: openapi3.Paths{ + "/hello": &openapi3.PathItem{ + Get: helloGET, + }, + }, + } + router, err := NewRouter(doc) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, "https://example.com/api/v1/hello", nil) + require.NoError(t, err) + route, _, err := router.FindRoute(req) + require.NoError(t, err) + require.Equal(t, "/hello", route.Path) +} From eb43e20aa5e2998972a18a3936413b128034afd2 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 2 Dec 2021 15:24:03 +0000 Subject: [PATCH 110/260] try reproducing #447 (#448) --- openapi3/loader_recursive_ref_test.go | 36 +++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go index bd6590364..5b7c1506e 100644 --- a/openapi3/loader_recursive_ref_test.go +++ b/openapi3/loader_recursive_ref_test.go @@ -11,7 +11,39 @@ func TestLoaderSupportsRecursiveReference(t *testing.T) { loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml") require.NoError(t, err) - require.NotNil(t, doc) - require.NoError(t, doc.Validate(loader.Context)) + 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) } + +func TestIssue447(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(` +openapi: 3.0.1 +info: + title: Recursive refs example + version: "1.0" +paths: {} +components: + schemas: + Complex: + type: object + properties: + parent: + $ref: '#/components/schemas/Complex' +`)) + require.NoError(t, err) + 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) +} From f7c80fecb8472b694244fb1119e0be9806fa0dee Mon Sep 17 00:00:00 2001 From: Nick Ufer Date: Thu, 2 Dec 2021 16:25:49 +0100 Subject: [PATCH 111/260] fix: duplicate error reason when parameter is required but not present (#453) --- openapi3filter/validate_request.go | 2 +- openapi3filter/validation_error_encoder.go | 2 +- openapi3filter/validation_error_test.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 2f9a5f14c..990b299ef 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -140,7 +140,7 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param // Validate a parameter's value. if value == nil { if parameter.Required { - return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired} + return &RequestError{Input: input, Parameter: parameter, Err: ErrInvalidRequired} } return nil } diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 707b22d4a..205186960 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -75,7 +75,7 @@ func convertBasicRequestError(e *RequestError) *ValidationError { } func convertErrInvalidRequired(e *RequestError) *ValidationError { - if e.Reason == ErrInvalidRequired.Error() && e.Parameter != nil { + if e.Err == ErrInvalidRequired && e.Parameter != nil { return &ValidationError{ Status: http.StatusBadRequest, Title: fmt.Sprintf("parameter %q in %s is required", e.Parameter.Name, e.Parameter.In), diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index d539c3f7e..6eadbd06c 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -180,7 +180,7 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrReason: ErrInvalidRequired.Error(), + wantErrBody: `parameter "status" in query has an error: value is required but missing`, wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "status" in query is required`}, }, @@ -424,7 +424,7 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "petId", wantErrParamIn: "path", - wantErrReason: ErrInvalidRequired.Error(), + wantErrBody: `parameter "petId" in path has an error: value is required but missing`, wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "petId" in path is required`}, }, From f1075be54150633faa1e25d0e9e7325d58228d57 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 2 Dec 2021 15:35:26 +0000 Subject: [PATCH 112/260] Provide support for generating recursive types into OpenAPI doc #451 + my touches (#454) Co-authored-by: Peter Broadhurst --- README.md | 5 + openapi3gen/openapi3gen.go | 47 +++- openapi3gen/openapi3gen_test.go | 419 +++++++++++++++++++++++++------- openapi3gen/simple_test.go | 6 +- 4 files changed, 378 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 92e7fad89..abefc2f48 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,11 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.84.0 +* The prototype of `openapi3gen.NewSchemaRefForValue` changed: + * It no longer returns a map but that is still accessible under the field `(*Generator).SchemaRefs`. + * It now takes in an additional argument (basically `doc.Components.Schemas`) which gets written to so `$ref` cycles can be properly handled. + ### v0.61.0 * Renamed `openapi2.Swagger` to `openapi2.T`. * Renamed `openapi2conv.FromV3Swagger` to `openapi2conv.FromV3`. diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index b4ae7b04c..45577bce0 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -52,14 +52,10 @@ func SchemaCustomizer(sc SchemaCustomizerFn) Option { return func(x *generatorOpt) { x.schemaCustomizer = sc } } -// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef. -func NewSchemaRefForValue(value interface{}, opts ...Option) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) { +// NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...) +func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) { g := NewGenerator(opts...) - ref, err := g.GenerateSchemaRef(reflect.TypeOf(value)) - for ref := range g.SchemaRefs { - ref.Ref = "" - } - return ref, g.SchemaRefs, err + return g.NewSchemaRefForValue(value, schemas) } type Generator struct { @@ -71,6 +67,9 @@ type Generator struct { // If count is 1, it's not ne // An OpenAPI identifier has been assigned to each. SchemaRefs map[*openapi3.SchemaRef]int + + // componentSchemaRefs is a set of schemas that must be defined in the components to avoid cycles + componentSchemaRefs map[string]struct{} } func NewGenerator(opts ...Option) *Generator { @@ -79,9 +78,10 @@ func NewGenerator(opts ...Option) *Generator { f(gOpt) } return &Generator{ - Types: make(map[reflect.Type]*openapi3.SchemaRef), - SchemaRefs: make(map[*openapi3.SchemaRef]int), - opts: *gOpt, + Types: make(map[reflect.Type]*openapi3.SchemaRef), + SchemaRefs: make(map[*openapi3.SchemaRef]int), + componentSchemaRefs: make(map[string]struct{}), + opts: *gOpt, } } @@ -90,17 +90,41 @@ func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, erro return g.generateSchemaRefFor(nil, t, "_root", "") } +// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef, and updates a supplied map with any dependent component schemas if they lead to cycles +func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas) (*openapi3.SchemaRef, error) { + ref, err := g.GenerateSchemaRef(reflect.TypeOf(value)) + if err != nil { + return nil, err + } + for ref := range g.SchemaRefs { + if _, ok := g.componentSchemaRefs[ref.Ref]; ok && schemas != nil { + schemas[ref.Ref] = &openapi3.SchemaRef{ + Value: ref.Value, + } + } + if strings.HasPrefix(ref.Ref, "#/components/schemas/") { + ref.Value = nil + } else { + ref.Ref = "" + } + } + return ref, nil +} + func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { if ref := g.Types[t]; ref != nil && g.opts.schemaCustomizer == nil { g.SchemaRefs[ref]++ return ref, nil } ref, err := g.generateWithoutSaving(parents, t, name, tag) + if err != nil { + return nil, err + } if ref != nil { g.Types[t] = ref g.SchemaRefs[ref]++ } - return ref, err + return ref, nil } func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.StructField { @@ -341,6 +365,7 @@ func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Sche typeName = t.Name() } + g.componentSchemaRefs[typeName] = struct{}{} return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema) } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 6d96db98e..a6d1620e6 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -1,29 +1,177 @@ -package openapi3gen +package openapi3gen_test import ( "encoding/json" "errors" + "fmt" "reflect" "strconv" "strings" "testing" + "time" "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3gen" "github.com/stretchr/testify/require" ) -type CyclicType0 struct { - CyclicField *CyclicType1 `json:"a"` -} -type CyclicType1 struct { - CyclicField *CyclicType0 `json:"b"` +func ExampleGenerator_SchemaRefs() { + type SomeOtherType string + type SomeStruct struct { + Bool bool `json:"bool"` + Int int `json:"int"` + Int64 int64 `json:"int64"` + Float64 float64 `json:"float64"` + String string `json:"string"` + Bytes []byte `json:"bytes"` + JSON json.RawMessage `json:"json"` + Time time.Time `json:"time"` + Slice []SomeOtherType `json:"slice"` + Map map[string]*SomeOtherType `json:"map"` + + Struct struct { + X string `json:"x"` + } `json:"struct"` + + EmptyStruct struct { + Y string + } `json:"structWithoutFields"` + + Ptr *SomeOtherType `json:"ptr"` + } + + g := openapi3gen.NewGenerator() + schemaRef, err := g.NewSchemaRefForValue(&SomeStruct{}, nil) + if err != nil { + panic(err) + } + + fmt.Printf("g.SchemaRefs: %d\n", len(g.SchemaRefs)) + var data []byte + if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemaRef: %s\n", data) + // Output: + // g.SchemaRefs: 15 + // schemaRef: { + // "properties": { + // "bool": { + // "type": "boolean" + // }, + // "bytes": { + // "format": "byte", + // "type": "string" + // }, + // "float64": { + // "format": "double", + // "type": "number" + // }, + // "int": { + // "type": "integer" + // }, + // "int64": { + // "format": "int64", + // "type": "integer" + // }, + // "json": {}, + // "map": { + // "additionalProperties": { + // "type": "string" + // }, + // "type": "object" + // }, + // "ptr": { + // "type": "string" + // }, + // "slice": { + // "items": { + // "type": "string" + // }, + // "type": "array" + // }, + // "string": { + // "type": "string" + // }, + // "struct": { + // "properties": { + // "x": { + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "structWithoutFields": {}, + // "time": { + // "format": "date-time", + // "type": "string" + // } + // }, + // "type": "object" + // } } -func TestCyclic(t *testing.T) { - schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{}, ThrowErrorOnCycle()) - require.IsType(t, &CycleError{}, err) - require.Nil(t, schemaRef) - require.Empty(t, refsMap) +func ExampleThrowErrorOnCycle() { + type CyclicType0 struct { + CyclicField *struct { + CyclicField *CyclicType0 `json:"b"` + } `json:"a"` + } + + schemas := make(openapi3.Schemas) + schemaRef, err := openapi3gen.NewSchemaRefForValue(&CyclicType0{}, schemas, openapi3gen.ThrowErrorOnCycle()) + if schemaRef != nil || err == nil { + panic(`With option ThrowErrorOnCycle, an error is returned when a schema reference cycle is found`) + } + if _, ok := err.(*openapi3gen.CycleError); !ok { + panic(`With option ThrowErrorOnCycle, an error of type CycleError is returned`) + } + if len(schemas) != 0 { + panic(`No references should have been collected at this point`) + } + + if schemaRef, err = openapi3gen.NewSchemaRefForValue(&CyclicType0{}, schemas); err != nil { + panic(err) + } + + var data []byte + if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemaRef: %s\n", data) + if data, err = json.MarshalIndent(schemas, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemas: %s\n", data) + // Output: + // schemaRef: { + // "properties": { + // "a": { + // "properties": { + // "b": { + // "$ref": "#/components/schemas/CyclicType0" + // } + // }, + // "type": "object" + // } + // }, + // "type": "object" + // } + // schemas: { + // "CyclicType0": { + // "properties": { + // "a": { + // "properties": { + // "b": { + // "$ref": "#/components/schemas/CyclicType0" + // } + // }, + // "type": "object" + // } + // }, + // "type": "object" + // } + // } } func TestExportedNonTagged(t *testing.T) { @@ -34,7 +182,7 @@ func TestExportedNonTagged(t *testing.T) { EvenAYaml string `yaml:"even_a_yaml"` } - schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields()) + schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields()) require.NoError(t, err) require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ Type: "object", @@ -45,21 +193,34 @@ func TestExportedNonTagged(t *testing.T) { }}}, schemaRef) } -func TestExportUint(t *testing.T) { +func ExampleUseAllExportedFields() { type UnsignedIntStruct struct { UnsignedInt uint `json:"uint"` } - schemaRef, _, err := NewSchemaRefForValue(&UnsignedIntStruct{}, UseAllExportedFields()) - require.NoError(t, err) - require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ - Type: "object", - Properties: map[string]*openapi3.SchemaRef{ - "uint": {Value: &openapi3.Schema{Type: "integer", Min: &zeroInt}}, - }}}, schemaRef) + schemaRef, err := openapi3gen.NewSchemaRefForValue(&UnsignedIntStruct{}, nil, openapi3gen.UseAllExportedFields()) + if err != nil { + panic(err) + } + + var data []byte + if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemaRef: %s\n", data) + // Output: + // schemaRef: { + // "properties": { + // "uint": { + // "minimum": 0, + // "type": "integer" + // } + // }, + // "type": "object" + // } } -func TestEmbeddedStructs(t *testing.T) { +func ExampleGenerator_GenerateSchemaRef() { type EmbeddedStruct struct { ID string } @@ -76,17 +237,31 @@ func TestEmbeddedStructs(t *testing.T) { }, } - generator := NewGenerator(UseAllExportedFields()) + generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields()) schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) - require.NoError(t, err) - - var ok bool - _, ok = schemaRef.Value.Properties["Name"] - require.Equal(t, true, ok) + if err != nil { + panic(err) + } - _, ok = schemaRef.Value.Properties["ID"] - require.Equal(t, true, ok) + var data []byte + if data, err = json.MarshalIndent(schemaRef.Value.Properties["Name"].Value, "", " "); err != nil { + panic(err) + } + fmt.Printf(`schemaRef.Value.Properties["Name"].Value: %s`, data) + fmt.Println() + if data, err = json.MarshalIndent(schemaRef.Value.Properties["ID"].Value, "", " "); err != nil { + panic(err) + } + fmt.Printf(`schemaRef.Value.Properties["ID"].Value: %s`, data) + fmt.Println() + // Output: + // schemaRef.Value.Properties["Name"].Value: { + // "type": "string" + // } + // schemaRef.Value.Properties["ID"].Value: { + // "type": "string" + // } } func TestEmbeddedPointerStructs(t *testing.T) { @@ -106,7 +281,7 @@ func TestEmbeddedPointerStructs(t *testing.T) { }, } - generator := NewGenerator(UseAllExportedFields()) + generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields()) schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) require.NoError(t, err) @@ -132,7 +307,7 @@ func TestCyclicReferences(t *testing.T) { MapCycle: nil, } - generator := NewGenerator(UseAllExportedFields()) + generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields()) schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) require.NoError(t, err) @@ -149,7 +324,7 @@ func TestCyclicReferences(t *testing.T) { require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) } -func TestSchemaCustomizer(t *testing.T) { +func ExampleSchemaCustomizer() { type NestedInnerBla struct { Enum1Field string `json:"enum1" myenumtag:"a,b"` } @@ -169,8 +344,7 @@ func TestSchemaCustomizer(t *testing.T) { EnumField3 string `json:"enum3" myenumtag:"e,f"` } - schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { - t.Logf("Field=%s,Tag=%s", name, tag) + customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { if tag.Get("mymintag") != "" { minVal, err := strconv.ParseFloat(tag.Get("mymintag"), 64) if err != nil { @@ -191,58 +365,137 @@ func TestSchemaCustomizer(t *testing.T) { } } return nil - })) - require.NoError(t, err) - jsonSchema, err := json.MarshalIndent(schemaRef, "", " ") - require.NoError(t, err) - require.JSONEq(t, `{ - "properties": { - "AnonStruct": { - "properties": { - "InnerFieldWithTag": { - "maximum": 50, - "minimum": -1, - "type": "integer" - }, - "InnerFieldWithoutTag": { - "type": "integer" - }, - "enum1": { - "enum": [ - "a", - "b" - ], - "type": "string" - } - }, - "type": "object" - }, - "UntaggedStringField": { - "type": "string" - }, - "enum2": { - "enum": [ - "c", - "d" - ], - "type": "string" - }, - "enum3": { - "enum": [ - "e", - "f" - ], - "type": "string" - } - }, - "type": "object" -}`, string(jsonSchema)) + }) + + schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer) + if err != nil { + panic(err) + } + + var data []byte + if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemaRef: %s\n", data) + // Output: + // schemaRef: { + // "properties": { + // "AnonStruct": { + // "properties": { + // "InnerFieldWithTag": { + // "maximum": 50, + // "minimum": -1, + // "type": "integer" + // }, + // "InnerFieldWithoutTag": { + // "type": "integer" + // }, + // "enum1": { + // "enum": [ + // "a", + // "b" + // ], + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "UntaggedStringField": { + // "type": "string" + // }, + // "enum2": { + // "enum": [ + // "c", + // "d" + // ], + // "type": "string" + // }, + // "enum3": { + // "enum": [ + // "e", + // "f" + // ], + // "type": "string" + // } + // }, + // "type": "object" + // } } func TestSchemaCustomizerError(t *testing.T) { - type Bla struct{} - _, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { + customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { return errors.New("test error") - })) + }) + + type Bla struct{} + _, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer) require.EqualError(t, err, "test error") } + +func ExampleNewSchemaRefForValue_recursive() { + type RecursiveType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + Components []*RecursiveType `json:"children,omitempty"` + } + + schemas := make(openapi3.Schemas) + schemaRef, err := openapi3gen.NewSchemaRefForValue(&RecursiveType{}, schemas) + if err != nil { + panic(err) + } + + var data []byte + if data, err = json.MarshalIndent(&schemas, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemas: %s\n", data) + if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemaRef: %s\n", data) + // Output: + // schemas: { + // "RecursiveType": { + // "properties": { + // "children": { + // "items": { + // "$ref": "#/components/schemas/RecursiveType" + // }, + // "type": "array" + // }, + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // }, + // "field3": { + // "type": "string" + // } + // }, + // "type": "object" + // } + // } + // schemaRef: { + // "properties": { + // "children": { + // "items": { + // "$ref": "#/components/schemas/RecursiveType" + // }, + // "type": "array" + // }, + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // }, + // "field3": { + // "type": "string" + // } + // }, + // "type": "object" + // } +} diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go index d997e23b2..99e94ae12 100644 --- a/openapi3gen/simple_test.go +++ b/openapi3gen/simple_test.go @@ -36,15 +36,11 @@ type ( ) func Example() { - schemaRef, refsMap, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}) + schemaRef, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}, nil) if err != nil { panic(err) } - if len(refsMap) != 15 { - panic(fmt.Sprintf("unintended len(refsMap) = %d", len(refsMap))) - } - data, err := json.MarshalIndent(schemaRef, "", " ") if err != nil { panic(err) From fd08c7f0b51291689a0530e0040dbfa4730cc590 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 2 Dec 2021 16:32:27 +0000 Subject: [PATCH 113/260] v2Tov3: handle parameter schema refs (#455) Co-authored-by: Vincent Behar --- openapi2conv/openapi2_conv.go | 12 ++++- openapi2conv/openapi2_conv_test.go | 77 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 245702622..04cb18d2e 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -298,6 +298,10 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete required = true } + var schemaRefRef string + if schemaRef := parameter.Schema; schemaRef != nil && schemaRef.Ref != "" { + schemaRefRef = schemaRef.Ref + } result := &openapi3.Parameter{ In: parameter.In, Name: parameter.Name, @@ -322,7 +326,9 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete AllowEmptyValue: parameter.AllowEmptyValue, UniqueItems: parameter.UniqueItems, MultipleOf: parameter.MultipleOf, - }}), + }, + Ref: schemaRefRef, + }), } return &openapi3.ParameterRef{Value: result}, nil, nil, nil } @@ -980,6 +986,10 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components } if schemaRef := parameter.Schema; schemaRef != nil { schemaRef, _ = FromV3SchemaRef(schemaRef, components) + if ref := schemaRef.Ref; ref != "" { + result.Schema = &openapi3.SchemaRef{Ref: FromV3Ref(ref)} + return result, nil + } schema := schemaRef.Value result.Type = schema.Type result.Format = schema.Format diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index a8322698f..f8f287836 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -79,6 +79,14 @@ const exampleV2 = ` "ItemExtension": { "description": "It could be anything.", "type": "boolean" + }, + "foo": { + "description": "foo description", + "enum": [ + "bar", + "baz" + ], + "type": "string" } }, "externalDocs": { @@ -305,6 +313,34 @@ const exampleV2 = ` }, "x-path": "path extension 1", "x-path2": "path extension 2" + }, + "/foo": { + "get": { + "operationId": "getFoo", + "consumes": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "x-originalParamName": "foo", + "in": "body", + "name": "foo", + "schema": { + "$ref": "#/definitions/foo" + } + } + ], + "responses": { + "default": { + "description": "OK", + "schema": { + "$ref": "#/definitions/foo" + } + } + }, + "summary": "get foo" + } } }, "responses": { @@ -420,6 +456,14 @@ const exampleV3 = ` "type": "string", "x-formData-name": "fileUpload2", "x-mimetype": "text/plain" + }, + "foo": { + "description": "foo description", + "enum": [ + "bar", + "baz" + ], + "type": "string" } } }, @@ -646,6 +690,39 @@ const exampleV3 = ` }, "x-path": "path extension 1", "x-path2": "path extension 2" + }, + "/foo": { + "get": { + "operationId": "getFoo", + "requestBody": { + "x-originalParamName": "foo", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/foo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/foo" + } + } + } + }, + "responses": { + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/foo" + } + } + }, + "description": "OK" + } + }, + "summary": "get foo" + } } }, "security": [ From a2254d99982d94a6774ef4cbcdf1ccb03d56bcff Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 3 Dec 2021 09:49:46 +0000 Subject: [PATCH 114/260] nitpicking: use type openapi3.Schemas (#456) --- openapi3/loader_issue212_test.go | 2 +- openapi3/loader_test.go | 10 +++++----- openapi3/schema.go | 6 +++--- openapi3/schema_test.go | 8 ++++---- openapi3filter/req_resp_decoder.go | 3 +-- openapi3filter/validation_error_test.go | 3 ++- routers/legacy/router.go | 5 ++++- 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/openapi3/loader_issue212_test.go b/openapi3/loader_issue212_test.go index 507b37522..252d0d224 100644 --- a/openapi3/loader_issue212_test.go +++ b/openapi3/loader_issue212_test.go @@ -81,7 +81,7 @@ components: expected, err := json.Marshal(&Schema{ Type: "object", Required: []string{"id", "uri"}, - Properties: map[string]*SchemaRef{ + Properties: Schemas{ "id": {Value: &Schema{Type: "string"}}, "uri": {Value: &Schema{Type: "string"}}, }, diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 4bc4ce432..eb293148d 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -227,18 +227,18 @@ paths: require.NotNil(t, doc.Paths["/"].Post.RequestBody.Value.Content.Get("application/json").Examples["test"]) } -func createTestServer(handler http.Handler) *httptest.Server { +func createTestServer(t *testing.T, handler http.Handler) *httptest.Server { ts := httptest.NewUnstartedServer(handler) - l, _ := net.Listen("tcp", addr) + l, err := net.Listen("tcp", addr) + require.NoError(t, err) ts.Listener.Close() ts.Listener = l return ts } func TestLoadFromRemoteURL(t *testing.T) { - fs := http.FileServer(http.Dir("testdata")) - ts := createTestServer(fs) + ts := createTestServer(t, fs) ts.Start() defer ts.Close() @@ -351,7 +351,7 @@ func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) { }`) fs := http.FileServer(http.Dir("testdata")) - ts := createTestServer(fs) + ts := createTestServer(t, fs) ts.Start() defer ts.Close() diff --git a/openapi3/schema.go b/openapi3/schema.go index a0796989a..a15677c66 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -372,7 +372,7 @@ func NewArraySchema() *Schema { func NewObjectSchema() *Schema { return &Schema{ Type: TypeObject, - Properties: make(map[string]*SchemaRef), + Properties: make(Schemas), } } @@ -493,7 +493,7 @@ func (schema *Schema) WithProperty(name string, propertySchema *Schema) *Schema func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema { properties := schema.Properties if properties == nil { - properties = make(map[string]*SchemaRef) + properties = make(Schemas) schema.Properties = properties } properties[name] = ref @@ -501,7 +501,7 @@ func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema { } func (schema *Schema) WithProperties(properties map[string]*Schema) *Schema { - result := make(map[string]*SchemaRef, len(properties)) + result := make(Schemas, len(properties)) for k, v := range properties { result[k] = &SchemaRef{ Value: v, diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index f724f08e2..fdbd5e7bf 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -389,7 +389,7 @@ var schemaExamples = []schemaExample{ UniqueItems: true, Items: (&Schema{ Type: "object", - Properties: map[string]*SchemaRef{ + Properties: Schemas{ "key1": NewFloat64Schema().NewRef(), }, }).NewRef(), @@ -446,7 +446,7 @@ var schemaExamples = []schemaExample{ UniqueItems: true, Items: (&Schema{ Type: "object", - Properties: map[string]*SchemaRef{ + Properties: Schemas{ "key1": (&Schema{ Type: "array", UniqueItems: true, @@ -579,7 +579,7 @@ var schemaExamples = []schemaExample{ UniqueItems: true, Items: (&Schema{ Type: "object", - Properties: map[string]*SchemaRef{ + Properties: Schemas{ "key1": NewFloat64Schema().NewRef(), }, }).NewRef(), @@ -678,7 +678,7 @@ var schemaExamples = []schemaExample{ Schema: &Schema{ Type: "object", MaxProps: Uint64Ptr(2), - Properties: map[string]*SchemaRef{ + Properties: Schemas{ "numberProperty": NewFloat64Schema().NewRef(), }, }, diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index cb58b62b6..12b368384 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -244,8 +244,6 @@ func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationIn } func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (interface{}, error) { - var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) - if len(schema.Value.AllOf) > 0 { var value interface{} var err error @@ -298,6 +296,7 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho } if schema.Value.Type != "" { + var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) switch schema.Value.Type { case "array": decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 6eadbd06c..bdf544210 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -56,7 +56,8 @@ type validationTest struct { } func getValidationTests(t *testing.T) []*validationTest { - badHost, _ := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil) + badHost, err := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil) + require.NoError(t, err) badPath := newPetstoreRequest(t, http.MethodGet, "/watdis", nil) badMethod := newPetstoreRequest(t, http.MethodTrace, "/pet", nil) diff --git a/routers/legacy/router.go b/routers/legacy/router.go index ecaae1348..f1f47d9ed 100644 --- a/routers/legacy/router.go +++ b/routers/legacy/router.go @@ -125,7 +125,10 @@ func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]s } } pathParams = make(map[string]string, 8) - paramNames, _ := server.ParameterNames() + paramNames, err := server.ParameterNames() + if err != nil { + return nil, nil, err + } for i, value := range paramValues { name := paramNames[i] pathParams[name] = value From ceb64e7146601423be1ac3163f285298470cdeed Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 8 Dec 2021 00:25:10 +0000 Subject: [PATCH 115/260] Create FUNDING.yml (#458) --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..c828e2ff3 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [fenollp] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 2c128aba366c81723b47bf026adff25b9f259d9b Mon Sep 17 00:00:00 2001 From: Matteo Pietro Dazzi Date: Sun, 12 Dec 2021 20:04:16 +0100 Subject: [PATCH 116/260] Insert produces field (#461) --- .gitignore | 1 + openapi2/openapi2.go | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index caf95473b..1a3ec37f1 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ .idea /openapi3/load_with_go_embed_test.go +.vscode \ No newline at end of file diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 4d2714d56..0d8c07228 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -17,6 +17,7 @@ type T struct { ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` + Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` Host string `json:"host,omitempty" yaml:"host,omitempty"` BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` From f13ef7f1c13bf20978f006d7002d0ea96c03cc14 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sat, 18 Dec 2021 12:29:09 +0000 Subject: [PATCH 117/260] fix error reason typo (#466) --- openapi3/schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index a15677c66..c1730b6ad 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1078,7 +1078,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "maximum", - Reason: fmt.Sprintf("number must be most %g", *v), + Reason: fmt.Sprintf("number must be at most %g", *v), } if !settings.multiError { return err From 4ce78d8b165088fef9f4ecab2297091407bc12d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20M=C3=B6ller?= <93589605+karl-dau@users.noreply.github.com> Date: Sat, 18 Dec 2021 15:59:20 +0200 Subject: [PATCH 118/260] Update rfc422 regex as per spec: 'case insensitive on input' (#463) --- openapi3/schema_formats.go | 2 +- openapi3/schema_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 095bb2228..8a8a9406d 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -9,7 +9,7 @@ import ( const ( // FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122 - FormatOfStringForUUIDOfRFC4122 = `^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$` + FormatOfStringForUUIDOfRFC4122 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$` ) //FormatCallback custom check on exotic formats diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index fdbd5e7bf..abdfb3491 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -275,6 +275,12 @@ var schemaExamples = []schemaExample{ "dd7d8481-81a3-407f-95f0-a2f1cb382a4b", "dcba3901-2fba-48c1-9db2-00422055804e", "ace8e3be-c254-4c10-8859-1401d9a9d52a", + "DD7D8481-81A3-407F-95F0-A2F1CB382A4B", + "DCBA3901-2FBA-48C1-9DB2-00422055804E", + "ACE8E3BE-C254-4C10-8859-1401D9A9D52A", + "dd7D8481-81A3-407f-95F0-A2F1CB382A4B", + "DCBA3901-2FBA-48C1-9db2-00422055804e", + "ACE8E3BE-c254-4C10-8859-1401D9A9D52A", }, AllInvalid: []interface{}{ nil, @@ -282,6 +288,13 @@ var schemaExamples = []schemaExample{ "4cf3i040-ea14-4daa-b0b5-ea9329473519", "aaf85740-7e27-4b4f-b4554-a03a43b1f5e3", "56f5bff4-z4b6-48e6-a10d-b6cf66a83b04", + "G39840B1-D0EF-446D-E555-48FCCA50A90A", + "4CF3I040-EA14-4DAA-B0B5-EA9329473519", + "AAF85740-7E27-4B4F-B4554-A03A43B1F5E3", + "56F5BFF4-Z4B6-48E6-A10D-B6CF66A83B04", + "4CF3I040-EA14-4Daa-B0B5-EA9329473519", + "AAf85740-7E27-4B4F-B4554-A03A43b1F5E3", + "56F5Bff4-Z4B6-48E6-a10D-B6CF66A83B04", }, }, From e4ff797c4001690eaf6714724aab856fbfd43c7c Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 20 Dec 2021 12:44:21 +0000 Subject: [PATCH 119/260] work around localhost host mismatch with relative server url (#467) Co-authored-by: Chris Rodwell --- .github/workflows/go.yml | 1 + routers/gorillamux/router.go | 6 +- routers/issue356_test.go | 141 +++++++++++++++++++++++++++++++++++ routers/types.go | 7 ++ 4 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 routers/issue356_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4a5b87c21..fc619f030 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -64,6 +64,7 @@ jobs: - if: runner.os == 'Linux' run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + - run: go test -count=10 -v -run TestIssue356 ./routers - run: go test ./... - run: go test -v -run TestRaceyPatternSchema -race ./... env: diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index eec1f0122..cec702a4f 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -17,6 +17,8 @@ import ( "github.com/gorilla/mux" ) +var _ routers.Router = &Router{} + // Router helps link http.Request.s and an OpenAPIv3 spec type Router struct { muxes []*mux.Route @@ -107,10 +109,10 @@ func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string if err := match.MatchErr; err != nil { // What then? } - route := r.routes[i] + route := *r.routes[i] route.Method = req.Method route.Operation = route.Spec.Paths[route.Path].GetOperation(route.Method) - return route, match.Vars, nil + return &route, match.Vars, nil } switch match.MatchErr { case nil: diff --git a/routers/issue356_test.go b/routers/issue356_test.go new file mode 100644 index 000000000..307e52aea --- /dev/null +++ b/routers/issue356_test.go @@ -0,0 +1,141 @@ +package routers_test + +import ( + "context" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/gorillamux" + "github.com/getkin/kin-openapi/routers/legacy" + "github.com/stretchr/testify/require" +) + +func TestIssue356(t *testing.T) { + spec := func(servers string) []byte { + return []byte(` +openapi: 3.0.0 +info: + title: Example + version: '1.0' + description: test +servers: +` + servers + ` +paths: + /test: + post: + responses: + '201': + description: Created + content: + application/json: + schema: {type: object} + requestBody: + content: + application/json: + schema: {type: object} + description: '' + description: Create a test object +`) + } + + for servers, expectError := range map[string]bool{ + ` +- url: http://localhost:3000/base +- url: /base +`: false, + + ` +- url: /base +- url: http://localhost:3000/base +`: false, + + `- url: /base`: false, + + `- url: http://localhost:3000/base`: true, + + ``: true, + } { + loader := &openapi3.Loader{Context: context.Background()} + t.Logf("using servers: %q (%v)", servers, expectError) + doc, err := loader.LoadFromData(spec(servers)) + require.NoError(t, err) + err = doc.Validate(context.Background()) + require.NoError(t, err) + + for i, newRouter := range []func(*openapi3.T) (routers.Router, error){gorillamux.NewRouter, legacy.NewRouter} { + t.Logf("using NewRouter from %s", map[int]string{0: "gorillamux", 1: "legacy"}[i]) + router, err := newRouter(doc) + require.NoError(t, err) + + if true { + t.Logf("using naked newRouter") + httpReq, err := http.NewRequest(http.MethodPost, "/base/test", strings.NewReader(`{}`)) + require.NoError(t, err) + httpReq.Header.Set("Content-Type", "application/json") + + route, pathParams, err := router.FindRoute(httpReq) + if expectError { + require.Error(t, err, routers.ErrPathNotFound) + return + } + require.NoError(t, err) + + requestValidationInput := &openapi3filter.RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } + err = openapi3filter.ValidateRequest(context.Background(), requestValidationInput) + require.NoError(t, err) + } + + if true { + t.Logf("using httptest.NewServer") + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route, pathParams, err := router.FindRoute(r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + requestValidationInput := &openapi3filter.RequestValidationInput{ + Request: r, + PathParams: pathParams, + Route: route, + } + err = openapi3filter.ValidateRequest(r.Context(), requestValidationInput) + require.NoError(t, err) + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{}")) + })) + defer ts.Close() + + req, err := http.NewRequest(http.MethodPost, ts.URL+"/base/test", strings.NewReader(`{}`)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + rep, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer rep.Body.Close() + body, err := ioutil.ReadAll(rep.Body) + require.NoError(t, err) + + if expectError { + require.Equal(t, 500, rep.StatusCode) + require.Equal(t, routers.ErrPathNotFound.Error(), string(body)) + return + } + require.Equal(t, 200, rep.StatusCode) + require.Equal(t, "{}", string(body)) + } + } + } +} diff --git a/routers/types.go b/routers/types.go index 3cdbee32c..93746cfe9 100644 --- a/routers/types.go +++ b/routers/types.go @@ -8,6 +8,13 @@ import ( // Router helps link http.Request.s and an OpenAPIv3 spec type Router interface { + // FindRoute matches an HTTP request with the operation it resolves to. + // Hosts are matched from the OpenAPIv3 servers key. + // + // If you experience ErrPathNotFound and have localhost hosts specified as your servers, + // turning these server URLs as relative (leaving only the path) should resolve this. + // + // See openapi3filter for example uses with request and response validation. FindRoute(req *http.Request) (route *Route, pathParams map[string]string, err error) } From 389b5e2a4ef9397032c0d1f5707c37fcee900c65 Mon Sep 17 00:00:00 2001 From: Casey Marshall Date: Mon, 20 Dec 2021 07:41:46 -0600 Subject: [PATCH 120/260] Add openapi3 validator middleware (#462) --- openapi3filter/middleware.go | 273 ++++++++++++++++ openapi3filter/middleware_test.go | 513 ++++++++++++++++++++++++++++++ 2 files changed, 786 insertions(+) create mode 100644 openapi3filter/middleware.go create mode 100644 openapi3filter/middleware_test.go diff --git a/openapi3filter/middleware.go b/openapi3filter/middleware.go new file mode 100644 index 000000000..3709faf9b --- /dev/null +++ b/openapi3filter/middleware.go @@ -0,0 +1,273 @@ +package openapi3filter + +import ( + "bytes" + "io" + "io/ioutil" + "log" + "net/http" + + "github.com/getkin/kin-openapi/routers" +) + +// Validator provides HTTP request and response validation middleware. +type Validator struct { + router routers.Router + errFunc ErrFunc + logFunc LogFunc + strict bool +} + +// ErrFunc handles errors that may occur during validation. +type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error) + +// LogFunc handles log messages that may occur during validation. +type LogFunc func(message string, err error) + +// ErrCode is used for classification of different types of errors that may +// occur during validation. These may be used to write an appropriate response +// in ErrFunc. +type ErrCode int + +const ( + // ErrCodeOK indicates no error. It is also the default value. + ErrCodeOK = 0 + // ErrCodeCannotFindRoute happens when the validator fails to resolve the + // request to a defined OpenAPI route. + ErrCodeCannotFindRoute = iota + // ErrCodeRequestInvalid happens when the inbound request does not conform + // to the OpenAPI 3 specification. + ErrCodeRequestInvalid = iota + // ErrCodeResponseInvalid happens when the wrapped handler response does + // not conform to the OpenAPI 3 specification. + ErrCodeResponseInvalid = iota +) + +func (e ErrCode) responseText() string { + switch e { + case ErrCodeOK: + return "OK" + case ErrCodeCannotFindRoute: + return "not found" + case ErrCodeRequestInvalid: + return "bad request" + default: + return "server error" + } +} + +// NewValidator returns a new response validation middlware, using the given +// routes from an OpenAPI 3 specification. +func NewValidator(router routers.Router, options ...ValidatorOption) *Validator { + v := &Validator{ + router: router, + errFunc: func(w http.ResponseWriter, status int, code ErrCode, _ error) { + http.Error(w, code.responseText(), status) + }, + logFunc: func(message string, err error) { + log.Printf("%s: %v", message, err) + }, + } + for i := range options { + options[i](v) + } + return v +} + +// ValidatorOption defines an option that may be specified when creating a +// Validator. +type ValidatorOption func(*Validator) + +// OnErr provides a callback that handles writing an HTTP response on a +// validation error. This allows customization of error responses without +// prescribing a particular form. This callback is only called on response +// validator errors in Strict mode. +func OnErr(f ErrFunc) ValidatorOption { + return func(v *Validator) { + v.errFunc = f + } +} + +// OnLog provides a callback that handles logging in the Validator. This allows +// the validator to integrate with a services' existing logging system without +// prescribing a particular one. +func OnLog(f LogFunc) ValidatorOption { + return func(v *Validator) { + v.logFunc = f + } +} + +// Strict, if set, causes an internal server error to be sent if the wrapped +// handler response fails response validation. If not set, the response is sent +// and the error is only logged. +func Strict(strict bool) ValidatorOption { + return func(v *Validator) { + v.strict = strict + } +} + +// Middleware returns an http.Handler which wraps the given handler with +// request and response validation. +func (v *Validator) Middleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route, pathParams, err := v.router.FindRoute(r) + if err != nil { + v.logFunc("validation error: failed to find route for "+r.URL.String(), err) + v.errFunc(w, http.StatusNotFound, ErrCodeCannotFindRoute, err) + return + } + requestValidationInput := &RequestValidationInput{ + Request: r, + PathParams: pathParams, + Route: route, + } + if err = ValidateRequest(r.Context(), requestValidationInput); err != nil { + v.logFunc("invalid request", err) + v.errFunc(w, http.StatusBadRequest, ErrCodeRequestInvalid, err) + return + } + + var wr responseWrapper + if v.strict { + wr = &strictResponseWrapper{w: w} + } else { + wr = newWarnResponseWrapper(w) + } + + h.ServeHTTP(wr, r) + + if err = ValidateResponse(r.Context(), &ResponseValidationInput{ + RequestValidationInput: requestValidationInput, + Status: wr.statusCode(), + Header: wr.Header(), + Body: ioutil.NopCloser(bytes.NewBuffer(wr.bodyContents())), + }); err != nil { + v.logFunc("invalid response", err) + if v.strict { + v.errFunc(w, http.StatusInternalServerError, ErrCodeResponseInvalid, err) + } + return + } + + if err = wr.flushBodyContents(); err != nil { + v.logFunc("failed to write response", err) + } + }) +} + +type responseWrapper interface { + http.ResponseWriter + + // flushBodyContents writes the buffered response to the client, if it has + // not yet been written. + flushBodyContents() error + + // statusCode returns the response status code, 0 if not set yet. + statusCode() int + + // bodyContents returns the buffered + bodyContents() []byte +} + +type warnResponseWrapper struct { + w http.ResponseWriter + headerWritten bool + status int + body bytes.Buffer + tee io.Writer +} + +func newWarnResponseWrapper(w http.ResponseWriter) *warnResponseWrapper { + wr := &warnResponseWrapper{ + w: w, + } + wr.tee = io.MultiWriter(w, &wr.body) + return wr +} + +// Write implements http.ResponseWriter. +func (wr *warnResponseWrapper) Write(b []byte) (int, error) { + if !wr.headerWritten { + wr.WriteHeader(http.StatusOK) + } + return wr.tee.Write(b) +} + +// WriteHeader implements http.ResponseWriter. +func (wr *warnResponseWrapper) WriteHeader(status int) { + if !wr.headerWritten { + // If the header hasn't been written, record the status for response + // validation. + wr.status = status + wr.headerWritten = true + } + wr.w.WriteHeader(wr.status) +} + +// Header implements http.ResponseWriter. +func (wr *warnResponseWrapper) Header() http.Header { + return wr.w.Header() +} + +// Flush implements the optional http.Flusher interface. +func (wr *warnResponseWrapper) Flush() { + // If the wrapped http.ResponseWriter implements optional http.Flusher, + // pass through. + if fl, ok := wr.w.(http.Flusher); ok { + fl.Flush() + } +} + +func (wr *warnResponseWrapper) flushBodyContents() error { + return nil +} + +func (wr *warnResponseWrapper) statusCode() int { + return wr.status +} + +func (wr *warnResponseWrapper) bodyContents() []byte { + return wr.body.Bytes() +} + +type strictResponseWrapper struct { + w http.ResponseWriter + headerWritten bool + status int + body bytes.Buffer +} + +// Write implements http.ResponseWriter. +func (wr *strictResponseWrapper) Write(b []byte) (int, error) { + if !wr.headerWritten { + wr.WriteHeader(http.StatusOK) + } + return wr.body.Write(b) +} + +// WriteHeader implements http.ResponseWriter. +func (wr *strictResponseWrapper) WriteHeader(status int) { + if !wr.headerWritten { + wr.status = status + wr.headerWritten = true + } +} + +// Header implements http.ResponseWriter. +func (wr *strictResponseWrapper) Header() http.Header { + return wr.w.Header() +} + +func (wr *strictResponseWrapper) flushBodyContents() error { + wr.w.WriteHeader(wr.status) + _, err := wr.w.Write(wr.body.Bytes()) + return err +} + +func (wr *strictResponseWrapper) statusCode() int { + return wr.status +} + +func (wr *strictResponseWrapper) bodyContents() []byte { + return wr.body.Bytes() +} diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go new file mode 100644 index 000000000..4d88aaf91 --- /dev/null +++ b/openapi3filter/middleware_test.go @@ -0,0 +1,513 @@ +package openapi3filter_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "path" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +const validatorSpec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: '0.0.0' +paths: + /test: + post: + operationId: newTest + description: create a new test + parameters: + - in: query + name: version + schema: + type: string + required: true + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TestContents' } + responses: + '201': + description: 'created test' + content: + application/json: + schema: { $ref: '#/components/schemas/TestResource' } + '400': { $ref: '#/components/responses/ErrorResponse' } + '500': { $ref: '#/components/responses/ErrorResponse' } + /test/{id}: + get: + operationId: getTest + description: get a test + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: query + name: version + schema: + type: string + required: true + responses: + '200': + description: 'respond with test resource' + content: + application/json: + schema: { $ref: '#/components/schemas/TestResource' } + '400': { $ref: '#/components/responses/ErrorResponse' } + '404': { $ref: '#/components/responses/ErrorResponse' } + '500': { $ref: '#/components/responses/ErrorResponse' } +components: + schemas: + TestContents: + type: object + properties: + name: + type: string + expected: + type: number + actual: + type: number + required: [name, expected, actual] + additionalProperties: false + TestResource: + type: object + properties: + id: + type: string + contents: + { $ref: '#/components/schemas/TestContents' } + required: [id, contents] + additionalProperties: false + Error: + type: object + properties: + code: + type: string + message: + type: string + required: [code, message] + additionalProperties: false + responses: + ErrorResponse: + description: 'an error occurred' + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } +` + +type validatorTestHandler struct { + contentType string + getBody, postBody string + errBody string + errStatusCode int +} + +const validatorOkResponse = `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}}` + +func (h validatorTestHandler) withDefaults() validatorTestHandler { + if h.contentType == "" { + h.contentType = "application/json" + } + if h.getBody == "" { + h.getBody = validatorOkResponse + } + if h.postBody == "" { + h.postBody = validatorOkResponse + } + if h.errBody == "" { + h.errBody = `{"code":"bad","message":"bad things"}` + } + return h +} + +var testUrlRE = regexp.MustCompile(`^/test(/\d+)?$`) + +func (h *validatorTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", h.contentType) + if h.errStatusCode != 0 { + w.WriteHeader(h.errStatusCode) + w.Write([]byte(h.errBody)) + return + } + if !testUrlRE.MatchString(r.URL.Path) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(h.errBody)) + return + } + switch r.Method { + case "GET": + w.WriteHeader(http.StatusOK) + w.Write([]byte(h.getBody)) + case "POST": + w.WriteHeader(http.StatusCreated) + w.Write([]byte(h.postBody)) + default: + http.Error(w, h.errBody, http.StatusMethodNotAllowed) + } +} + +func TestValidator(t *testing.T) { + doc, err := openapi3.NewLoader().LoadFromData([]byte(validatorSpec)) + require.NoError(t, err, "failed to load test fixture spec") + + ctx := context.Background() + err = doc.Validate(ctx) + require.NoError(t, err, "invalid test fixture spec") + + type testRequest struct { + method, path, body, contentType string + } + type testResponse struct { + statusCode int + body string + } + tests := []struct { + name string + handler validatorTestHandler + options []openapi3filter.ValidatorOption + request testRequest + response testResponse + strict bool + }{{ + name: "valid GET", + handler: validatorTestHandler{}.withDefaults(), + request: testRequest{ + method: "GET", + path: "/test/42?version=1", + }, + response: testResponse{ + 200, validatorOkResponse, + }, + strict: true, + }, { + name: "valid POST", + handler: validatorTestHandler{}.withDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10}`, + contentType: "application/json", + }, + response: testResponse{ + 201, validatorOkResponse, + }, + strict: true, + }, { + name: "not found; no GET operation for /test", + handler: validatorTestHandler{}.withDefaults(), + request: testRequest{ + method: "GET", + path: "/test?version=1", + }, + response: testResponse{ + 404, "not found\n", + }, + strict: true, + }, { + name: "not found; no POST operation for /test/42", + handler: validatorTestHandler{}.withDefaults(), + request: testRequest{ + method: "POST", + path: "/test/42?version=1", + }, + response: testResponse{ + 404, "not found\n", + }, + strict: true, + }, { + name: "invalid request; missing version", + handler: validatorTestHandler{}.withDefaults(), + request: testRequest{ + method: "GET", + path: "/test/42", + }, + response: testResponse{ + 400, "bad request\n", + }, + strict: true, + }, { + name: "invalid POST request; wrong property type", + handler: validatorTestHandler{}.withDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": "nine", "actual": "ten"}`, + contentType: "application/json", + }, + response: testResponse{ + 400, "bad request\n", + }, + strict: true, + }, { + name: "invalid POST request; missing property", + handler: validatorTestHandler{}.withDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9}`, + contentType: "application/json", + }, + response: testResponse{ + 400, "bad request\n", + }, + strict: true, + }, { + name: "invalid POST request; extra property", + handler: validatorTestHandler{}.withDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10, "ideal": 8}`, + contentType: "application/json", + }, + response: testResponse{ + 400, "bad request\n", + }, + strict: true, + }, { + name: "valid response; 404 error", + handler: validatorTestHandler{ + contentType: "application/json", + errBody: `{"code": "404", "message": "not found"}`, + errStatusCode: 404, + }.withDefaults(), + request: testRequest{ + method: "GET", + path: "/test/42?version=1", + }, + response: testResponse{ + 404, `{"code": "404", "message": "not found"}`, + }, + strict: true, + }, { + name: "invalid response; invalid error", + handler: validatorTestHandler{ + errBody: `"not found"`, + errStatusCode: 404, + }.withDefaults(), + request: testRequest{ + method: "GET", + path: "/test/42?version=1", + }, + response: testResponse{ + 500, "server error\n", + }, + strict: true, + }, { + name: "invalid POST response; not strict", + handler: validatorTestHandler{ + postBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, + }.withDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10}`, + contentType: "application/json", + }, + response: testResponse{ + statusCode: 201, + body: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, + }, + strict: false, + }} + for i, test := range tests { + t.Logf("test#%d: %s", i, test.name) + t.Run(test.name, func(t *testing.T) { + // Set up a test HTTP server + var h http.Handler + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + })) + defer s.Close() + + // Update the OpenAPI servers section with the test server URL This is + // needed by the router which matches request routes for OpenAPI + // validation. + doc.Servers = []*openapi3.Server{{URL: s.URL}} + err = doc.Validate(ctx) + require.NoError(t, err, "failed to validate with test server") + + // Create the router and validator + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err, "failed to create router") + + // Now wrap the test handler with the validator middlware + v := openapi3filter.NewValidator(router, append(test.options, openapi3filter.Strict(test.strict))...) + h = v.Middleware(&test.handler) + + // Test: make a client request + var requestBody io.Reader + if test.request.body != "" { + requestBody = bytes.NewBufferString(test.request.body) + } + req, err := http.NewRequest(test.request.method, s.URL+test.request.path, requestBody) + require.NoError(t, err, "failed to create request") + + if test.request.contentType != "" { + req.Header.Set("Content-Type", test.request.contentType) + } + resp, err := s.Client().Do(req) + require.NoError(t, err, "request failed") + defer resp.Body.Close() + require.Equalf(t, test.response.statusCode, resp.StatusCode, + "response code expect %d got %d", test.response.statusCode, resp.StatusCode) + + body, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err, "failed to read response body") + require.Equalf(t, test.response.body, string(body), + "response body expect %q got %q", test.response.body, string(body)) + }) + } +} + +func ExampleValidator() { + // OpenAPI specification for a simple service that squares integers, with + // some limitations. + doc, err := openapi3.NewLoader().LoadFromData([]byte(` +info: +openapi: 3.0.0 +info: + title: 'Validator - square example' + version: '0.0.0' +paths: + /square/{x}: + get: + description: square an integer + parameters: + - name: x + in: path + schema: + type: integer + required: true + responses: + '200': + description: squared integer response + content: + "application/json": + schema: + type: object + properties: + result: + type: integer + minimum: 0 + maximum: 1000000 + required: [result] + additionalProperties: false`[1:])) + if err != nil { + panic(err) + } + + // Square service handler sanity checks inputs, but just crashes on invalid + // requests. + squareHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + xParam := path.Base(r.URL.Path) + x, err := strconv.Atoi(xParam) + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + result := map[string]interface{}{"result": x * x} + if x == 42 { + // An easter egg. Unfortunately, the spec does not allow additional properties... + result["comment"] = "the answer to the ulitimate question of life, the universe, and everything" + } + if err = json.NewEncoder(w).Encode(&result); err != nil { + panic(err) + } + }) + + // Start an http server. + var mainHandler http.Handler + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Why are we wrapping the main server handler with a closure here? + // Validation matches request Host: to server URLs in the spec. With an + // httptest.Server, the URL is dynamic and we have to create it first! + // In a real configured service, this is less likely to be needed. + mainHandler.ServeHTTP(w, r) + })) + defer srv.Close() + + // Patch the OpenAPI spec to match the httptest.Server.URL. Only needed + // because the server URL is dynamic here. + doc.Servers = []*openapi3.Server{{URL: srv.URL}} + if err := doc.Validate(context.Background()); err != nil { // Assert our OpenAPI is valid! + panic(err) + } + // This router is used by the validator to match requests with the OpenAPI + // spec. It does not place restrictions on how the wrapped handler routes + // requests; use of gorilla/mux is just a validator implementation detail. + router, err := gorillamux.NewRouter(doc) + if err != nil { + panic(err) + } + // Strict validation will respond HTTP 500 if the service tries to emit a + // response that does not conform to the OpenAPI spec. Very useful for + // testing a service against its spec in development and CI. In production, + // availability may be more important than strictness. + v := openapi3filter.NewValidator(router, openapi3filter.Strict(true), + openapi3filter.OnErr(func(w http.ResponseWriter, status int, code openapi3filter.ErrCode, err error) { + // Customize validation error responses to use JSON + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": status, + "message": http.StatusText(status), + }) + })) + // Now we can finally set the main server handler. + mainHandler = v.Middleware(squareHandler) + + printResp := func(resp *http.Response, err error) { + if err != nil { + panic(err) + } + defer resp.Body.Close() + contents, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + fmt.Println(resp.StatusCode, strings.TrimSpace(string(contents))) + } + // Valid requests to our sum service + printResp(srv.Client().Get(srv.URL + "/square/2")) + printResp(srv.Client().Get(srv.URL + "/square/789")) + // 404 Not found requests - method or path not found + printResp(srv.Client().Post(srv.URL+"/square/2", "application/json", bytes.NewBufferString(`{"result": 5}`))) + printResp(srv.Client().Get(srv.URL + "/sum/2")) + printResp(srv.Client().Get(srv.URL + "/square/circle/4")) // Handler would process this; validation rejects it + printResp(srv.Client().Get(srv.URL + "/square")) + // 400 Bad requests - note they never reach the wrapped square handler (which would panic) + printResp(srv.Client().Get(srv.URL + "/square/five")) + // 500 Invalid responses + printResp(srv.Client().Get(srv.URL + "/square/42")) // Our "easter egg" added a property which is not allowed + printResp(srv.Client().Get(srv.URL + "/square/65536")) // Answer overflows the maximum allowed value (1000000) + // Output: + // 200 {"result":4} + // 200 {"result":622521} + // 404 {"message":"Not Found","status":404} + // 404 {"message":"Not Found","status":404} + // 404 {"message":"Not Found","status":404} + // 404 {"message":"Not Found","status":404} + // 400 {"message":"Bad Request","status":400} + // 500 {"message":"Internal Server Error","status":500} + // 500 {"message":"Internal Server Error","status":500} +} From 2a1c4b13dbb31687bd9d72f5e706d1d700e7e19e Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 20 Dec 2021 13:42:02 +0000 Subject: [PATCH 121/260] document union behaviour of XyzRef.s (#468) --- .github/workflows/go.yml | 1 - openapi3/refs.go | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index fc619f030..4a5b87c21 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -64,7 +64,6 @@ jobs: - if: runner.os == 'Linux' run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - - run: go test -count=10 -v -run TestIssue356 ./routers - run: go test ./... - run: go test -v -run TestRaceyPatternSchema -race ./... env: diff --git a/openapi3/refs.go b/openapi3/refs.go index 4b64035f8..312250929 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -12,6 +12,8 @@ type Ref struct { Ref string `json:"$ref" yaml:"$ref"` } +// CallbackRef represents either a Callback or a $ref to a Callback. +// When serializing and both fields are set, Ref is preferred over Value. type CallbackRef struct { Ref string Value *Callback @@ -43,6 +45,8 @@ func (value CallbackRef) JSONLookup(token string) (interface{}, error) { return ptr, err } +// ExampleRef represents either a Example or a $ref to a Example. +// When serializing and both fields are set, Ref is preferred over Value. type ExampleRef struct { Ref string Value *Example @@ -74,6 +78,8 @@ func (value ExampleRef) JSONLookup(token string) (interface{}, error) { return ptr, err } +// HeaderRef represents either a Header or a $ref to a Header. +// When serializing and both fields are set, Ref is preferred over Value. type HeaderRef struct { Ref string Value *Header @@ -105,6 +111,8 @@ func (value HeaderRef) JSONLookup(token string) (interface{}, error) { return ptr, err } +// LinkRef represents either a Link or a $ref to a Link. +// When serializing and both fields are set, Ref is preferred over Value. type LinkRef struct { Ref string Value *Link @@ -125,6 +133,8 @@ func (value *LinkRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// ParameterRef represents either a Parameter or a $ref to a Parameter. +// When serializing and both fields are set, Ref is preferred over Value. type ParameterRef struct { Ref string Value *Parameter @@ -156,6 +166,8 @@ func (value ParameterRef) JSONLookup(token string) (interface{}, error) { return ptr, err } +// ResponseRef represents either a Response or a $ref to a Response. +// When serializing and both fields are set, Ref is preferred over Value. type ResponseRef struct { Ref string Value *Response @@ -187,6 +199,8 @@ func (value ResponseRef) JSONLookup(token string) (interface{}, error) { return ptr, err } +// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody. +// When serializing and both fields are set, Ref is preferred over Value. type RequestBodyRef struct { Ref string Value *RequestBody @@ -218,6 +232,8 @@ func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { return ptr, err } +// SchemaRef represents either a Schema or a $ref to a Schema. +// When serializing and both fields are set, Ref is preferred over Value. type SchemaRef struct { Ref string Value *Schema @@ -256,6 +272,8 @@ func (value SchemaRef) JSONLookup(token string) (interface{}, error) { return ptr, err } +// SecuritySchemeRef represents either a SecurityScheme or a $ref to a SecurityScheme. +// When serializing and both fields are set, Ref is preferred over Value. type SecuritySchemeRef struct { Ref string Value *SecurityScheme From c95dd68aef43fa9ac8c1f52f169b387d7681626a Mon Sep 17 00:00:00 2001 From: general-kroll-4-life <82620104+general-kroll-4-life@users.noreply.github.com> Date: Wed, 5 Jan 2022 08:56:56 +1100 Subject: [PATCH 122/260] extensible-paths (#470) --- routers/gorillamux/router.go | 4 ++-- routers/gorillamux/router_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index cec702a4f..83bbf829e 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -128,7 +128,7 @@ func orderedPaths(paths map[string]*openapi3.PathItem) []string { // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathsObject // When matching URLs, concrete (non-templated) paths would be matched // before their templated counterparts. - // NOTE: sorting by number of variables ASC then by lexicographical + // NOTE: sorting by number of variables ASC then by descending lexicographical // order seems to be a good heuristic. vars := make(map[int][]string) max := 0 @@ -142,7 +142,7 @@ func orderedPaths(paths map[string]*openapi3.PathItem) []string { ordered := make([]string, 0, len(paths)) for c := 0; c <= max; c++ { if ps, ok := vars[c]; ok { - sort.Strings(ps) + sort.Sort(sort.Reverse(sort.StringSlice(ps))) ordered = append(ordered, ps...) } } diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index 6c660187e..90f5c3dba 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -59,10 +59,10 @@ func TestRouter(t *testing.T) { &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, }, - "/books/{bookid2}.json": &openapi3.PathItem{ + "/books/{bookid}.json": &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ - &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")}, + &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, }, "/partial": &openapi3.PathItem{ @@ -152,7 +152,7 @@ func TestRouter(t *testing.T) { "bookid": "War.and.Peace", }) expect(r, http.MethodPost, "/books/War.and.Peace.json", booksPOST, map[string]string{ - "bookid2": "War.and.Peace", + "bookid": "War.and.Peace", }) expect(r, http.MethodPost, "/partial", nil, nil) From e02b3c094318e90829d8fdd6e3f7ba2d5cac058d Mon Sep 17 00:00:00 2001 From: Andreas Paul Date: Thu, 13 Jan 2022 12:03:34 +0100 Subject: [PATCH 123/260] fix recipe for validating http requests/responses (#474) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index abefc2f48..1a773fb1c 100644 --- a/README.md +++ b/README.md @@ -70,15 +70,16 @@ import ( "log" "net/http" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) func main() { ctx := context.Background() - loader := &openapi3.Loader{Context: ctx} + loader := openapi3.Loader{Context: ctx} doc, _ := loader.LoadFromFile("openapi3_spec.json") - _ := doc.Validate(ctx) + _ = doc.Validate(ctx) router, _ := legacyrouter.NewRouter(doc) httpReq, _ := http.NewRequest(http.MethodGet, "/items", nil) From a99f24ae4cc10440e722c2eacc4282956663c79b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 14 Jan 2022 13:23:50 +0000 Subject: [PATCH 124/260] amend README.md to reflect BodyDecoder type (#475) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a773fb1c..b01c44a36 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ func main() { } } -func xmlBodyDecoder(body []byte) (interface{}, error) { +func xmlBodyDecoder(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn openapi3filter.EncodingFn) (decoded interface{}, err error) { // Decode body to a primitive, []inteface{}, or map[string]interface{}. } ``` From 0846d700650012c02a16668ebd2bf1e77e9a1669 Mon Sep 17 00:00:00 2001 From: Sergey Vilgelm Date: Thu, 3 Feb 2022 11:26:53 -0800 Subject: [PATCH 125/260] openapi2conv: Convert response headers (#483) --- openapi2/openapi2.go | 5 +--- openapi2conv/openapi2_conv.go | 42 ++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv_test.go | 21 +++++++++++++-- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 0d8c07228..de9247f67 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -232,10 +232,7 @@ func (response *Response) UnmarshalJSON(data []byte) error { } type Header struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` + Parameter } func (header *Header) MarshalJSON() ([]byte, error) { diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 04cb18d2e..4177f1f55 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -424,9 +424,29 @@ func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) { if schemaRef := response.Schema; schemaRef != nil { result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef)) } + if headers := response.Headers; len(headers) > 0 { + result.Headers = ToV3Headers(headers) + } return &openapi3.ResponseRef{Value: result}, nil } +func ToV3Headers(defs map[string]*openapi2.Header) openapi3.Headers { + headers := make(openapi3.Headers, len(defs)) + for name, header := range defs { + header.In = "" + header.Name = "" + if ref := header.Ref; ref != "" { + headers[name] = &openapi3.HeaderRef{Ref: ToV3Ref(ref)} + } else { + parameter, _, _, _ := ToV3Parameter(nil, &header.Parameter, nil) + headers[name] = &openapi3.HeaderRef{Value: &openapi3.Header{ + Parameter: *parameter.Value, + }} + } + } + return headers +} + func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef { schemas := make(map[string]*openapi3.SchemaRef, len(defs)) for name, schema := range defs { @@ -654,6 +674,7 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { doc2.SecurityDefinitions = doc2SecuritySchemes } doc2.Security = FromV3SecurityRequirements(doc3.Security) + return doc2, nil } @@ -1048,9 +1069,30 @@ func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) result.Schema, _ = FromV3SchemaRef(ct.Schema, components) } } + if headers := response.Headers; len(headers) > 0 { + var err error + if result.Headers, err = FromV3Headers(headers, components); err != nil { + return nil, err + } + } return result, nil } +func FromV3Headers(defs openapi3.Headers, components *openapi3.Components) (map[string]*openapi2.Header, error) { + headers := make(map[string]*openapi2.Header, len(defs)) + for name, header := range defs { + ref := openapi3.ParameterRef{Ref: header.Ref, Value: &header.Value.Parameter} + parameter, err := FromV3Parameter(&ref, components) + if err != nil { + return nil, err + } + parameter.In = "" + parameter.Name = "" + headers[name] = &openapi2.Header{Parameter: *parameter} + } + return headers, nil +} + func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) { securityScheme := ref.Value if securityScheme == nil { diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index f8f287836..24ead5610 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -5,9 +5,10 @@ import ( "encoding/json" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" - "github.com/stretchr/testify/require" ) func TestConvOpenAPIV3ToV2(t *testing.T) { @@ -179,6 +180,13 @@ const exampleV2 = ` "$ref": "#/definitions/Item" }, "type": "array" + }, + "headers": { + "ETag": { + "description": "The ETag (or entity tag) HTTP response header is an identifier for a specific version of a resource.", + "type": "string", + "maxLength": 64 + } } }, "404": { @@ -543,7 +551,16 @@ const exampleV3 = ` } } }, - "description": "ok" + "description": "ok", + "headers": { + "ETag": { + "description": "The ETag (or entity tag) HTTP response header is an identifier for a specific version of a resource.", + "schema": { + "type": "string", + "maxLength": 64 + } + } + } }, "404": { "description": "404 response" From 124b07e4133487ee2b054d898f2ddd5e3020b3ab Mon Sep 17 00:00:00 2001 From: Clifton Kaznocha Date: Mon, 21 Feb 2022 03:01:40 -0800 Subject: [PATCH 126/260] Fix oauth2 in openapi2conv.FromV3SecurityScheme (#491) --- openapi2conv/openapi2_conv.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 4177f1f55..35cf43d67 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -1124,17 +1124,33 @@ func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*o if flows != nil { var flow *openapi3.OAuthFlow // TODO: Is this the right priority? What if multiple defined? - if flow = flows.Implicit; flow != nil { + switch { + case flows.Implicit != nil: result.Flow = "implicit" - } else if flow = flows.AuthorizationCode; flow != nil { + flow = flows.Implicit + result.AuthorizationURL = flow.AuthorizationURL + + case flows.AuthorizationCode != nil: result.Flow = "accessCode" - } else if flow = flows.Password; flow != nil { + flow = flows.AuthorizationCode + result.AuthorizationURL = flow.AuthorizationURL + result.TokenURL = flow.TokenURL + + case flows.Password != nil: result.Flow = "password" - } else if flow = flows.ClientCredentials; flow != nil { + flow = flows.Password + result.TokenURL = flow.TokenURL + + case flows.ClientCredentials != nil: result.Flow = "application" - } else { + flow = flows.ClientCredentials + result.TokenURL = flow.TokenURL + + default: return nil, nil } + + result.Scopes = make(map[string]string, len(flow.Scopes)) for scope, desc := range flow.Scopes { result.Scopes[scope] = desc } From 7027e1b4eb58c73728c8389cd41a89a210ca4302 Mon Sep 17 00:00:00 2001 From: Vasiliy Tsybenko Date: Mon, 21 Feb 2022 14:17:42 +0300 Subject: [PATCH 127/260] Fix openapi3 validation: path param must be required (#490) --- openapi3/loader_test.go | 1 + openapi3/parameter.go | 4 ++++ openapi3filter/req_resp_decoder_test.go | 1 + openapi3filter/validation_test.go | 7 ++++--- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index eb293148d..384e54d87 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -412,6 +412,7 @@ paths: parameters: - name: id, in: path + required: true schema: type: string responses: diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 2081e4e1d..a0b8ed11f 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -252,6 +252,10 @@ func (value *Parameter) Validate(ctx context.Context) error { return fmt.Errorf("parameter can't have 'in' value %q", value.In) } + if in == ParameterInPath && !value.Required { + return fmt.Errorf("path parameter %q must be required", value.Name) + } + // Validate a parameter's serialization method. sm, err := value.SerializationMethod() if err != nil { diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 34e63712d..f40a7a53e 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -908,6 +908,7 @@ func TestDecodeParameter(t *testing.T) { path := "/test" if tc.path != "" { path += "/{" + tc.param.Name + "}" + tc.param.Required = true } info := &openapi3.Info{ diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index d4d0a12f1..2f7cf80e6 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -62,9 +62,10 @@ func TestFilter(t *testing.T) { Parameters: openapi3.Parameters{ { Value: &openapi3.Parameter{ - In: "path", - Name: "pathArg", - Schema: openapi3.NewStringSchema().WithMaxLength(2).NewRef(), + In: "path", + Name: "pathArg", + Schema: openapi3.NewStringSchema().WithMaxLength(2).NewRef(), + Required: true, }, }, { From ed20aa7f04d6502214034953d68d9f777af9627e Mon Sep 17 00:00:00 2001 From: Anthony Clerc <21290922+Cr4psy@users.noreply.github.com> Date: Mon, 21 Feb 2022 16:22:00 +0100 Subject: [PATCH 128/260] updated date-time string format regexp to fully comply to standard (#493) --- openapi3/schema_formats.go | 2 +- openapi3/schema_issue492_test.go | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 openapi3/schema_issue492_test.go diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 8a8a9406d..29fbd51fb 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -90,7 +90,7 @@ func init() { DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`) // date-time - DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`) + DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`) } diff --git a/openapi3/schema_issue492_test.go b/openapi3/schema_issue492_test.go new file mode 100644 index 000000000..535c82a66 --- /dev/null +++ b/openapi3/schema_issue492_test.go @@ -0,0 +1,41 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue492(t *testing.T) { + spec := []byte(`components: + schemas: + Server: + properties: + time: + $ref: "#/components/schemas/timestamp" + name: + type: string + type: object + timestamp: + type: string + format: date-time +openapi: "3.0.1" +`) + + s, err := NewLoader().LoadFromData(spec) + require.NoError(t, err) + + // verify that the expected format works + err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ + "name": "kin-openapi", + "time": "2001-02-03T04:05:06.789Z", + }) + require.NoError(t, err) + + // verify that the issue is fixed + err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ + "name": "kin-openapi", + "time": "2001-02-03T04:05:06:789Z", + }) + require.EqualError(t, err, "Error at \"/time\": string doesn't match the format \"date-time\" (regular expression \"^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|(\\+|-)[0-9]{2}:[0-9]{2})?$\")\nSchema:\n {\n \"format\": \"date-time\",\n \"type\": \"string\"\n }\n\nValue:\n \"2001-02-03T04:05:06:789Z\"\n") +} From 69874b2ff85e3fc11ff7e2a91d2729f791edd537 Mon Sep 17 00:00:00 2001 From: Clifton Kaznocha Date: Tue, 22 Feb 2022 23:09:01 -0800 Subject: [PATCH 129/260] distinguish form data in fromV3RequestBodies (#494) --- openapi2conv/openapi2_conv.go | 28 ++++----- openapi2conv/openapi2_conv_test.go | 91 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 13 deletions(-) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 35cf43d67..387a05ad0 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -698,27 +698,29 @@ func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, c return } - //Only select one formData or request body for an individual requesstBody as OpenAPI 2 does not support multiples + //Only select one formData or request body for an individual requestBody as OpenAPI 2 does not support multiples if requestBodyRef.Value != nil { for contentType, mediaType := range requestBodyRef.Value.Content { if consumes == nil { consumes = make(map[string]struct{}) } consumes[contentType] = struct{}{} - if formParams := FromV3RequestBodyFormData(mediaType); len(formParams) != 0 { - formParameters = formParams - } else { - paramName := name - if originalName, ok := requestBodyRef.Value.Extensions["x-originalParamName"]; ok { - json.Unmarshal(originalName.(json.RawMessage), ¶mName) - } + if contentType == "application/x-www-form-urlencoded" || contentType == "multipart/form-data" { + formParameters = FromV3RequestBodyFormData(mediaType) + continue + } - var r *openapi2.Parameter - if r, err = FromV3RequestBody(paramName, requestBodyRef, mediaType, components); err != nil { - return - } - bodyOrRefParameters = append(bodyOrRefParameters, r) + paramName := name + if originalName, ok := requestBodyRef.Value.Extensions["x-originalParamName"]; ok { + json.Unmarshal(originalName.(json.RawMessage), ¶mName) } + + var r *openapi2.Parameter + if r, err = FromV3RequestBody(paramName, requestBodyRef, mediaType, components); err != nil { + return + } + + bodyOrRefParameters = append(bodyOrRefParameters, r) } } return diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index 24ead5610..9edce54de 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -31,6 +31,26 @@ func TestConvOpenAPIV3ToV2(t *testing.T) { require.JSONEq(t, exampleV2, string(data)) } +func TestConvOpenAPIV3ToV2WithReqBody(t *testing.T) { + var doc3 openapi3.T + err := json.Unmarshal([]byte(exampleRequestBodyV3), &doc3) + require.NoError(t, err) + { + // Refs need resolving before we can Validate + sl := openapi3.NewLoader() + err = sl.ResolveRefsIn(&doc3, nil) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + } + + doc2, err := FromV3(&doc3) + require.NoError(t, err) + data, err := json.Marshal(doc2) + require.NoError(t, err) + require.JSONEq(t, exampleRequestBodyV2, string(data)) +} + func TestConvOpenAPIV2ToV3(t *testing.T) { var doc2 openapi2.T err := json.Unmarshal([]byte(exampleV2), &doc2) @@ -766,3 +786,74 @@ const exampleV3 = ` "x-root2": "root extension 2" } ` + +const exampleRequestBodyV3 = `{ + "info": { + "description": "Test Spec", + "title": "Test Spec", + "version": "0.0.0" + }, + "components": { + "requestBodies": { + "FooBody": { + "content": { + "application/json": { + "schema": { + "properties": { "message": { "type": "string" } }, + "type": "object" + } + } + }, + "description": "test spec request body.", + "required": true + } + } + }, + "paths": { + "/foo-path": { + "post": { + "requestBody": { "$ref": "#/components/requestBodies/FooBody" }, + "responses": { "202": { "description": "Test spec post." } }, + "summary": "Test spec path" + } + } + }, + "servers": [{ "url": "http://localhost/" }], + "openapi": "3.0.3" +} +` + +const exampleRequestBodyV2 = `{ + "basePath": "/", + "consumes": ["application/json"], + "host": "localhost", + "info": { + "description": "Test Spec", + "title": "Test Spec", + "version": "0.0.0" + }, + "parameters": { + "FooBody": { + "description": "test spec request body.", + "in": "body", + "name": "FooBody", + "required": true, + "schema": { + "properties": { "message": { "type": "string" } }, + "type": "object" + } + } + }, + "paths": { + "/foo-path": { + "post": { + "parameters": [{ "$ref": "#/parameters/FooBody" }], + "responses": { "202": { "description": "Test spec post." } }, + "summary": "Test spec path" + } + } + }, + "schemes": ["http"], + "swagger": "2.0" +} +` From d54840d41c35ccd1e55a27df3ea852eabb3864bf Mon Sep 17 00:00:00 2001 From: Casey Marshall Date: Wed, 23 Feb 2022 01:15:07 -0600 Subject: [PATCH 130/260] feat: cache resolved refs, improve URI reader extensibility (#469) --- openapi3/loader.go | 21 +------ openapi3/loader_uri_reader.go | 104 ++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 openapi3/loader_uri_reader.go diff --git a/openapi3/loader.go b/openapi3/loader.go index 0b8d0e1cc..8af733c3b 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -5,8 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" - "net/http" "net/url" "path" "path/filepath" @@ -31,7 +29,7 @@ type Loader struct { IsExternalRefsAllowed bool // ReadFromURIFunc allows overriding the any file/URL reading func - ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error) + ReadFromURIFunc ReadFromURIFunc Context context.Context @@ -121,22 +119,7 @@ func (loader *Loader) readURL(location *url.URL) ([]byte, error) { if f := loader.ReadFromURIFunc; f != nil { return f(loader, location) } - - if location.Scheme != "" && location.Host != "" { - resp, err := http.Get(location.String()) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode > 399 { - return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode) - } - return ioutil.ReadAll(resp.Body) - } - if location.Scheme != "" || location.Host != "" || location.RawQuery != "" { - return nil, fmt.Errorf("unsupported URI: %q", location.String()) - } - return ioutil.ReadFile(location.Path) + return DefaultReadFromURI(loader, location) } // LoadFromData loads a spec from a byte array diff --git a/openapi3/loader_uri_reader.go b/openapi3/loader_uri_reader.go new file mode 100644 index 000000000..8357a980d --- /dev/null +++ b/openapi3/loader_uri_reader.go @@ -0,0 +1,104 @@ +package openapi3 + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path/filepath" +) + +// ReadFromURIFunc defines a function which reads the contents of a resource +// located at a URI. +type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error) + +// ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a +// given URI. +var ErrURINotSupported = errors.New("unsupported URI") + +// ReadFromURIs returns a ReadFromURIFunc which tries to read a URI using the +// given reader functions, in the same order. If a reader function does not +// support the URI and returns ErrURINotSupported, the next function is checked +// until a match is found, or the URI is not supported by any. +func ReadFromURIs(readers ...ReadFromURIFunc) ReadFromURIFunc { + return func(loader *Loader, url *url.URL) ([]byte, error) { + for i := range readers { + buf, err := readers[i](loader, url) + if err == ErrURINotSupported { + continue + } else if err != nil { + return nil, err + } + return buf, nil + } + return nil, ErrURINotSupported + } +} + +// DefaultReadFromURI returns a caching ReadFromURIFunc which can read remote +// HTTP URIs and local file URIs. +var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile)) + +// ReadFromHTTP returns a ReadFromURIFunc which uses the given http.Client to +// read the contents from a remote HTTP URI. This client may be customized to +// implement timeouts, RFC 7234 caching, etc. +func ReadFromHTTP(cl *http.Client) ReadFromURIFunc { + return func(loader *Loader, location *url.URL) ([]byte, error) { + if location.Scheme == "" || location.Host == "" { + return nil, ErrURINotSupported + } + req, err := http.NewRequest("GET", location.String(), nil) + if err != nil { + return nil, err + } + resp, err := cl.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode > 399 { + return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode) + } + return ioutil.ReadAll(resp.Body) + } +} + +// ReadFromFile is a ReadFromURIFunc which reads local file URIs. +func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) { + if location.Host != "" { + return nil, ErrURINotSupported + } + if location.Scheme != "" && location.Scheme != "file" { + return nil, ErrURINotSupported + } + return ioutil.ReadFile(location.Path) +} + +// URIMapCache returns a ReadFromURIFunc that caches the contents read from URI +// locations in a simple map. This cache implementation is suitable for +// short-lived processes such as command-line tools which process OpenAPI +// documents. +func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc { + cache := map[string][]byte{} + return func(loader *Loader, location *url.URL) (buf []byte, err error) { + if location.Scheme == "" || location.Scheme == "file" { + if !filepath.IsAbs(location.Path) { + // Do not cache relative file paths; this can cause trouble if + // the current working directory changes when processing + // multiple top-level documents. + return reader(loader, location) + } + } + uri := location.String() + var ok bool + if buf, ok = cache[uri]; ok { + return + } + if buf, err = reader(loader, location); err != nil { + return + } + cache[uri] = buf + return + } +} From 1f72b3725d4d84fa7ce20bb632126091e8124a50 Mon Sep 17 00:00:00 2001 From: Vasiliy Tsybenko Date: Sun, 27 Feb 2022 14:21:44 +0300 Subject: [PATCH 131/260] Fix OpenAPI 3 validation: request body content is required (#498) --- openapi3/openapi3_test.go | 5 ++++- openapi3/request_body.go | 10 ++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 38d488d4e..b2173f0ee 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -123,6 +123,7 @@ components: requestBodies: someRequestBody: description: Some request body + content: {} responses: someResponse: description: Some response @@ -194,7 +195,8 @@ var specJSON = []byte(` }, "requestBodies": { "someRequestBody": { - "description": "Some request body" + "description": "Some request body", + "content": {} } }, "responses": { @@ -253,6 +255,7 @@ func spec() *T { } requestBody := &RequestBody{ Description: "Some request body", + Content: NewContent(), } responseDescription := "Some response" response := &Response{ diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 66b512fa0..b0ae83119 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -29,7 +29,7 @@ type RequestBody struct { ExtensionProps Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Content Content `json:"content" yaml:"content"` } func NewRequestBody() *RequestBody { @@ -98,10 +98,8 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { } func (value *RequestBody) Validate(ctx context.Context) error { - if v := value.Content; v != nil { - if err := v.Validate(ctx); err != nil { - return err - } + if value.Content == nil { + return fmt.Errorf("content of the request body is required") } - return nil + return value.Content.Validate(ctx) } From 5352767564ece15c85c3d0a1ea1143d1f4206b04 Mon Sep 17 00:00:00 2001 From: Vasiliy Tsybenko Date: Sun, 27 Feb 2022 14:25:15 +0300 Subject: [PATCH 132/260] Add OpenAPI 3 externalDocs validation (#497) --- openapi3/external_docs.go | 14 +++++++ openapi3/external_docs_test.go | 42 +++++++++++++++++++ openapi3/openapi3.go | 18 ++++++++ openapi3/openapi3_test.go | 76 +++++++++++++++++++++++++++------- openapi3/operation.go | 6 +++ openapi3/schema.go | 6 +++ openapi3/tag.go | 25 ++++++++++- 7 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 openapi3/external_docs_test.go diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index 5a1476bde..e57f18aa5 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -1,6 +1,10 @@ package openapi3 import ( + "context" + "fmt" + "net/url" + "github.com/getkin/kin-openapi/jsoninfo" ) @@ -19,3 +23,13 @@ func (e *ExternalDocs) MarshalJSON() ([]byte, error) { func (e *ExternalDocs) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, e) } + +func (e *ExternalDocs) Validate(ctx context.Context) error { + if e.URL == "" { + return fmt.Errorf("url is required") + } + if _, err := url.Parse(e.URL); err != nil { + return fmt.Errorf("url is incorrect: %w", err) + } + return nil +} diff --git a/openapi3/external_docs_test.go b/openapi3/external_docs_test.go new file mode 100644 index 000000000..f2fb64f2e --- /dev/null +++ b/openapi3/external_docs_test.go @@ -0,0 +1,42 @@ +package openapi3 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExternalDocs_Validate(t *testing.T) { + tests := []struct { + name string + extDocs *ExternalDocs + expectedErr string + }{ + { + name: "url is missing", + extDocs: &ExternalDocs{}, + expectedErr: "url is required", + }, + { + name: "url is incorrect", + extDocs: &ExternalDocs{URL: "ht tps://example.com"}, + expectedErr: `url is incorrect: parse "ht tps://example.com": first path segment in URL cannot contain colon`, + }, + { + name: "ok", + extDocs: &ExternalDocs{URL: "https://example.com"}, + }, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + err := tt.extDocs.Validate(context.Background()) + if tt.expectedErr != "" { + require.EqualError(t, err, tt.expectedErr) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index ee6887727..f6c0033ce 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -101,5 +101,23 @@ func (value *T) Validate(ctx context.Context) error { } } + { + wrap := func(e error) error { return fmt.Errorf("invalid tags: %w", e) } + if v := value.Tags; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) + } + } + } + + { + wrap := func(e error) error { return fmt.Errorf("invalid external docs: %w", e) } + if v := value.ExternalDocs; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) + } + } + } + return nil } diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index b2173f0ee..69a2f959b 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -354,6 +354,9 @@ func spec() *T { } func TestValidation(t *testing.T) { + version := ` +openapi: 3.0.2 +` info := ` info: title: "Hello World REST APIs" @@ -381,9 +384,17 @@ paths: 200: description: "Get a single greeting object" ` - spec := ` -openapi: 3.0.2 -` + info + paths + ` + externalDocs := ` +externalDocs: + url: https://root-ext-docs.com +` + tags := ` +tags: + - name: "pet" + externalDocs: + url: https://tags-ext-docs.com +` + spec := version + info + paths + externalDocs + tags + ` components: schemas: GreetingObject: @@ -399,23 +410,58 @@ components: type: string ` - tests := map[string]string{ - spec: "", - strings.Replace(spec, `openapi: 3.0.2`, ``, 1): "value of openapi must be a non-empty string", - strings.Replace(spec, `openapi: 3.0.2`, `openapi: ''`, 1): "value of openapi must be a non-empty string", - strings.Replace(spec, info, ``, 1): "invalid info: must be an object", - strings.Replace(spec, paths, ``, 1): "invalid paths: must be an object", + tests := []struct { + name string + spec string + expectedErr string + }{ + { + name: "no errors", + spec: spec, + }, + { + name: "version is missing", + spec: strings.Replace(spec, version, "", 1), + expectedErr: "value of openapi must be a non-empty string", + }, + { + name: "version is empty string", + spec: strings.Replace(spec, version, "openapi: ''", 1), + expectedErr: "value of openapi must be a non-empty string", + }, + { + name: "info section is missing", + spec: strings.Replace(spec, info, ``, 1), + expectedErr: "invalid info: must be an object", + }, + { + name: "paths section is missing", + spec: strings.Replace(spec, paths, ``, 1), + expectedErr: "invalid paths: must be an object", + }, + { + name: "externalDocs section is invalid", + spec: strings.Replace(spec, externalDocs, + strings.ReplaceAll(externalDocs, "url: https://root-ext-docs.com", "url: ''"), 1), + expectedErr: "invalid external docs: url is required", + }, + { + name: "tags section is invalid", + spec: strings.Replace(spec, tags, + strings.ReplaceAll(tags, "url: https://tags-ext-docs.com", "url: ''"), 1), + expectedErr: "invalid tags: invalid external docs: url is required", + }, } - - for spec, expectedErr := range tests { - t.Run(expectedErr, func(t *testing.T) { + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { doc := &T{} - err := yaml.Unmarshal([]byte(spec), &doc) + err := yaml.Unmarshal([]byte(tt.spec), &doc) require.NoError(t, err) err = doc.Validate(context.Background()) - if expectedErr != "" { - require.EqualError(t, err, expectedErr) + if tt.expectedErr != "" { + require.EqualError(t, err, tt.expectedErr) } else { require.NoError(t, err) } diff --git a/openapi3/operation.go b/openapi3/operation.go index 0de7c421a..d842719cb 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -3,6 +3,7 @@ package openapi3 import ( "context" "errors" + "fmt" "strconv" "github.com/getkin/kin-openapi/jsoninfo" @@ -138,5 +139,10 @@ func (value *Operation) Validate(ctx context.Context) error { } else { return errors.New("value of responses must be an object") } + if v := value.ExternalDocs; v != nil { + if err := v.Validate(ctx); err != nil { + return fmt.Errorf("invalid external docs: %w", err) + } + } return nil } diff --git a/openapi3/schema.go b/openapi3/schema.go index c1730b6ad..a78e45541 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -729,6 +729,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } + if v := schema.ExternalDocs; v != nil { + if err = v.Validate(ctx); err != nil { + return fmt.Errorf("invalid external docs: %w", err) + } + } + return } diff --git a/openapi3/tag.go b/openapi3/tag.go index 210b69248..de2f48793 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -1,6 +1,11 @@ package openapi3 -import "github.com/getkin/kin-openapi/jsoninfo" +import ( + "context" + "fmt" + + "github.com/getkin/kin-openapi/jsoninfo" +) // Tags is specified by OpenAPI/Swagger 3.0 standard. type Tags []*Tag @@ -14,6 +19,15 @@ func (tags Tags) Get(name string) *Tag { return nil } +func (tags Tags) Validate(ctx context.Context) error { + for _, v := range tags { + if err := v.Validate(ctx); err != nil { + return err + } + } + return nil +} + // Tag is specified by OpenAPI/Swagger 3.0 standard. type Tag struct { ExtensionProps @@ -29,3 +43,12 @@ func (t *Tag) MarshalJSON() ([]byte, error) { func (t *Tag) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, t) } + +func (t *Tag) Validate(ctx context.Context) error { + if v := t.ExternalDocs; v != nil { + if err := v.Validate(ctx); err != nil { + return fmt.Errorf("invalid external docs: %w", err) + } + } + return nil +} From dfd16a7b08e1999c7e373392e2e72f6478ca8952 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Wed, 2 Mar 2022 03:55:07 -0500 Subject: [PATCH 133/260] issue/500 (#501) Co-authored-by: Nathaniel J Cochran --- openapi3gen/openapi3gen.go | 3 +++ openapi3gen/openapi3gen_test.go | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 45577bce0..f2a45c4e0 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -133,6 +133,9 @@ func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.Struct for i := 0; i < len(fieldInfo.Index); i++ { ff = t.Field(fieldInfo.Index[i]) t = ff.Type + for t.Kind() == reflect.Ptr { + t = t.Elem() + } } return ff } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index a6d1620e6..3640d4c0f 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -294,6 +294,42 @@ func TestEmbeddedPointerStructs(t *testing.T) { require.Equal(t, true, ok) } +// See: https://github.com/getkin/kin-openapi/issues/500 +func TestEmbeddedPointerStructsWithSchemaCustomizer(t *testing.T) { + type EmbeddedStruct struct { + ID string + } + + type ContainerStruct struct { + Name string + *EmbeddedStruct + } + + instance := &ContainerStruct{ + Name: "Container", + EmbeddedStruct: &EmbeddedStruct{ + ID: "Embedded", + }, + } + + customizerFn := func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { + return nil + } + customizerOpt := openapi3gen.SchemaCustomizer(customizerFn) + + generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields(), customizerOpt) + + schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) + require.NoError(t, err) + + var ok bool + _, ok = schemaRef.Value.Properties["Name"] + require.Equal(t, true, ok) + + _, ok = schemaRef.Value.Properties["ID"] + require.Equal(t, true, ok) +} + func TestCyclicReferences(t *testing.T) { type ObjectDiff struct { FieldCycle *ObjectDiff From 1013da349382b4fe9be17236c140347fd8139c62 Mon Sep 17 00:00:00 2001 From: Vasiliy Tsybenko Date: Mon, 7 Mar 2022 10:22:30 +0300 Subject: [PATCH 134/260] Fix OpenAPI 3 validation: operationId must be unique (#504) --- openapi3/paths.go | 29 +++++++++ openapi3/paths_test.go | 84 ++++++++++++++++++++++++--- openapi3filter/fixtures/petstore.json | 2 +- 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/openapi3/paths.go b/openapi3/paths.go index bdb87ae7d..ca2209e2b 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -82,6 +82,11 @@ func (value Paths) Validate(ctx context.Context) error { return err } } + + if err := value.validateUniqueOperationIDs(); err != nil { + return err + } + return nil } @@ -114,6 +119,30 @@ func (paths Paths) Find(key string) *PathItem { return nil } +func (value Paths) validateUniqueOperationIDs() error { + operationIDs := make(map[string]string) + for urlPath, pathItem := range value { + if pathItem == nil { + continue + } + for httpMethod, operation := range pathItem.Operations() { + if operation == nil || operation.OperationID == "" { + continue + } + endpoint := httpMethod + " " + urlPath + if endpointDup, ok := operationIDs[operation.OperationID]; ok { + if endpoint > endpointDup { // For make error message a bit more deterministic. May be useful for tests. + endpoint, endpointDup = endpointDup, endpoint + } + return fmt.Errorf("operations %q and %q have the same operation id %q", + endpoint, endpointDup, operation.OperationID) + } + operationIDs[operation.OperationID] = endpoint + } + } + return nil +} + func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) { if strings.IndexByte(path, '{') < 0 { return path, 0, nil diff --git a/openapi3/paths_test.go b/openapi3/paths_test.go index 402288b67..4ff9fba00 100644 --- a/openapi3/paths_test.go +++ b/openapi3/paths_test.go @@ -7,22 +7,88 @@ import ( "github.com/stretchr/testify/require" ) -var emptyPathSpec = ` +func TestPathsValidate(t *testing.T) { + tests := []struct { + name string + spec string + wantErr string + }{ + { + name: "ok, empty paths", + spec: ` openapi: "3.0.0" info: version: 1.0.0 title: Swagger Petstore license: name: MIT -servers: - - url: http://petstore.swagger.io/v1 paths: /pets: -` +`, + }, + { + name: "operation ids are not unique, same path", + spec: ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +paths: + /pets: + post: + operationId: createPet + responses: + 201: + description: "entity created" + delete: + operationId: createPet + responses: + 204: + description: "entity deleted" +`, + wantErr: `operations "DELETE /pets" and "POST /pets" have the same operation id "createPet"`, + }, + { + name: "operation ids are not unique, different paths", + spec: ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +paths: + /pets: + post: + operationId: createPet + responses: + 201: + description: "entity created" + /users: + post: + operationId: createPet + responses: + 201: + description: "entity created" +`, + wantErr: `operations "POST /pets" and "POST /users" have the same operation id "createPet"`, + }, + } + + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + doc, err := NewLoader().LoadFromData([]byte(tt.spec)) + require.NoError(t, err) -func TestPathValidate(t *testing.T) { - doc, err := NewLoader().LoadFromData([]byte(emptyPathSpec)) - require.NoError(t, err) - err = doc.Paths.Validate(context.Background()) - require.NoError(t, err) + err = doc.Paths.Validate(context.Background()) + if tt.wantErr == "" { + require.NoError(t, err) + return + } + require.Equal(t, tt.wantErr, err.Error()) + }) + } } diff --git a/openapi3filter/fixtures/petstore.json b/openapi3filter/fixtures/petstore.json index 398e9b861..932241fc9 100644 --- a/openapi3filter/fixtures/petstore.json +++ b/openapi3filter/fixtures/petstore.json @@ -121,7 +121,7 @@ ], "summary": "Add a new pet to the store", "description": "", - "operationId": "addPet", + "operationId": "addPet2", "responses": { "405": { "description": "Invalid input" From 1b3c813c759981cd263c21faef65b528c911a913 Mon Sep 17 00:00:00 2001 From: Ole Petersen <56505957+peteole@users.noreply.github.com> Date: Thu, 10 Mar 2022 10:21:06 +0100 Subject: [PATCH 135/260] Check response headers and links (#505) Co-authored-by: Ole Petersen Co-authored-by: Pierre Fenoll --- openapi3/response.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openapi3/response.go b/openapi3/response.go index 2ab33aca2..e1da461fc 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -104,5 +104,20 @@ func (value *Response) Validate(ctx context.Context) error { return err } } + if headers := value.Headers; headers != nil { + for _, header := range headers { + if err := header.Validate(ctx); err != nil { + return err + } + } + } + + if links := value.Links; links != nil { + for _, link := range links { + if err := link.Validate(ctx); err != nil { + return err + } + } + } return nil } From 32d9f548fbc614c67a259a75a83377bc2371b99f Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 10 Mar 2022 10:40:29 +0100 Subject: [PATCH 136/260] fix that test situation (#506) --- .github/workflows/go.yml | 17 ++++------------- .gitignore | 4 +--- go.mod | 2 +- openapi3/external_docs.go | 3 ++- .../{testdata => }/load_with_go_embed_test.go | 3 +++ openapi3/request_body.go | 3 ++- 6 files changed, 13 insertions(+), 19 deletions(-) rename openapi3/{testdata => }/load_with_go_embed_test.go (95%) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4a5b87c21..6fa60a53c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,12 +12,11 @@ jobs: strategy: fail-fast: true matrix: - go: ['1.14', '1.x'] - # Locked at https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on + go: ['1.16', '1.x'] os: - - ubuntu-20.04 - - windows-2019 - - macos-10.15 + - ubuntu-latest + - windows-latest + - macos-latest runs-on: ${{ matrix.os }} defaults: run: @@ -47,8 +46,6 @@ jobs: with: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-${{ matrix.go }}-mod-${{ hashFiles('**/go.sum') }} - if: matrix.go != '1.14' - - uses: actions/checkout@v2 @@ -70,12 +67,6 @@ jobs: CGO_ENABLED: '1' - if: runner.os == 'Linux' run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - - run: | - cp openapi3/testdata/load_with_go_embed_test.go openapi3/ - cat go.mod | sed 's%go 1.14%go 1.16%' >gomod && mv gomod go.mod - go test ./... - if: matrix.go != '1.14' - - if: runner.os == 'Linux' name: Errors must not be capitalized https://github.com/golang/go/wiki/CodeReviewComments#error-strings diff --git a/.gitignore b/.gitignore index 1a3ec37f1..40b671156 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,4 @@ # IntelliJ / GoLand .idea - -/openapi3/load_with_go_embed_test.go -.vscode \ No newline at end of file +.vscode diff --git a/go.mod b/go.mod index 9da0ae552..df32d6a7e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/getkin/kin-openapi -go 1.14 +go 1.16 require ( github.com/ghodss/yaml v1.0.0 diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index e57f18aa5..5ea4d455d 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -2,6 +2,7 @@ package openapi3 import ( "context" + "errors" "fmt" "net/url" @@ -26,7 +27,7 @@ func (e *ExternalDocs) UnmarshalJSON(data []byte) error { func (e *ExternalDocs) Validate(ctx context.Context) error { if e.URL == "" { - return fmt.Errorf("url is required") + return errors.New("url is required") } if _, err := url.Parse(e.URL); err != nil { return fmt.Errorf("url is incorrect: %w", err) diff --git a/openapi3/testdata/load_with_go_embed_test.go b/openapi3/load_with_go_embed_test.go similarity index 95% rename from openapi3/testdata/load_with_go_embed_test.go rename to openapi3/load_with_go_embed_test.go index 56b274c9b..e0fb915ba 100644 --- a/openapi3/testdata/load_with_go_embed_test.go +++ b/openapi3/load_with_go_embed_test.go @@ -1,3 +1,6 @@ +//go:build go1.16 +// +build go1.16 + package openapi3_test import ( diff --git a/openapi3/request_body.go b/openapi3/request_body.go index b0ae83119..a1a8ac572 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -2,6 +2,7 @@ package openapi3 import ( "context" + "errors" "fmt" "github.com/getkin/kin-openapi/jsoninfo" @@ -99,7 +100,7 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { func (value *RequestBody) Validate(ctx context.Context) error { if value.Content == nil { - return fmt.Errorf("content of the request body is required") + return errors.New("content of the request body is required") } return value.Content.Validate(ctx) } From 590c85c2ecb0a6217e555bb298e5df2028145473 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 11 Mar 2022 22:43:28 +0100 Subject: [PATCH 137/260] Define missing XML in schema, minor fixes and doc additions (#508) --- .github/workflows/go.yml | 73 +++++++++++++++++++++++++--- README.md | 5 +- jsoninfo/marshal_ref.go | 6 +-- openapi3/callback.go | 3 +- openapi3/components.go | 4 +- openapi3/discriminator.go | 4 +- openapi3/encoding.go | 1 + openapi3/{examples.go => example.go} | 1 + openapi3/external_docs.go | 7 +-- openapi3/header.go | 2 +- openapi3/info.go | 12 +++-- openapi3/link.go | 6 ++- openapi3/media_type.go | 1 + openapi3/openapi3.go | 2 + openapi3/operation.go | 1 + openapi3/parameter.go | 3 +- openapi3/path_item.go | 3 ++ openapi3/paths.go | 3 +- openapi3/refs.go | 1 + openapi3/request_body.go | 2 + openapi3/response.go | 19 ++++---- openapi3/schema.go | 13 ++--- openapi3/security_requirements.go | 2 + openapi3/security_scheme.go | 8 +++ openapi3/server.go | 10 ++-- openapi3/tag.go | 2 + openapi3/xml.go | 31 ++++++++++++ openapi3filter/validation_error.go | 16 +++--- 28 files changed, 187 insertions(+), 54 deletions(-) rename openapi3/{examples.go => example.go} (93%) create mode 100644 openapi3/xml.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6fa60a53c..fc17de29f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -47,26 +47,25 @@ jobs: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-${{ matrix.go }}-mod-${{ hashFiles('**/go.sum') }} + - if: runner.os == 'Linux' + run: sudo apt install silversearcher-ag + - uses: actions/checkout@v2 - run: go mod download && go mod tidy && go mod verify - - if: runner.os == 'Linux' - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + - run: git --no-pager diff --exit-code - run: go vet ./... - - if: runner.os == 'Linux' - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + - run: git --no-pager diff --exit-code - run: go fmt ./... - - if: runner.os == 'Linux' - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + - run: git --no-pager diff --exit-code - run: go test ./... - run: go test -v -run TestRaceyPatternSchema -race ./... env: CGO_ENABLED: '1' - - if: runner.os == 'Linux' - run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + - run: git --no-pager diff --exit-code - if: runner.os == 'Linux' name: Errors must not be capitalized https://github.com/golang/go/wiki/CodeReviewComments#error-strings @@ -77,3 +76,61 @@ jobs: name: Did you mean %q run: | ! git grep -E "'[%].'" + + - if: runner.os == 'Linux' + name: Also add yaml tags + run: | + ! git grep -InE 'json:"' | grep -v _test.go | grep -v yaml: + + - if: runner.os == 'Linux' + name: Missing specification object link to definition + run: | + [[ 30 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]] + + - if: runner.os == 'Linux' + name: Style around ExtensionProps embedding + run: | + ! ag -B2 -A2 'type.[A-Z].+struct..\n.+ExtensionProps\n[^\n]' openapi3/*.go + + - if: runner.os == 'Linux' + name: Ensure all exported fields are mentioned in Validate() impls + run: | + for ty in $TYPES; do + # Ensure definition + if ! ag 'type.[A-Z].+struct..\n.+ExtensionProps' openapi3/*.go | grep -F "type $ty struct"; then + echo "OAI type $ty is not defined" && exit 1 + fi + + # Ensure impl Validate() + if ! git grep -InE 'func [(].+Schema[)] Validate[(]ctx context.Context[)].+error.+[{]'; then + echo "OAI type $ty does not implement Validate()" && exit 1 + fi + + # TODO: $ty mention all its exported fields within Validate() + done + env: + TYPES: > + Components + Contact + Discriminator + Encoding + Example + ExternalDocs + Info + License + Link + MediaType + OAuthFlow + OAuthFlows + Operation + Parameter + PathItem + RequestBody + Response + Schema + SecurityScheme + Server + ServerVariable + T + Tag + XML diff --git a/README.md b/README.md index b01c44a36..a36e37f2e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # Introduction A [Go](https://golang.org) project for handling [OpenAPI](https://www.openapis.org/) files. We target the latest OpenAPI version (currently 3), but the project contains support for older OpenAPI versions too. -Licensed under the [MIT License](LICENSE). +Licensed under the [MIT License](./LICENSE). ## Contributors and users The project has received pull requests from many people. Thanks to everyone! @@ -27,7 +27,8 @@ Here's some projects that depend on _kin-openapi_: * [go-openapi](https://github.com/go-openapi)'s [spec3](https://github.com/go-openapi/spec3) * an iteration on [spec](https://github.com/go-openapi/spec) (for OpenAPIv2) * see [README](https://github.com/go-openapi/spec3/tree/3fab9faa9094e06ebd19ded7ea96d156c2283dca#oai-object-model---) for the missing parts -* See [https://github.com/OAI](https://github.com/OAI)'s [great tooling list](https://github.com/OAI/OpenAPI-Specification/blob/master/IMPLEMENTATIONS.md) + +Be sure to check [OpenAPI Initiative](https://github.com/OAI)'s [great tooling list](https://github.com/OAI/OpenAPI-Specification/blob/master/IMPLEMENTATIONS.md) as well as [OpenAPI.Tools](https://openapi.tools/). # Structure * _openapi2_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi2)) diff --git a/jsoninfo/marshal_ref.go b/jsoninfo/marshal_ref.go index 9738bf08f..29575e9e9 100644 --- a/jsoninfo/marshal_ref.go +++ b/jsoninfo/marshal_ref.go @@ -5,7 +5,7 @@ import ( ) func MarshalRef(value string, otherwise interface{}) ([]byte, error) { - if len(value) > 0 { + if value != "" { return json.Marshal(&refProps{ Ref: value, }) @@ -17,7 +17,7 @@ func UnmarshalRef(data []byte, destRef *string, destOtherwise interface{}) error refProps := &refProps{} if err := json.Unmarshal(data, refProps); err == nil { ref := refProps.Ref - if len(ref) > 0 { + if ref != "" { *destRef = ref return nil } @@ -26,5 +26,5 @@ func UnmarshalRef(data []byte, destRef *string, destOtherwise interface{}) error } type refProps struct { - Ref string `json:"$ref,omitempty"` + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` } diff --git a/openapi3/callback.go b/openapi3/callback.go index 8995e4792..5f883c1c9 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -23,7 +23,8 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) { return ref.Value, nil } -// Callback is specified by OpenAPI/Swagger standard version 3.0. +// Callback is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callbackObject type Callback map[string]*PathItem func (value Callback) Validate(ctx context.Context) error { diff --git a/openapi3/components.go b/openapi3/components.go index 7acafabf9..42af634d6 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -8,9 +8,11 @@ import ( "github.com/getkin/kin-openapi/jsoninfo" ) -// Components is specified by OpenAPI/Swagger standard version 3.0. +// Components is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject type Components struct { ExtensionProps + Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 82ad7040b..5e181a291 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -6,9 +6,11 @@ import ( "github.com/getkin/kin-openapi/jsoninfo" ) -// Discriminator is specified by OpenAPI/Swagger standard version 3.0. +// Discriminator is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject type Discriminator struct { ExtensionProps + PropertyName string `json:"propertyName" yaml:"propertyName"` Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index ad48b9160..b0dab7be0 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -8,6 +8,7 @@ import ( ) // Encoding is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject type Encoding struct { ExtensionProps diff --git a/openapi3/examples.go b/openapi3/example.go similarity index 93% rename from openapi3/examples.go rename to openapi3/example.go index f7f90ce54..19cceb4d9 100644 --- a/openapi3/examples.go +++ b/openapi3/example.go @@ -25,6 +25,7 @@ func (e Examples) JSONLookup(token string) (interface{}, error) { } // Example is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject type Example struct { ExtensionProps diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index 5ea4d455d..bb9dd5a89 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -9,12 +9,13 @@ import ( "github.com/getkin/kin-openapi/jsoninfo" ) -// ExternalDocs is specified by OpenAPI/Swagger standard version 3.0. +// ExternalDocs is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object type ExternalDocs struct { ExtensionProps - Description string `json:"description,omitempty"` - URL string `json:"url,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` } func (e *ExternalDocs) MarshalJSON() ([]byte, error) { diff --git a/openapi3/header.go b/openapi3/header.go index 5fdc31771..9adb5ac35 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -26,7 +26,7 @@ func (h Headers) JSONLookup(token string) (interface{}, error) { } // Header is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#headerObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#headerObject type Header struct { Parameter } diff --git a/openapi3/info.go b/openapi3/info.go index 2adffff1a..6b41589b5 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -7,9 +7,11 @@ import ( "github.com/getkin/kin-openapi/jsoninfo" ) -// Info is specified by OpenAPI/Swagger standard version 3.0. +// Info is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject type Info struct { ExtensionProps + Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` @@ -50,9 +52,11 @@ func (value *Info) Validate(ctx context.Context) error { return nil } -// Contact is specified by OpenAPI/Swagger standard version 3.0. +// Contact is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contactObject type Contact struct { ExtensionProps + Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` Email string `json:"email,omitempty" yaml:"email,omitempty"` @@ -70,9 +74,11 @@ func (value *Contact) Validate(ctx context.Context) error { return nil } -// License is specified by OpenAPI/Swagger standard version 3.0. +// License is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#licenseObject type License struct { ExtensionProps + Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` } diff --git a/openapi3/link.go b/openapi3/link.go index 7d627b8bc..19a725a86 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -25,11 +25,13 @@ func (l Links) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*Links)(nil) -// Link is specified by OpenAPI/Swagger standard version 3.0. +// Link is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#linkObject type Link struct { ExtensionProps - OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` + OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` Server *Server `json:"server,omitempty" yaml:"server,omitempty"` diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 2dd0842f6..5c001ca64 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -8,6 +8,7 @@ import ( ) // MediaType is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject type MediaType struct { ExtensionProps diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index f6c0033ce..d376812b5 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -9,8 +9,10 @@ import ( ) // T is the root of an OpenAPI v3 document +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oasObject type T struct { ExtensionProps + OpenAPI string `json:"openapi" yaml:"openapi"` // Required Components Components `json:"components,omitempty" yaml:"components,omitempty"` Info *Info `json:"info" yaml:"info"` // Required diff --git a/openapi3/operation.go b/openapi3/operation.go index d842719cb..29e70c774 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -11,6 +11,7 @@ import ( ) // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object type Operation struct { ExtensionProps diff --git a/openapi3/parameter.go b/openapi3/parameter.go index a0b8ed11f..e283a98fb 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -83,9 +83,10 @@ func (value Parameters) Validate(ctx context.Context) error { } // Parameter is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#parameterObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameterObject type Parameter struct { ExtensionProps + Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` diff --git a/openapi3/path_item.go b/openapi3/path_item.go index a66502046..4473d639d 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -8,8 +8,11 @@ import ( "github.com/getkin/kin-openapi/jsoninfo" ) +// PathItem is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject type PathItem struct { ExtensionProps + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` diff --git a/openapi3/paths.go b/openapi3/paths.go index ca2209e2b..24ab5f300 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -6,7 +6,8 @@ import ( "strings" ) -// Paths is specified by OpenAPI/Swagger standard version 3.0. +// 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 func (value Paths) Validate(ctx context.Context) error { diff --git a/openapi3/refs.go b/openapi3/refs.go index 312250929..333cd1740 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -8,6 +8,7 @@ import ( ) // Ref is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#referenceObject type Ref struct { Ref string `json:"$ref" yaml:"$ref"` } diff --git a/openapi3/request_body.go b/openapi3/request_body.go index a1a8ac572..0be098c0b 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -26,8 +26,10 @@ func (r RequestBodies) JSONLookup(token string) (interface{}, error) { } // RequestBody is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#requestBodyObject type RequestBody struct { ExtensionProps + Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` Content Content `json:"content" yaml:"content"` diff --git a/openapi3/response.go b/openapi3/response.go index e1da461fc..8e22698f6 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -11,6 +11,7 @@ import ( ) // Responses is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responsesObject type Responses map[string]*ResponseRef var _ jsonpointer.JSONPointable = (*Responses)(nil) @@ -54,8 +55,10 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) { } // Response is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responseObject type Response struct { ExtensionProps + Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` Content Content `json:"content,omitempty" yaml:"content,omitempty"` @@ -104,19 +107,15 @@ func (value *Response) Validate(ctx context.Context) error { return err } } - if headers := value.Headers; headers != nil { - for _, header := range headers { - if err := header.Validate(ctx); err != nil { - return err - } + for _, header := range value.Headers { + if err := header.Validate(ctx); err != nil { + return err } } - if links := value.Links; links != nil { - for _, link := range links { - if err := link.Validate(ctx); err != nil { - return err - } + for _, link := range value.Links { + if err := link.Validate(ctx); err != nil { + return err } } return nil diff --git a/openapi3/schema.go b/openapi3/schema.go index a78e45541..73506f760 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -102,6 +102,7 @@ func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { } // Schema is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject type Schema struct { ExtensionProps @@ -124,12 +125,12 @@ type Schema struct { ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` // Properties - Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` - ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` - WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` + ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"` // Number Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index ce6fcc6f1..df6b6b2d1 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -24,6 +24,8 @@ func (value SecurityRequirements) Validate(ctx context.Context) error { return nil } +// SecurityRequirement is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securityRequirementObject type SecurityRequirement map[string][]string func NewSecurityRequirement() SecurityRequirement { diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 990f258d4..9b89fb950 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -25,6 +25,8 @@ func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) +// SecurityScheme is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securitySchemeObject type SecurityScheme struct { ExtensionProps @@ -166,8 +168,11 @@ func (value *SecurityScheme) Validate(ctx context.Context) error { return nil } +// OAuthFlows is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowsObject type OAuthFlows struct { ExtensionProps + Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` @@ -207,8 +212,11 @@ func (flows *OAuthFlows) Validate(ctx context.Context) error { return errors.New("no OAuth flow is defined") } +// OAuthFlow is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowObject type OAuthFlow struct { ExtensionProps + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` diff --git a/openapi3/server.go b/openapi3/server.go index 4415bd08f..94092a6e6 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -11,7 +11,7 @@ import ( "github.com/getkin/kin-openapi/jsoninfo" ) -// Servers is specified by OpenAPI/Swagger standard version 3.0. +// Servers is specified by OpenAPI/Swagger standard version 3. type Servers []*Server // Validate ensures servers are per the OpenAPIv3 specification. @@ -38,9 +38,11 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) return nil, nil, "" } -// Server is specified by OpenAPI/Swagger standard version 3.0. +// Server is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject type Server struct { ExtensionProps + URL string `json:"url" yaml:"url"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` @@ -147,9 +149,11 @@ func (value *Server) Validate(ctx context.Context) (err error) { return } -// ServerVariable is specified by OpenAPI/Swagger standard version 3.0. +// ServerVariable is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object type ServerVariable struct { ExtensionProps + Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` diff --git a/openapi3/tag.go b/openapi3/tag.go index de2f48793..6aa9a1ea2 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -29,8 +29,10 @@ func (tags Tags) Validate(ctx context.Context) error { } // Tag is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject type Tag struct { ExtensionProps + Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` diff --git a/openapi3/xml.go b/openapi3/xml.go new file mode 100644 index 000000000..8fd2abdee --- /dev/null +++ b/openapi3/xml.go @@ -0,0 +1,31 @@ +package openapi3 + +import ( + "context" + + "github.com/getkin/kin-openapi/jsoninfo" +) + +// XML is specified by OpenAPI/Swagger standard version 3. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject +type XML struct { + ExtensionProps + + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` + Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"` + Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"` +} + +func (value *XML) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(value) +} + +func (value *XML) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, value) +} + +func (value *XML) Validate(ctx context.Context) error { + return nil // TODO +} diff --git a/openapi3filter/validation_error.go b/openapi3filter/validation_error.go index bfeeaa7da..7e685cdef 100644 --- a/openapi3filter/validation_error.go +++ b/openapi3filter/validation_error.go @@ -10,25 +10,25 @@ import ( // Based on https://jsonapi.org/format/#error-objects type ValidationError struct { // A unique identifier for this particular occurrence of the problem. - Id string `json:"id,omitempty"` + Id string `json:"id,omitempty" yaml:"id,omitempty"` // The HTTP status code applicable to this problem. - Status int `json:"status,omitempty"` + Status int `json:"status,omitempty" yaml:"status,omitempty"` // An application-specific error code, expressed as a string value. - Code string `json:"code,omitempty"` + Code string `json:"code,omitempty" yaml:"code,omitempty"` // A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization. - Title string `json:"title,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` // A human-readable explanation specific to this occurrence of the problem. - Detail string `json:"detail,omitempty"` + Detail string `json:"detail,omitempty" yaml:"detail,omitempty"` // An object containing references to the source of the error - Source *ValidationErrorSource `json:"source,omitempty"` + Source *ValidationErrorSource `json:"source,omitempty" yaml:"source,omitempty"` } // ValidationErrorSource struct type ValidationErrorSource struct { // A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute]. - Pointer string `json:"pointer,omitempty"` + Pointer string `json:"pointer,omitempty" yaml:"pointer,omitempty"` // A string indicating which query parameter caused the error. - Parameter string `json:"parameter,omitempty"` + Parameter string `json:"parameter,omitempty" yaml:"parameter,omitempty"` } var _ error = &ValidationError{} From 943ee0538fd1aa84d2b810c513ac01df534b666a Mon Sep 17 00:00:00 2001 From: K Zhang Date: Mon, 14 Mar 2022 05:38:12 -0400 Subject: [PATCH 138/260] discriminator value should verify the type is string to avoid panic (#509) --- openapi3/schema.go | 7 ++++++- openapi3/schema_oneOf_test.go | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 73506f760..6f99d0e6f 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -871,7 +871,12 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return errors.New("input does not contain the discriminator property") } - if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorVal.(string)]; len(schema.Discriminator.Mapping) > 0 && !okcheck { + discriminatorValString, okcheck := discriminatorVal.(string) + if !okcheck { + return errors.New("descriminator value is not a string") + } + + if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck { return errors.New("input does not contain a valid discriminator value") } } diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go index 03fb670b1..7faf26864 100644 --- a/openapi3/schema_oneOf_test.go +++ b/openapi3/schema_oneOf_test.go @@ -116,3 +116,21 @@ func TestVisitJSON_OneOf_NoDiscriptor_MissingField(t *testing.T) { }) require.EqualError(t, err, "doesn't match schema due to: Error at \"/scratches\": property \"scratches\" is missing\nSchema:\n {\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"scratches\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"name\",\n \"scratches\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"name\": \"snoopy\"\n }\n Or Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"name\": \"snoopy\"\n }\n") } + +func TestVisitJSON_OneOf_BadDescriminatorType(t *testing.T) { + s, err := NewLoader().LoadFromData(oneofSpec) + require.NoError(t, err) + err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + "name": "snoopy", + "scratches": true, + "$type": 1, + }) + require.EqualError(t, err, "descriminator value is not a string") + + err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + "name": "snoopy", + "barks": true, + "$type": nil, + }) + require.EqualError(t, err, "descriminator value is not a string") +} From 1a03b66af73a60a87315abdcf85c511b3851c96e Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 14 Mar 2022 15:13:46 +0100 Subject: [PATCH 139/260] Add nilness check to CI (#510) --- .github/workflows/go.yml | 4 ++++ openapi3/schema.go | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index fc17de29f..c652acf97 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -82,6 +82,10 @@ jobs: run: | ! git grep -InE 'json:"' | grep -v _test.go | grep -v yaml: + - if: runner.os == 'Linux' && matrix.go != '1.16' + name: nilness + run: go run golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@latest ./... + - if: runner.os == 'Linux' name: Missing specification object link to definition run: | diff --git a/openapi3/schema.go b/openapi3/schema.go index 6f99d0e6f..1b500b7f3 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -795,8 +795,6 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf } switch value := value.(type) { - case nil: - return schema.visitJSONNull(settings) case bool: return schema.visitJSONBoolean(settings, value) case float64: @@ -1421,7 +1419,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value } } allowed := schema.AdditionalPropertiesAllowed - if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) { + if additionalProperties != nil || allowed == nil || *allowed { if additionalProperties != nil { if err := additionalProperties.visitJSON(settings, v); err != nil { if settings.failfast { From 6c261cec83ddeb864fba46f2f4d96b2554af970c Mon Sep 17 00:00:00 2001 From: slessard Date: Wed, 23 Mar 2022 03:23:54 -0700 Subject: [PATCH 140/260] Add support for formats defined in JSON Draft 2019-09 (#512) Co-authored-by: Steve Lessard --- openapi3/schema.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 1b500b7f3..ee21bc21b 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -671,14 +671,18 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) case TypeString: if format := schema.Format; len(format) > 0 { switch format { - // Supported by OpenAPIv3.0.1: + // Supported by OpenAPIv3.0.3: + // https://spec.openapis.org/oas/v3.0.3 case "byte", "binary", "date", "date-time", "password": - // In JSON Draft-07 (not validated yet though): - case "regex": - case "time", "email", "idn-email": - case "hostname", "idn-hostname", "ipv4", "ipv6": - case "uri", "uri-reference", "iri", "iri-reference", "uri-template": - case "json-pointer", "relative-json-pointer": + // In JSON Draft-07 (not validated yet though): + // https://json-schema.org/draft-07/json-schema-release-notes.html#formats + case "iri", "iri-reference", "uri-template", "idn-email", "idn-hostname": + case "json-pointer", "relative-json-pointer", "regex", "time": + // In JSON Draft 2019-09 (not validated yet though): + // https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary + case "duration", "uuid": + // Defined in some other specification + case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: // Try to check for custom defined formats if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled { From b29c7b74a41f3331b516eb93ea33fe1031af3a7e Mon Sep 17 00:00:00 2001 From: Yarne Decuyper Date: Thu, 24 Mar 2022 16:26:08 +0100 Subject: [PATCH 141/260] Change the order of request validation to validate the Security schemas first before all other paramters (#514) Co-authored-by: yarne --- openapi3filter/validate_request.go | 34 ++--- openapi3filter/validate_request_test.go | 184 ++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 17 deletions(-) create mode 100644 openapi3filter/validate_request_test.go diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 990b299ef..b1bb84fb1 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -40,6 +40,23 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { operationParameters := operation.Parameters pathItemParameters := route.PathItem.Parameters + // Security + security := operation.Security + // If there aren't any security requirements for the operation + if security == nil { + // Use the global security requirements. + security = &route.Spec.Security + } + if security != nil { + if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError { + return err + } + + if err != nil { + me = append(me, err) + } + } + // For each parameter of the PathItem for _, parameterRef := range pathItemParameters { parameter := parameterRef.Value @@ -81,23 +98,6 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { } } - // Security - security := operation.Security - // If there aren't any security requirements for the operation - if security == nil { - // Use the global security requirements. - security = &route.Spec.Security - } - if security != nil { - if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError { - return err - } - - if err != nil { - me = append(me, err) - } - } - if len(me) > 0 { return me } diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go new file mode 100644 index 000000000..b43f6c813 --- /dev/null +++ b/openapi3filter/validate_request_test.go @@ -0,0 +1,184 @@ +package openapi3filter + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/gorillamux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTestRouter(t *testing.T, spec string) routers.Router { + t.Helper() + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + return router +} + +func TestValidateRequest(t *testing.T) { + const spec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: 0.0.1 +paths: + /category: + post: + parameters: + - name: category + in: query + schema: + type: string + required: true + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - subCategory + properties: + subCategory: + type: string + responses: + '201': + description: Created + security: + - apiKey: [] +components: + securitySchemes: + apiKey: + type: apiKey + name: Api-Key + in: header +` + + router := setupTestRouter(t, spec) + + verifyAPIKeyPresence := func(c context.Context, input *AuthenticationInput) error { + if input.SecurityScheme.Type == "apiKey" { + var found bool + switch input.SecurityScheme.In { + case "query": + _, found = input.RequestValidationInput.GetQueryParams()[input.SecurityScheme.Name] + case "header": + _, found = input.RequestValidationInput.Request.Header[http.CanonicalHeaderKey(input.SecurityScheme.Name)] + case "cookie": + _, err := input.RequestValidationInput.Request.Cookie(input.SecurityScheme.Name) + found = !errors.Is(err, http.ErrNoCookie) + } + if !found { + return fmt.Errorf("%v not found in %v", input.SecurityScheme.Name, input.SecurityScheme.In) + } + } + return nil + } + + type testRequestBody struct { + SubCategory string `json:"subCategory"` + } + type args struct { + requestBody *testRequestBody + url string + apiKey string + } + tests := []struct { + name string + args args + expectedErr error + }{ + { + name: "Valid request", + args: args{ + requestBody: &testRequestBody{SubCategory: "Chocolate"}, + url: "/category?category=cookies", + apiKey: "SomeKey", + }, + expectedErr: nil, + }, + { + name: "Invalid operation params", + args: args{ + requestBody: &testRequestBody{SubCategory: "Chocolate"}, + url: "/category?invalidCategory=badCookie", + apiKey: "SomeKey", + }, + expectedErr: &RequestError{}, + }, + { + name: "Invalid request body", + args: args{ + requestBody: nil, + url: "/category?category=cookies", + apiKey: "SomeKey", + }, + expectedErr: &RequestError{}, + }, + { + name: "Invalid security", + args: args{ + requestBody: &testRequestBody{SubCategory: "Chocolate"}, + url: "/category?category=cookies", + apiKey: "", + }, + expectedErr: &SecurityRequirementsError{}, + }, + { + name: "Invalid request body and security", + args: args{ + requestBody: nil, + url: "/category?category=cookies", + apiKey: "", + }, + expectedErr: &SecurityRequirementsError{}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var requestBody io.Reader + if tc.args.requestBody != nil { + testingBody, err := json.Marshal(tc.args.requestBody) + require.NoError(t, err) + requestBody = bytes.NewReader(testingBody) + } + req, err := http.NewRequest(http.MethodPost, tc.args.url, requestBody) + require.NoError(t, err) + req.Header.Add("Content-Type", "application/json") + if tc.args.apiKey != "" { + req.Header.Add("Api-Key", tc.args.apiKey) + } + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + validationInput := &RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + Options: &Options{ + AuthenticationFunc: verifyAPIKeyPresence, + }, + } + err = ValidateRequest(context.Background(), validationInput) + assert.IsType(t, tc.expectedErr, err, "ValidateRequest(): error = %v, expectedError %v", err, tc.expectedErr) + }) + } +} From a5284e92dfa93257a86969175f0c1aedeb34bdfd Mon Sep 17 00:00:00 2001 From: Yarne Decuyper Date: Thu, 24 Mar 2022 21:22:09 +0100 Subject: [PATCH 142/260] Add support for allowEmptyValue (#515) Co-authored-by: Pierre Fenoll --- openapi3filter/fixtures/petstore.json | 105 +++++++++ openapi3filter/internal.go | 12 + openapi3filter/req_resp_decoder.go | 262 +++++++++++++-------- openapi3filter/req_resp_decoder_test.go | 127 +++++++++- openapi3filter/validate_request.go | 21 +- openapi3filter/validation_error_encoder.go | 15 ++ openapi3filter/validation_error_test.go | 26 +- 7 files changed, 453 insertions(+), 115 deletions(-) diff --git a/openapi3filter/fixtures/petstore.json b/openapi3filter/fixtures/petstore.json index 932241fc9..1a8cd8c04 100644 --- a/openapi3filter/fixtures/petstore.json +++ b/openapi3filter/fixtures/petstore.json @@ -211,6 +211,111 @@ ] } }, + "/pets/": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pets by the specified filters", + "description": "Returns a list of pets that comply with the specified filters", + "operationId": "findPets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "explode": true, + "allowEmptyValue": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "pending", + "sold" + ], + "default": "available" + } + } + }, + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "kind", + "in": "query", + "description": "Kinds to filter by", + "required": false, + "explode": false, + "style": "pipeDelimited", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "dog", + "cat", + "turtle", + "bird,with,commas" + ] + } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + {"$ref": "#/components/schemas/PetRequiredProperties"} + ] + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + {"$ref": "#/components/schemas/PetRequiredProperties"} + ] + } + } + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, "/pet/findByTags": { "get": { "tags": [ diff --git a/openapi3filter/internal.go b/openapi3filter/internal.go index facaf1de5..5c6a8a6c6 100644 --- a/openapi3filter/internal.go +++ b/openapi3filter/internal.go @@ -1,6 +1,7 @@ package openapi3filter import ( + "reflect" "strings" ) @@ -11,3 +12,14 @@ func parseMediaType(contentType string) string { } return contentType[:i] } + +func isNilValue(value interface{}) bool { + if value == nil { + return true + } + switch reflect.TypeOf(value).Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(value).IsNil() + } + return false +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 12b368384..0408d8da3 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -110,10 +110,9 @@ func invalidSerializationMethodErr(sm *openapi3.SerializationMethod) error { // Decodes a parameter defined via the content property as an object. It uses // the user specified decoder, or our build-in decoder for application/json func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationInput) ( - value interface{}, schema *openapi3.Schema, err error) { + value interface{}, schema *openapi3.Schema, found bool, err error) { var paramValues []string - var found bool switch param.In { case openapi3.ParameterInPath: var paramValue string @@ -123,9 +122,9 @@ func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationI case openapi3.ParameterInQuery: paramValues, found = input.GetQueryParams()[param.Name] case openapi3.ParameterInHeader: - if paramValue := input.Request.Header.Get(http.CanonicalHeaderKey(param.Name)); paramValue != "" { - paramValues = []string{paramValue} - found = true + var headerValues []string + if headerValues, found = input.Request.Header[http.CanonicalHeaderKey(param.Name)]; found { + paramValues = headerValues } case openapi3.ParameterInCookie: var cookie *http.Cookie @@ -206,30 +205,30 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) } type valueDecoder interface { - DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) - DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) - DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) + DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) + DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) + DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) } // decodeStyledParameter returns a value of an operation's parameter from HTTP request for -// parameters defined using the style format. +// parameters defined using the style format, and whether the parameter is supplied in the input. // The function returns ParseError when HTTP request contains an invalid value of a parameter. -func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationInput) (interface{}, error) { +func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationInput) (interface{}, bool, error) { sm, err := param.SerializationMethod() if err != nil { - return nil, err + return nil, false, err } var dec valueDecoder switch param.In { case openapi3.ParameterInPath: if len(input.PathParams) == 0 { - return nil, nil + return nil, false, nil } dec = &pathParamDecoder{pathParams: input.PathParams} case openapi3.ParameterInQuery: if len(input.GetQueryParams()) == 0 { - return nil, nil + return nil, false, nil } dec = &urlValuesDecoder{values: input.GetQueryParams()} case openapi3.ParameterInHeader: @@ -237,73 +236,79 @@ func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationIn case openapi3.ParameterInCookie: dec = &cookieParamDecoder{req: input.Request} default: - return nil, fmt.Errorf("unsupported parameter's 'in': %s", param.In) + return nil, false, fmt.Errorf("unsupported parameter's 'in': %s", param.In) } return decodeValue(dec, param.Name, sm, param.Schema, param.Required) } -func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (interface{}, error) { +func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (interface{}, bool, error) { + var found bool + if len(schema.Value.AllOf) > 0 { var value interface{} var err error for _, sr := range schema.Value.AllOf { - value, err = decodeValue(dec, param, sm, sr, required) + var f bool + value, f, err = decodeValue(dec, param, sm, sr, required) + found = found || f if value == nil || err != nil { break } } - return value, err + return value, found, err } if len(schema.Value.AnyOf) > 0 { for _, sr := range schema.Value.AnyOf { - value, _ := decodeValue(dec, param, sm, sr, required) + value, f, _ := decodeValue(dec, param, sm, sr, required) + found = found || f if value != nil { - return value, nil + return value, found, nil } } if required { - return nil, fmt.Errorf("decoding anyOf for parameter %q failed", param) + return nil, found, fmt.Errorf("decoding anyOf for parameter %q failed", param) } - return nil, nil + return nil, found, nil } if len(schema.Value.OneOf) > 0 { isMatched := 0 var value interface{} for _, sr := range schema.Value.OneOf { - v, _ := decodeValue(dec, param, sm, sr, required) + v, f, _ := decodeValue(dec, param, sm, sr, required) + found = found || f if v != nil { value = v isMatched++ } } if isMatched == 1 { - return value, nil + return value, found, nil } else if isMatched > 1 { - return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched) + return nil, found, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched) } if required { - return nil, fmt.Errorf("decoding oneOf failed: %q is required", param) + return nil, found, fmt.Errorf("decoding oneOf failed: %q is required", param) } - return nil, nil + return nil, found, nil } if schema.Value.Not != nil { // TODO(decode not): handle decoding "not" JSON Schema - return nil, errors.New("not implemented: decoding 'not'") + return nil, found, errors.New("not implemented: decoding 'not'") } if schema.Value.Type != "" { - var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) + var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) switch schema.Value.Type { case "array": - decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { return dec.DecodeArray(param, sm, schema) } case "object": - decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { return dec.DecodeObject(param, sm, schema) } default: @@ -311,8 +316,20 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho } return decodeFn(param, sm, schema) } - - return nil, nil + switch vDecoder := dec.(type) { + case *pathParamDecoder: + _, found = vDecoder.pathParams[param] + case *urlValuesDecoder: + _, found = vDecoder.values[param] + case *headerParamDecoder: + _, found = vDecoder.header[param] + case *cookieParamDecoder: + _, err := vDecoder.req.Cookie(param) + found = err != http.ErrNoCookie + default: + return nil, found, errors.New("unsupported decoder") + } + return nil, found, nil } // pathParamDecoder decodes values of path parameters. @@ -320,7 +337,7 @@ type pathParamDecoder struct { pathParams map[string]string } -func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { +func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { var prefix string switch sm.Style { case "simple": @@ -330,26 +347,27 @@ func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializat case "matrix": prefix = ";" + param + "=" default: - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } if d.pathParams == nil { // HTTP request does not contains a value of the target path parameter. - return nil, nil + return nil, false, nil } raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. - return nil, nil + return nil, false, nil } src, err := cutPrefix(raw, prefix) if err != nil { - return nil, err + return nil, ok, err } - return parsePrimitive(src, schema) + val, err := parsePrimitive(src, schema) + return val, ok, err } -func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { +func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { var prefix, delim string switch { case sm.Style == "simple": @@ -367,26 +385,27 @@ func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationM prefix = ";" + param + "=" delim = ";" + param + "=" default: - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } if d.pathParams == nil { // HTTP request does not contains a value of the target path parameter. - return nil, nil + return nil, false, nil } raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. - return nil, nil + return nil, false, nil } src, err := cutPrefix(raw, prefix) if err != nil { - return nil, err + return nil, ok, err } - return parseArray(strings.Split(src, delim), schema) + val, err := parseArray(strings.Split(src, delim), schema) + return val, ok, err } -func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { +func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { var prefix, propsDelim, valueDelim string switch { case sm.Style == "simple" && !sm.Explode: @@ -412,27 +431,28 @@ func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.Serialization propsDelim = ";" valueDelim = "=" default: - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } if d.pathParams == nil { // HTTP request does not contains a value of the target path parameter. - return nil, nil + return nil, false, nil } raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. - return nil, nil + return nil, false, nil } src, err := cutPrefix(raw, prefix) if err != nil { - return nil, err + return nil, ok, err } props, err := propsFromString(src, propsDelim, valueDelim) if err != nil { - return nil, err + return nil, ok, err } - return makeObject(props, schema) + val, err := makeObject(props, schema) + return val, ok, err } // cutPrefix validates that a raw value of a path parameter has the specified prefix, @@ -456,28 +476,29 @@ type urlValuesDecoder struct { values url.Values } -func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { +func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { if sm.Style != "form" { - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } - values := d.values[param] + values, ok := d.values[param] if len(values) == 0 { // HTTP request does not contain a value of the target query parameter. - return nil, nil + return nil, ok, nil } - return parsePrimitive(values[0], schema) + val, err := parsePrimitive(values[0], schema) + return val, ok, err } -func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { +func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { if sm.Style == "deepObject" { - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } - values := d.values[param] + values, ok := d.values[param] if len(values) == 0 { // HTTP request does not contain a value of the target query parameter. - return nil, nil + return nil, ok, nil } if !sm.Explode { var delim string @@ -491,10 +512,11 @@ func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationM } values = strings.Split(values[0], delim) } - return parseArray(values, schema) + val, err := parseArray(values, schema) + return val, ok, err } -func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { +func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { var propsFn func(url.Values) (map[string]string, error) switch sm.Style { case "form": @@ -535,17 +557,27 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization return props, nil } default: - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } props, err := propsFn(d.values) if err != nil { - return nil, err + return nil, false, err } if props == nil { - return nil, nil + return nil, false, nil } - return makeObject(props, schema) + + // check the props + found := false + for propName := range schema.Value.Properties { + if _, ok := props[propName]; ok { + found = true + break + } + } + val, err := makeObject(props, schema) + return val, found, err } // headerParamDecoder decodes values of header parameters. @@ -553,47 +585,56 @@ type headerParamDecoder struct { header http.Header } -func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { +func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { if sm.Style != "simple" { - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } - raw := d.header.Get(http.CanonicalHeaderKey(param)) - return parsePrimitive(raw, schema) + raw, ok := d.header[http.CanonicalHeaderKey(param)] + if !ok || len(raw) == 0 { + // HTTP request does not contains a corresponding header or has the empty value + return nil, ok, nil + } + + val, err := parsePrimitive(raw[0], schema) + return val, ok, err } -func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { +func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { if sm.Style != "simple" { - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } - raw := d.header.Get(http.CanonicalHeaderKey(param)) - if raw == "" { + raw, ok := d.header[http.CanonicalHeaderKey(param)] + if !ok || len(raw) == 0 { // HTTP request does not contains a corresponding header - return nil, nil + return nil, ok, nil } - return parseArray(strings.Split(raw, ","), schema) + + val, err := parseArray(strings.Split(raw[0], ","), schema) + return val, ok, err } -func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { +func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { if sm.Style != "simple" { - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } valueDelim := "," if sm.Explode { valueDelim = "=" } - raw := d.header.Get(http.CanonicalHeaderKey(param)) - if raw == "" { + raw, ok := d.header[http.CanonicalHeaderKey(param)] + if !ok || len(raw) == 0 { // HTTP request does not contain a corresponding header. - return nil, nil + return nil, ok, nil } - props, err := propsFromString(raw, ",", valueDelim) + props, err := propsFromString(raw[0], ",", valueDelim) if err != nil { - return nil, err + return nil, ok, err } - return makeObject(props, schema) + val, err := makeObject(props, schema) + return val, ok, err } // cookieParamDecoder decodes values of cookie parameters. @@ -601,56 +642,63 @@ type cookieParamDecoder struct { req *http.Request } -func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { +func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { if sm.Style != "form" { - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } cookie, err := d.req.Cookie(param) - if err == http.ErrNoCookie { + found := err != http.ErrNoCookie + if !found { // HTTP request does not contain a corresponding cookie. - return nil, nil + return nil, found, nil } if err != nil { - return nil, fmt.Errorf("decoding param %q: %s", param, err) + return nil, found, fmt.Errorf("decoding param %q: %s", param, err) } - return parsePrimitive(cookie.Value, schema) + + val, err := parsePrimitive(cookie.Value, schema) + return val, found, err } -func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { +func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { if sm.Style != "form" || sm.Explode { - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } cookie, err := d.req.Cookie(param) - if err == http.ErrNoCookie { + found := err != http.ErrNoCookie + if !found { // HTTP request does not contain a corresponding cookie. - return nil, nil + return nil, found, nil } if err != nil { - return nil, fmt.Errorf("decoding param %q: %s", param, err) + return nil, found, fmt.Errorf("decoding param %q: %s", param, err) } - return parseArray(strings.Split(cookie.Value, ","), schema) + val, err := parseArray(strings.Split(cookie.Value, ","), schema) + return val, found, err } -func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { +func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { if sm.Style != "form" || sm.Explode { - return nil, invalidSerializationMethodErr(sm) + return nil, false, invalidSerializationMethodErr(sm) } cookie, err := d.req.Cookie(param) - if err == http.ErrNoCookie { + found := err != http.ErrNoCookie + if !found { // HTTP request does not contain a corresponding cookie. - return nil, nil + return nil, found, nil } if err != nil { - return nil, fmt.Errorf("decoding param %q: %s", param, err) + return nil, found, fmt.Errorf("decoding param %q: %s", param, err) } props, err := propsFromString(cookie.Value, ",", ",") if err != nil { - return nil, err + return nil, found, err } - return makeObject(props, schema) + val, err := makeObject(props, schema) + return val, found, err } // propsFromString returns a properties map that is created by splitting a source string by propDelim and valueDelim. @@ -725,6 +773,12 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err } return nil, fmt.Errorf("item %d: %s", i, err) } + + // If the items are nil, then the array is nil. There shouldn't be case where some values are actual primitive + // values and some are nil values. + if item == nil { + return nil, nil + } value = append(value, item) } return value, nil @@ -913,7 +967,7 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. } sm := enc.SerializationMethod() - if value, err = decodeValue(dec, name, sm, prop, false); err != nil { + if value, _, err = decodeValue(dec, name, sm, prop, false); err != nil { return nil, err } obj[name] = value diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index f40a7a53e..de93547b5 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -31,7 +31,7 @@ func TestDecodeParameter(t *testing.T) { objectOf = func(args ...interface{}) *openapi3.SchemaRef { s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "object", Properties: make(map[string]*openapi3.SchemaRef)}} if len(args)%2 != 0 { - panic("invalid arguments. must be an odd number of arguments") + panic("invalid arguments. must be an even number of arguments") } for i := 0; i < len(args)/2; i++ { propName := args[i*2].(string) @@ -75,6 +75,7 @@ func TestDecodeParameter(t *testing.T) { header string cookie string want interface{} + found bool err error } @@ -90,23 +91,27 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: stringSchema}, path: "/foo", want: "foo", + found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: stringSchema}, path: "/foo", want: "foo", + found: true, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringSchema}, path: "/.foo", want: "foo", + found: true, }, { name: "label invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringSchema}, path: "/foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -114,11 +119,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringSchema}, path: "/.foo", want: "foo", + found: true, }, { name: "label explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringSchema}, path: "/foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -126,11 +133,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringSchema}, path: "/;param=foo", want: "foo", + found: true, }, { name: "matrix invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringSchema}, path: "/foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -138,11 +147,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringSchema}, path: "/;param=foo", want: "foo", + found: true, }, { name: "matrix explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringSchema}, path: "/foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -150,23 +161,27 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringSchema}, path: "/foo", want: "foo", + found: true, }, { name: "string", param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringSchema}, path: "/foo", want: "foo", + found: true, }, { name: "integer", param: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema}, path: "/1", want: float64(1), + found: true, }, { name: "integer invalid", param: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema}, path: "/foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -174,11 +189,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: numberSchema}, path: "/1.1", want: 1.1, + found: true, }, { name: "number invalid", param: &openapi3.Parameter{Name: "param", In: "path", Schema: numberSchema}, path: "/foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -186,11 +203,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: booleanSchema}, path: "/true", want: true, + found: true, }, { name: "boolean invalid", param: &openapi3.Parameter{Name: "param", In: "path", Schema: booleanSchema}, path: "/foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, }, @@ -203,23 +222,27 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: arraySchema}, path: "/foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: arraySchema}, path: "/foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema}, path: "/.foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "label invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema}, path: "/foo,bar", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"}, }, { @@ -227,11 +250,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema}, path: "/.foo.bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "label explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema}, path: "/foo.bar", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo.bar"}, }, { @@ -239,11 +264,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema}, path: "/;param=foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "matrix invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema}, path: "/foo,bar", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"}, }, { @@ -251,11 +278,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema}, path: "/;param=foo;param=bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "matrix explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema}, path: "/foo,bar", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"}, }, { @@ -263,23 +292,27 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: arraySchema}, path: "/foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "invalid integer items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(integerSchema)}, path: "/1,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(numberSchema)}, path: "/1.1,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(booleanSchema)}, path: "/true,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, @@ -292,23 +325,27 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: objectSchema}, path: "/id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: objectSchema}, path: "/id=foo,name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema}, path: "/.id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "label invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema}, path: "/id,foo,name,bar", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "id,foo,name,bar"}, }, { @@ -316,11 +353,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema}, path: "/.id=foo.name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "label explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema}, path: "/id=foo.name=bar", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "id=foo.name=bar"}, }, { @@ -328,11 +367,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema}, path: "/;param=id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "matrix invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema}, path: "/id,foo,name,bar", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "id,foo,name,bar"}, }, { @@ -340,11 +381,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema}, path: "/;id=foo;name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "matrix explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema}, path: "/id=foo;name=bar", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "id=foo;name=bar"}, }, { @@ -352,23 +395,27 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectSchema}, path: "/id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", integerSchema)}, path: "/foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", numberSchema)}, path: "/foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", booleanSchema)}, path: "/foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, @@ -381,35 +428,41 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: stringSchema}, query: "param=foo", want: "foo", + found: true, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: stringSchema}, query: "param=foo", want: "foo", + found: true, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringSchema}, query: "param=foo", want: "foo", + found: true, }, { name: "string", param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringSchema}, query: "param=foo", want: "foo", + found: true, }, { name: "integer", param: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema}, query: "param=1", want: float64(1), + found: true, }, { name: "integer invalid", param: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema}, query: "param=foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -417,11 +470,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: numberSchema}, query: "param=1.1", want: 1.1, + found: true, }, { name: "number invalid", param: &openapi3.Parameter{Name: "param", In: "query", Schema: numberSchema}, query: "param=foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -429,11 +484,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: booleanSchema}, query: "param=true", want: true, + found: true, }, { name: "boolean invalid", param: &openapi3.Parameter{Name: "param", In: "query", Schema: booleanSchema}, query: "param=foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, }, @@ -446,11 +503,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: allofSchema}, query: "param=1", want: float64(1), + found: true, }, { name: "allofSchema string", param: &openapi3.Parameter{Name: "param", In: "query", Schema: allofSchema}, query: "param=abdf", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "abdf"}, }, }, @@ -463,12 +522,14 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: anyofSchema}, query: "param=1", want: float64(1), + found: true, }, { name: "anyofSchema string", param: &openapi3.Parameter{Name: "param", In: "query", Schema: anyofSchema}, query: "param=abdf", want: "abdf", + found: true, }, }, }, @@ -480,18 +541,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema}, query: "param=true", want: true, + found: true, }, { name: "oneofSchema int", param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema}, query: "param=1122", want: float64(1122), + found: true, }, { name: "oneofSchema string", param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema}, query: "param=abcd", want: nil, + found: true, }, }, }, @@ -503,59 +567,69 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: arraySchema}, query: "param=foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: arraySchema}, query: "param=foo¶m=bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "spaceDelimited", param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: noExplode, Schema: arraySchema}, query: "param=foo bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "spaceDelimited explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: explode, Schema: arraySchema}, query: "param=foo¶m=bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "pipeDelimited", param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: noExplode, Schema: arraySchema}, query: "param=foo|bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "pipeDelimited explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: explode, Schema: arraySchema}, query: "param=foo¶m=bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arraySchema}, query: "param=foo¶m=bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "invalid integer items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(integerSchema)}, query: "param=1¶m=foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(numberSchema)}, query: "param=1.1¶m=foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(booleanSchema)}, query: "param=true¶m=foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, @@ -568,41 +642,48 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: objectSchema}, query: "param=id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: objectSchema}, query: "id=foo&name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "deepObject explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectSchema}, query: "param[id]=foo¶m[name]=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectSchema}, query: "id=foo&name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", integerSchema)}, query: "foo=bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", numberSchema)}, query: "foo=bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", booleanSchema)}, query: "foo=bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, @@ -615,35 +696,41 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: stringSchema}, header: "X-Param:foo", want: "foo", + found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: stringSchema}, header: "X-Param:foo", want: "foo", + found: true, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringSchema}, header: "X-Param:foo", want: "foo", + found: true, }, { name: "string", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringSchema}, header: "X-Param:foo", want: "foo", + found: true, }, { name: "integer", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema}, header: "X-Param:1", want: float64(1), + found: true, }, { name: "integer invalid", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema}, header: "X-Param:foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -651,11 +738,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: numberSchema}, header: "X-Param:1.1", want: 1.1, + found: true, }, { name: "number invalid", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: numberSchema}, header: "X-Param:foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -663,11 +752,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: booleanSchema}, header: "X-Param:true", want: true, + found: true, }, { name: "boolean invalid", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: booleanSchema}, header: "X-Param:foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, }, @@ -680,35 +771,41 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: arraySchema}, header: "X-Param:foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: arraySchema}, header: "X-Param:foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arraySchema}, header: "X-Param:foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "invalid integer items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(integerSchema)}, header: "X-Param:1,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(numberSchema)}, header: "X-Param:1.1,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(booleanSchema)}, header: "X-Param:true,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, @@ -721,35 +818,41 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: objectSchema}, header: "X-Param:id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: objectSchema}, header: "X-Param:id=foo,name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectSchema}, header: "X-Param:id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", integerSchema)}, header: "X-Param:foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", numberSchema)}, header: "X-Param:foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", booleanSchema)}, header: "X-Param:foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, @@ -762,35 +865,41 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: stringSchema}, cookie: "X-Param:foo", want: "foo", + found: true, }, { name: "form explode", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: explode, Schema: stringSchema}, cookie: "X-Param:foo", want: "foo", + found: true, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: stringSchema}, cookie: "X-Param:foo", want: "foo", + found: true, }, { name: "string", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: stringSchema}, cookie: "X-Param:foo", want: "foo", + found: true, }, { name: "integer", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema}, cookie: "X-Param:1", want: float64(1), + found: true, }, { name: "integer invalid", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema}, cookie: "X-Param:foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -798,11 +907,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: numberSchema}, cookie: "X-Param:1.1", want: 1.1, + found: true, }, { name: "number invalid", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: numberSchema}, cookie: "X-Param:foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { @@ -810,11 +921,13 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: booleanSchema}, cookie: "X-Param:true", want: true, + found: true, }, { name: "boolean invalid", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: booleanSchema}, cookie: "X-Param:foo", + found: true, err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, }, @@ -827,23 +940,27 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arraySchema}, cookie: "X-Param:foo,bar", want: []interface{}{"foo", "bar"}, + found: true, }, { name: "invalid integer items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(integerSchema)}, cookie: "X-Param:1,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(numberSchema)}, cookie: "X-Param:1.1,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(booleanSchema)}, cookie: "X-Param:true,foo", + found: true, err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, @@ -856,23 +973,27 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectSchema}, cookie: "X-Param:id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, + found: true, }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", integerSchema)}, cookie: "X-Param:foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", numberSchema)}, cookie: "X-Param:foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", booleanSchema)}, cookie: "X-Param:foo,bar", + found: true, err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, @@ -931,7 +1052,9 @@ func TestDecodeParameter(t *testing.T) { require.NoError(t, err) input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route} - got, err := decodeStyledParameter(tc.param, input) + got, found, err := decodeStyledParameter(tc.param, input) + + require.Truef(t, found == tc.found, "got found: %t, want found: %t", found, tc.found) if tc.err != nil { require.Error(t, err) diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index b1bb84fb1..fae6b09f9 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -19,6 +19,9 @@ var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc") // ErrInvalidRequired is returned when a required value of a parameter or request body is not defined. var ErrInvalidRequired = errors.New("value is required but missing") +// ErrInvalidEmptyValue is returned when a value of a parameter or request body is empty while it's not allowed. +var ErrInvalidEmptyValue = errors.New("empty value is not allowed") + // ValidateRequest is used to validate the given input according to previous // loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a // non-nil error will be returned. @@ -108,6 +111,7 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { // ValidateParameter validates a parameter's value by JSON schema. // The function returns RequestError with a ParseError cause when unable to parse a value. // The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined. +// The function returns RequestError with ErrInvalidEmptyValue cause when a value of a required parameter is not defined. // The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. func ValidateParameter(ctx context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error { if parameter.Schema == nil && parameter.Content == nil { @@ -124,23 +128,28 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param var value interface{} var err error + var found bool var schema *openapi3.Schema // Validation will ensure that we either have content or schema. if parameter.Content != nil { - if value, schema, err = decodeContentParameter(parameter, input); err != nil { + if value, schema, found, err = decodeContentParameter(parameter, input); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } } else { - if value, err = decodeStyledParameter(parameter, input); err != nil { + if value, found, err = decodeStyledParameter(parameter, input); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } schema = parameter.Schema.Value } - // Validate a parameter's value. - if value == nil { - if parameter.Required { - return &RequestError{Input: input, Parameter: parameter, Err: ErrInvalidRequired} + // Validate a parameter's value and presence. + if parameter.Required && !found { + return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired} + } + + if isNilValue(value) { + if !parameter.AllowEmptyValue && found { + return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidEmptyValue.Error(), Err: ErrInvalidEmptyValue} } return nil } diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 205186960..779887db0 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -34,6 +34,8 @@ func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http cErr = convertBasicRequestError(e) } else if e.Err == ErrInvalidRequired { cErr = convertErrInvalidRequired(e) + } else if e.Err == ErrInvalidEmptyValue { + cErr = convertErrInvalidEmptyValue(e) } else if innerErr, ok := e.Err.(*ParseError); ok { cErr = convertParseError(e, innerErr) } else if innerErr, ok := e.Err.(*openapi3.SchemaError); ok { @@ -87,6 +89,19 @@ func convertErrInvalidRequired(e *RequestError) *ValidationError { } } +func convertErrInvalidEmptyValue(e *RequestError) *ValidationError { + if e.Err == ErrInvalidEmptyValue && e.Parameter != nil { + return &ValidationError{ + Status: http.StatusBadRequest, + Title: fmt.Sprintf("parameter %q in %s is not allowed to be empty", e.Parameter.Name, e.Parameter.In), + } + } + return &ValidationError{ + Status: http.StatusBadRequest, + Title: e.Error(), + } +} + func convertParseError(e *RequestError, innerErr *ParseError) *ValidationError { // We treat path params of the wrong type like a 404 instead of a 400 if innerErr.Kind == KindInvalidFormat && e.Parameter != nil && e.Parameter.In == "path" { diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index bdf544210..11032c141 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -181,7 +181,8 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrBody: `parameter "status" in query has an error: value is required but missing`, + wantErrBody: `parameter "status" in query has an error: value is required but missing: value is required but missing`, + wantErrReason: "value is required but missing", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "status" in query is required`}, }, @@ -203,7 +204,25 @@ func getValidationTests(t *testing.T) []*validationTest { { name: "success - ignores unknown query string parameter", args: validationArgs{ - r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?wat=isdis", nil), + r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=available&wat=isdis", nil), + }, + }, + { + name: "error - non required query string has empty value", + args: validationArgs{ + r: newPetstoreRequest(t, http.MethodGet, "/pets/?tags=", nil), + }, + wantErrParam: "tags", + wantErrParamIn: "query", + wantErrBody: `parameter "tags" in query has an error: empty value is not allowed: empty value is not allowed`, + wantErrReason: "empty value is not allowed", + wantErrResponse: &ValidationError{Status: http.StatusBadRequest, + Title: `parameter "tags" in query is not allowed to be empty`}, + }, + { + name: "success - non required query string has empty value, but has AllowEmptyValue", + args: validationArgs{ + r: newPetstoreRequest(t, http.MethodGet, "/pets/?status=", nil), }, }, { @@ -425,7 +444,8 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "petId", wantErrParamIn: "path", - wantErrBody: `parameter "petId" in path has an error: value is required but missing`, + wantErrBody: `parameter "petId" in path has an error: value is required but missing: value is required but missing`, + wantErrReason: "value is required but missing", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "petId" in path is required`}, }, From ca21ef50e5232035978b7cee351f2f06416d5247 Mon Sep 17 00:00:00 2001 From: K Zhang Date: Wed, 30 Mar 2022 09:52:12 -0400 Subject: [PATCH 143/260] RequestError Error() does not include reason if it is the same as err (#517) Co-authored-by: Kanda --- openapi3filter/errors.go | 2 +- openapi3filter/validation_error_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openapi3filter/errors.go b/openapi3filter/errors.go index 8454c817f..1094bcf75 100644 --- a/openapi3filter/errors.go +++ b/openapi3filter/errors.go @@ -22,7 +22,7 @@ var _ interface{ Unwrap() error } = RequestError{} func (err *RequestError) Error() string { reason := err.Reason if e := err.Err; e != nil { - if len(reason) == 0 { + if len(reason) == 0 || reason == e.Error() { reason = e.Error() } else { reason += ": " + e.Error() diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 11032c141..aae26e56b 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -181,7 +181,7 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrBody: `parameter "status" in query has an error: value is required but missing: value is required but missing`, + wantErrBody: `parameter "status" in query has an error: value is required but missing`, wantErrReason: "value is required but missing", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "status" in query is required`}, @@ -214,7 +214,7 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "tags", wantErrParamIn: "query", - wantErrBody: `parameter "tags" in query has an error: empty value is not allowed: empty value is not allowed`, + wantErrBody: `parameter "tags" in query has an error: empty value is not allowed`, wantErrReason: "empty value is not allowed", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "tags" in query is not allowed to be empty`}, @@ -444,7 +444,7 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "petId", wantErrParamIn: "path", - wantErrBody: `parameter "petId" in path has an error: value is required but missing: value is required but missing`, + wantErrBody: `parameter "petId" in path has an error: value is required but missing`, wantErrReason: "value is required but missing", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "petId" in path is required`}, From cd4e291bcf5b739503e9c5e169d08c628b58a596 Mon Sep 17 00:00:00 2001 From: Anthony Fok Date: Wed, 30 Mar 2022 08:03:17 -0600 Subject: [PATCH 144/260] Fix ExampleValidator test for 32-bit architectures (#516) --- openapi3filter/middleware_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go index 4d88aaf91..930af2ada 100644 --- a/openapi3filter/middleware_test.go +++ b/openapi3filter/middleware_test.go @@ -420,7 +420,7 @@ paths: // requests. squareHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { xParam := path.Base(r.URL.Path) - x, err := strconv.Atoi(xParam) + x, err := strconv.ParseInt(xParam, 10, 64) if err != nil { panic(err) } From 4053935af7a9becbbc7261d137a365726e37623e Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 30 Mar 2022 16:52:51 +0200 Subject: [PATCH 145/260] openapi2: add missing schemes field of operation object (#519) --- openapi2/openapi2.go | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index de9247f67..167bc572a 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -151,6 +151,7 @@ type Operation struct { Responses map[string]*Response `json:"responses" yaml:"responses"` Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` + Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` } From 8287d3697f36b53c0aca5926bab441b0de465ae7 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 30 Mar 2022 16:53:36 +0200 Subject: [PATCH 146/260] Run CI tests on 386 too cc #516 (#518) --- .github/workflows/go.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c652acf97..5cdecc97b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -61,6 +61,10 @@ jobs: - run: go fmt ./... - run: git --no-pager diff --exit-code + - if: runner.os == 'Linux' + run: go test ./... + env: + GOARCH: '386' - run: go test ./... - run: go test -v -run TestRaceyPatternSchema -race ./... env: From 136a868a30c261eed70c2b84d64e2fb91ba15d50 Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Fri, 1 Apr 2022 12:53:09 -0400 Subject: [PATCH 147/260] Add ExcludeSchema sentinel error for schemaCustomizer (#522) Co-authored-by: Pierre Fenoll --- openapi3gen/openapi3gen.go | 14 +++++++++++++- openapi3gen/openapi3gen_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index f2a45c4e0..4387727f7 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -18,6 +18,11 @@ type CycleError struct{} func (err *CycleError) Error() string { return "detected cycle" } +// ExcludeSchemaSentinel indicates that the schema for a specific field should not be included in the final output. +type ExcludeSchemaSentinel struct{} + +func (err *ExcludeSchemaSentinel) Error() string { return "schema excluded" } + // Option allows tweaking SchemaRef generation type Option func(*generatorOpt) @@ -25,7 +30,10 @@ type Option func(*generatorOpt) // the OpenAPI schema definition to be updated with additional // properties during the generation process, based on the // name of the field, the Go type, and the struct tags. -// name will be "_root" for the top level object, and tag will be "" +// name will be "_root" for the top level object, and tag will be "". +// A SchemaCustomizerFn can return an ExcludeSchemaSentinel error to +// indicate that the schema for this field should not be included in +// the final output type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error type generatorOpt struct { @@ -117,6 +125,10 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect return ref, nil } ref, err := g.generateWithoutSaving(parents, t, name, tag) + if _, ok := err.(*ExcludeSchemaSentinel); ok { + // This schema should not be included in the final output + return nil, nil + } if err != nil { return nil, err } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 3640d4c0f..a85f5da4a 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -468,6 +468,30 @@ func TestSchemaCustomizerError(t *testing.T) { require.EqualError(t, err, "test error") } +func TestSchemaCustomizerExcludeSchema(t *testing.T) { + type Bla struct { + Str string + } + + customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { + return nil + }) + schema, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer) + require.NoError(t, err) + require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ + Type: "object", + Properties: map[string]*openapi3.SchemaRef{ + "Str": {Value: &openapi3.Schema{Type: "string"}}, + }}}, schema) + + customizer = openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { + return &openapi3gen.ExcludeSchemaSentinel{} + }) + schema, err = openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer) + require.NoError(t, err) + require.Nil(t, schema) +} + func ExampleNewSchemaRefForValue_recursive() { type RecursiveType struct { Field1 string `json:"field1"` From 173d9bf6f0164e248bd3d2156517c8d3d58689f2 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 1 Apr 2022 19:45:44 +0200 Subject: [PATCH 148/260] test link refs (#525) --- openapi3/loader_test.go | 14 ++ openapi3/testdata/link-example.yaml | 203 ++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 openapi3/testdata/link-example.yaml diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 384e54d87..e33b75d72 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -438,6 +438,20 @@ paths: require.Equal(t, "link to to the father", link.Description) } +func TestLinksFromOAISpec(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromFile("testdata/link-example.yaml") + 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 + link := response.Links[`repositoryPullRequests`].Value + require.Equal(t, map[string]interface{}{ + "username": "$response.body#/owner/username", + "slug": "$response.body#/slug", + }, link.Parameters) +} + func TestResolveNonComponentsRef(t *testing.T) { spec := []byte(` openapi: 3.0.0 diff --git a/openapi3/testdata/link-example.yaml b/openapi3/testdata/link-example.yaml new file mode 100644 index 000000000..5837d705e --- /dev/null +++ b/openapi3/testdata/link-example.yaml @@ -0,0 +1,203 @@ +openapi: 3.0.0 +info: + title: Link Example + version: 1.0.0 +paths: + /2.0/users/{username}: + get: + operationId: getUserByName + parameters: + - name: username + in: path + required: true + schema: + type: string + responses: + '200': + description: The User + content: + application/json: + schema: + $ref: '#/components/schemas/user' + links: + userRepositories: + $ref: '#/components/links/UserRepositories' + /2.0/repositories/{username}: + get: + operationId: getRepositoriesByOwner + parameters: + - name: username + in: path + required: true + schema: + type: string + responses: + '200': + description: repositories owned by the supplied user + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/repository' + links: + userRepository: + $ref: '#/components/links/UserRepository' + /2.0/repositories/{username}/{slug}: + get: + operationId: getRepository + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: slug + in: path + required: true + schema: + type: string + responses: + '200': + description: The repository + content: + application/json: + schema: + $ref: '#/components/schemas/repository' + links: + repositoryPullRequests: + $ref: '#/components/links/RepositoryPullRequests' + /2.0/repositories/{username}/{slug}/pullrequests: + get: + operationId: getPullRequestsByRepository + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: slug + in: path + required: true + schema: + type: string + - name: state + in: query + schema: + type: string + enum: + - open + - merged + - declined + responses: + '200': + description: an array of pull request objects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/pullrequest' + /2.0/repositories/{username}/{slug}/pullrequests/{pid}: + get: + operationId: getPullRequestsById + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: slug + in: path + required: true + schema: + type: string + - name: pid + in: path + required: true + schema: + type: string + responses: + '200': + description: a pull request object + content: + application/json: + schema: + $ref: '#/components/schemas/pullrequest' + links: + pullRequestMerge: + $ref: '#/components/links/PullRequestMerge' + /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge: + post: + operationId: mergePullRequest + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: slug + in: path + required: true + schema: + type: string + - name: pid + in: path + required: true + schema: + type: string + responses: + '204': + description: the PR was successfully merged +components: + links: + UserRepositories: + # returns array of '#/components/schemas/repository' + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + UserRepository: + # returns '#/components/schemas/repository' + operationId: getRepository + parameters: + username: $response.body#/owner/username + slug: $response.body#/slug + RepositoryPullRequests: + # returns '#/components/schemas/pullrequest' + operationId: getPullRequestsByRepository + parameters: + username: $response.body#/owner/username + slug: $response.body#/slug + PullRequestMerge: + # executes /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge + operationId: mergePullRequest + parameters: + username: $response.body#/author/username + slug: $response.body#/repository/slug + pid: $response.body#/id + schemas: + user: + type: object + properties: + username: + type: string + uuid: + type: string + repository: + type: object + properties: + slug: + type: string + owner: + $ref: '#/components/schemas/user' + pullrequest: + type: object + properties: + id: + type: integer + title: + type: string + repository: + $ref: '#/components/schemas/repository' + author: + $ref: '#/components/schemas/user' From 97370ebd190568b238279b457516f8ced94df3d4 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sat, 2 Apr 2022 02:30:21 +0200 Subject: [PATCH 149/260] add missing validation of components: examples, links, callbacks (#526) --- openapi3/components.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openapi3/components.go b/openapi3/components.go index 42af634d6..6d4fe8086 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -91,6 +91,33 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } + for k, v := range components.Examples { + if err = ValidateIdentifier(k); err != nil { + return + } + if err = v.Validate(ctx); err != nil { + return + } + } + + for k, v := range components.Links { + if err = ValidateIdentifier(k); err != nil { + return + } + if err = v.Validate(ctx); err != nil { + return + } + } + + for k, v := range components.Callbacks { + if err = ValidateIdentifier(k); err != nil { + return + } + if err = v.Validate(ctx); err != nil { + return + } + } + return } From d32b516d8d848c04c99428b0a89c72b9ed077857 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sun, 3 Apr 2022 13:24:32 +0200 Subject: [PATCH 150/260] openapi2: remove undefined tag (#527) --- openapi2/openapi2.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 167bc572a..756507a61 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -21,9 +21,9 @@ type T struct { Host string `json:"host,omitempty" yaml:"host,omitempty"` BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` - Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty,noref" yaml:"definitions,omitempty,noref"` - Parameters map[string]*Parameter `json:"parameters,omitempty,noref" yaml:"parameters,omitempty,noref"` - Responses map[string]*Response `json:"responses,omitempty,noref" yaml:"responses,omitempty,noref"` + Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` + Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` From 4ecabc193e47523dc4fbfeb406706e78ec7ef6e3 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sun, 3 Apr 2022 14:31:11 +0200 Subject: [PATCH 151/260] testing: fix incorrect document (#529) --- openapi3filter/middleware_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go index 930af2ada..c6a5e9bc2 100644 --- a/openapi3filter/middleware_test.go +++ b/openapi3filter/middleware_test.go @@ -383,7 +383,6 @@ func ExampleValidator() { // OpenAPI specification for a simple service that squares integers, with // some limitations. doc, err := openapi3.NewLoader().LoadFromData([]byte(` -info: openapi: 3.0.0 info: title: 'Validator - square example' From 869d5dfeed3e0c9df488aea945be24ac77a907b2 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sun, 3 Apr 2022 14:36:26 +0200 Subject: [PATCH 152/260] testing: compare graphs using graph tools (#528) --- openapi2conv/issue187_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index 1c113b708..a7016893a 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -103,7 +103,7 @@ func TestIssue187(t *testing.T) { spec3, err := json.Marshal(doc3) require.NoError(t, err) const expected = `{"components":{"schemas":{"model.ProductSearchAttributeRequest":{"properties":{"filterField":{"type":"string"},"filterKey":{"type":"string"},"type":{"type":"string"},"values":{"$ref":"#/components/schemas/model.ProductSearchAttributeValueRequest"}},"title":"model.ProductSearchAttributeRequest","type":"object"},"model.ProductSearchAttributeValueRequest":{"properties":{"imageUrl":{"type":"string"},"text":{"type":"string"}},"title":"model.ProductSearchAttributeValueRequest","type":"object"}}},"info":{"contact":{"email":"test@test.com","name":"Test"},"description":"Test Golang Application","title":"Test","version":"1.0"},"openapi":"3.0.3","paths":{"/me":{"get":{"operationId":"someTest","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/model.ProductSearchAttributeRequest"}}},"description":"successful operation"}},"summary":"Some test","tags":["probe"]}}}}` - require.Equal(t, string(spec3), expected) + require.JSONEq(t, string(spec3), expected) err = doc3.Validate(context.Background()) require.NoError(t, err) From ebcbb7269761b8506a38a3b001f508939aadc097 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sun, 3 Apr 2022 15:24:54 +0200 Subject: [PATCH 153/260] Fix some golints (#530) --- openapi2/openapi2.go | 14 ++++ openapi3/callback.go | 6 +- openapi3/components.go | 3 + openapi3/content.go | 6 +- openapi3/discriminator.go | 13 ++-- openapi3/encoding.go | 11 +-- openapi3/example.go | 6 +- openapi3/external_docs.go | 3 + openapi3/header.go | 68 +++++++++--------- openapi3/info.go | 49 +++++++------ openapi3/link.go | 24 ++++--- openapi3/media_type.go | 10 ++- openapi3/openapi3.go | 21 +++--- openapi3/operation.go | 14 ++-- openapi3/parameter.go | 115 ++++++++++++++++-------------- openapi3/path_item.go | 7 +- openapi3/paths.go | 15 ++-- openapi3/refs.go | 35 +++++++++ openapi3/request_body.go | 10 ++- openapi3/response.go | 21 +++--- openapi3/schema.go | 10 ++- openapi3/security_requirements.go | 8 ++- openapi3/security_scheme.go | 46 +++++++----- openapi3/server.go | 30 ++++---- openapi3/tag.go | 4 ++ openapi3/xml.go | 13 ++-- 26 files changed, 354 insertions(+), 208 deletions(-) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 756507a61..c5042882d 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -29,10 +29,12 @@ type T struct { Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } +// MarshalJSON returns the JSON encoding of T. func (doc *T) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(doc) } +// UnmarshalJSON sets T to a copy of data. func (doc *T) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, doc) } @@ -64,10 +66,12 @@ type PathItem struct { Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` } +// MarshalJSON returns the JSON encoding of PathItem. func (pathItem *PathItem) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(pathItem) } +// UnmarshalJSON sets PathItem to a copy of data. func (pathItem *PathItem) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, pathItem) } @@ -155,10 +159,12 @@ type Operation struct { Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` } +// MarshalJSON returns the JSON encoding of Operation. func (operation *Operation) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(operation) } +// UnmarshalJSON sets Operation to a copy of data. func (operation *Operation) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, operation) } @@ -207,10 +213,12 @@ type Parameter struct { Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` } +// MarshalJSON returns the JSON encoding of Parameter. func (parameter *Parameter) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(parameter) } +// UnmarshalJSON sets Parameter to a copy of data. func (parameter *Parameter) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, parameter) } @@ -224,10 +232,12 @@ type Response struct { Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` } +// MarshalJSON returns the JSON encoding of Response. func (response *Response) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(response) } +// UnmarshalJSON sets Response to a copy of data. func (response *Response) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, response) } @@ -236,10 +246,12 @@ type Header struct { Parameter } +// MarshalJSON returns the JSON encoding of Header. func (header *Header) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(header) } +// UnmarshalJSON sets Header to a copy of data. func (header *Header) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, header) } @@ -260,10 +272,12 @@ type SecurityScheme struct { Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } +// MarshalJSON returns the JSON encoding of SecurityScheme. func (securityScheme *SecurityScheme) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(securityScheme) } +// UnmarshalJSON sets SecurityScheme to a copy of data. func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, securityScheme) } diff --git a/openapi3/callback.go b/openapi3/callback.go index 5f883c1c9..718f47c1e 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -11,6 +11,7 @@ type Callbacks map[string]*CallbackRef var _ jsonpointer.JSONPointable = (*Callbacks)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (c Callbacks) JSONLookup(token string) (interface{}, error) { ref, ok := c[token] if ref == nil || !ok { @@ -27,8 +28,9 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) { // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callbackObject type Callback map[string]*PathItem -func (value Callback) Validate(ctx context.Context) error { - for _, v := range value { +// Validate returns an error if Callback does not comply with the OpenAPI spec. +func (callback Callback) Validate(ctx context.Context) error { + for _, v := range callback { if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/components.go b/openapi3/components.go index 6d4fe8086..2f19943db 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -28,14 +28,17 @@ func NewComponents() Components { return Components{} } +// MarshalJSON returns the JSON encoding of Components. func (components *Components) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(components) } +// UnmarshalJSON sets Components to a copy of data. func (components *Components) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, components) } +// Validate returns an error if Components does not comply with the OpenAPI spec. func (components *Components) Validate(ctx context.Context) (err error) { for k, v := range components.Schemas { if err = ValidateIdentifier(k); err != nil { diff --git a/openapi3/content.go b/openapi3/content.go index 5edb7d3fa..10e3e6009 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -104,9 +104,9 @@ func (content Content) Get(mime string) *MediaType { return content["*/*"] } -func (value Content) Validate(ctx context.Context) error { - for _, v := range value { - // Validate MediaType +// Validate returns an error if Content does not comply with the OpenAPI spec. +func (content Content) Validate(ctx context.Context) error { + for _, v := range content { if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 5e181a291..4cc4df903 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -15,14 +15,17 @@ type Discriminator struct { Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` } -func (value *Discriminator) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(value) +// MarshalJSON returns the JSON encoding of Discriminator. +func (discriminator *Discriminator) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(discriminator) } -func (value *Discriminator) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +// UnmarshalJSON sets Discriminator to a copy of data. +func (discriminator *Discriminator) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, discriminator) } -func (value *Discriminator) Validate(ctx context.Context) error { +// Validate returns an error if Discriminator does not comply with the OpenAPI spec. +func (discriminator *Discriminator) Validate(ctx context.Context) error { return nil } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index b0dab7be0..e6453ecc1 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -39,10 +39,12 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding { return encoding } +// MarshalJSON returns the JSON encoding of Encoding. func (encoding *Encoding) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(encoding) } +// UnmarshalJSON sets Encoding to a copy of data. func (encoding *Encoding) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, encoding) } @@ -62,11 +64,12 @@ func (encoding *Encoding) SerializationMethod() *SerializationMethod { return sm } -func (value *Encoding) Validate(ctx context.Context) error { - if value == nil { +// Validate returns an error if Encoding does not comply with the OpenAPI spec. +func (encoding *Encoding) Validate(ctx context.Context) error { + if encoding == nil { return nil } - for k, v := range value.Headers { + for k, v := range encoding.Headers { if err := ValidateIdentifier(k); err != nil { return nil } @@ -76,7 +79,7 @@ func (value *Encoding) Validate(ctx context.Context) error { } // Validate a media types's serialization method. - sm := value.SerializationMethod() + sm := encoding.SerializationMethod() switch { case sm.Style == SerializationForm && sm.Explode, sm.Style == SerializationForm && !sm.Explode, diff --git a/openapi3/example.go b/openapi3/example.go index 19cceb4d9..080845cad 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -12,6 +12,7 @@ type Examples map[string]*ExampleRef var _ jsonpointer.JSONPointable = (*Examples)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (e Examples) JSONLookup(token string) (interface{}, error) { ref, ok := e[token] if ref == nil || !ok { @@ -41,14 +42,17 @@ func NewExample(value interface{}) *Example { } } +// MarshalJSON returns the JSON encoding of Example. func (example *Example) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(example) } +// UnmarshalJSON sets Example to a copy of data. func (example *Example) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, example) } -func (value *Example) Validate(ctx context.Context) error { +// Validate returns an error if Example does not comply with the OpenAPI spec. +func (example *Example) Validate(ctx context.Context) error { return nil // TODO } diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index bb9dd5a89..17a38eec0 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -18,14 +18,17 @@ type ExternalDocs struct { URL string `json:"url,omitempty" yaml:"url,omitempty"` } +// MarshalJSON returns the JSON encoding of ExternalDocs. func (e *ExternalDocs) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(e) } +// UnmarshalJSON sets ExternalDocs to a copy of data. func (e *ExternalDocs) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, e) } +// Validate returns an error if ExternalDocs does not comply with the OpenAPI spec. func (e *ExternalDocs) Validate(ctx context.Context) error { if e.URL == "" { return errors.New("url is required") diff --git a/openapi3/header.go b/openapi3/header.go index 9adb5ac35..84ffd8866 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -13,6 +13,7 @@ type Headers map[string]*HeaderRef var _ jsonpointer.JSONPointable = (*Headers)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (h Headers) JSONLookup(token string) (interface{}, error) { ref, ok := h[token] if ref == nil || !ok { @@ -33,33 +34,35 @@ type Header struct { var _ jsonpointer.JSONPointable = (*Header)(nil) -func (value *Header) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +// UnmarshalJSON sets Header to a copy of data. +func (header *Header) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, header) } // SerializationMethod returns a header's serialization method. -func (value *Header) SerializationMethod() (*SerializationMethod, error) { - style := value.Style +func (header *Header) SerializationMethod() (*SerializationMethod, error) { + style := header.Style if style == "" { style = SerializationSimple } explode := false - if value.Explode != nil { - explode = *value.Explode + if header.Explode != nil { + explode = *header.Explode } return &SerializationMethod{Style: style, Explode: explode}, nil } -func (value *Header) Validate(ctx context.Context) error { - if value.Name != "" { +// Validate returns an error if Header does not comply with the OpenAPI spec. +func (header *Header) Validate(ctx context.Context) error { + if header.Name != "" { return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map") } - if value.In != "" { + if header.In != "" { return errors.New("header 'in' MUST NOT be specified, it is implicitly in header") } // Validate a parameter's serialization method. - sm, err := value.SerializationMethod() + sm, err := header.SerializationMethod() if err != nil { return err } @@ -70,17 +73,17 @@ func (value *Header) Validate(ctx context.Context) error { return fmt.Errorf("header schema is invalid: %v", e) } - if (value.Schema == nil) == (value.Content == nil) { - e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", value) + if (header.Schema == nil) == (header.Content == nil) { + e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", header) return fmt.Errorf("header schema is invalid: %v", e) } - if schema := value.Schema; schema != nil { + if schema := header.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { return fmt.Errorf("header schema is invalid: %v", err) } } - if content := value.Content; content != nil { + if content := header.Content; content != nil { if err := content.Validate(ctx); err != nil { return fmt.Errorf("header content is invalid: %v", err) } @@ -88,41 +91,42 @@ func (value *Header) Validate(ctx context.Context) error { return nil } -func (value Header) JSONLookup(token string) (interface{}, error) { +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable +func (header Header) JSONLookup(token string) (interface{}, error) { switch token { case "schema": - if value.Schema != nil { - if value.Schema.Ref != "" { - return &Ref{Ref: value.Schema.Ref}, nil + if header.Schema != nil { + if header.Schema.Ref != "" { + return &Ref{Ref: header.Schema.Ref}, nil } - return value.Schema.Value, nil + return header.Schema.Value, nil } case "name": - return value.Name, nil + return header.Name, nil case "in": - return value.In, nil + return header.In, nil case "description": - return value.Description, nil + return header.Description, nil case "style": - return value.Style, nil + return header.Style, nil case "explode": - return value.Explode, nil + return header.Explode, nil case "allowEmptyValue": - return value.AllowEmptyValue, nil + return header.AllowEmptyValue, nil case "allowReserved": - return value.AllowReserved, nil + return header.AllowReserved, nil case "deprecated": - return value.Deprecated, nil + return header.Deprecated, nil case "required": - return value.Required, nil + return header.Required, nil case "example": - return value.Example, nil + return header.Example, nil case "examples": - return value.Examples, nil + return header.Examples, nil case "content": - return value.Content, nil + return header.Content, nil } - v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(header.ExtensionProps, token) return v, err } diff --git a/openapi3/info.go b/openapi3/info.go index 6b41589b5..e60f8882f 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -20,32 +20,35 @@ type Info struct { Version string `json:"version" yaml:"version"` // Required } -func (value *Info) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(value) +// MarshalJSON returns the JSON encoding of Info. +func (info *Info) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(info) } -func (value *Info) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +// UnmarshalJSON sets Info to a copy of data. +func (info *Info) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, info) } -func (value *Info) Validate(ctx context.Context) error { - if contact := value.Contact; contact != nil { +// Validate returns an error if Info does not comply with the OpenAPI spec. +func (info *Info) Validate(ctx context.Context) error { + if contact := info.Contact; contact != nil { if err := contact.Validate(ctx); err != nil { return err } } - if license := value.License; license != nil { + if license := info.License; license != nil { if err := license.Validate(ctx); err != nil { return err } } - if value.Version == "" { + if info.Version == "" { return errors.New("value of version must be a non-empty string") } - if value.Title == "" { + if info.Title == "" { return errors.New("value of title must be a non-empty string") } @@ -62,15 +65,18 @@ type Contact struct { Email string `json:"email,omitempty" yaml:"email,omitempty"` } -func (value *Contact) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(value) +// MarshalJSON returns the JSON encoding of Contact. +func (contact *Contact) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(contact) } -func (value *Contact) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +// UnmarshalJSON sets Contact to a copy of data. +func (contact *Contact) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, contact) } -func (value *Contact) Validate(ctx context.Context) error { +// Validate returns an error if Contact does not comply with the OpenAPI spec. +func (contact *Contact) Validate(ctx context.Context) error { return nil } @@ -83,16 +89,19 @@ type License struct { URL string `json:"url,omitempty" yaml:"url,omitempty"` } -func (value *License) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(value) +// MarshalJSON returns the JSON encoding of License. +func (license *License) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(license) } -func (value *License) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +// UnmarshalJSON sets License to a copy of data. +func (license *License) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, license) } -func (value *License) Validate(ctx context.Context) error { - if value.Name == "" { +// Validate returns an error if License does not comply with the OpenAPI spec. +func (license *License) Validate(ctx context.Context) error { + if license.Name == "" { return errors.New("value of license name must be a non-empty string") } return nil diff --git a/openapi3/link.go b/openapi3/link.go index 19a725a86..2f0bc57c9 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -11,8 +11,9 @@ import ( type Links map[string]*LinkRef -func (l Links) JSONLookup(token string) (interface{}, error) { - ref, ok := l[token] +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable +func (links Links) JSONLookup(token string) (interface{}, error) { + ref, ok := links[token] if ok == false { return nil, fmt.Errorf("object has no field %q", token) } @@ -38,20 +39,23 @@ type Link struct { RequestBody interface{} `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` } -func (value *Link) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(value) +// MarshalJSON returns the JSON encoding of Link. +func (link *Link) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(link) } -func (value *Link) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +// UnmarshalJSON sets Link to a copy of data. +func (link *Link) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, link) } -func (value *Link) Validate(ctx context.Context) error { - if value.OperationID == "" && value.OperationRef == "" { +// Validate returns an error if Link does not comply with the OpenAPI spec. +func (link *Link) Validate(ctx context.Context) error { + if link.OperationID == "" && link.OperationRef == "" { return errors.New("missing operationId or operationRef on link") } - if value.OperationID != "" && value.OperationRef != "" { - return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", value.OperationID, value.OperationRef) + if link.OperationID != "" && link.OperationRef != "" { + return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", link.OperationID, link.OperationRef) } return nil } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 5c001ca64..dd33b99b2 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -60,19 +60,22 @@ func (mediaType *MediaType) WithEncoding(name string, enc *Encoding) *MediaType return mediaType } +// MarshalJSON returns the JSON encoding of MediaType. func (mediaType *MediaType) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(mediaType) } +// UnmarshalJSON sets MediaType to a copy of data. func (mediaType *MediaType) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, mediaType) } -func (value *MediaType) Validate(ctx context.Context) error { - if value == nil { +// Validate returns an error if MediaType does not comply with the OpenAPI spec. +func (mediaType *MediaType) Validate(ctx context.Context) error { + if mediaType == nil { return nil } - if schema := value.Schema; schema != nil { + if schema := mediaType.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { return err } @@ -80,6 +83,7 @@ func (value *MediaType) Validate(ctx context.Context) error { return nil } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (mediaType MediaType) JSONLookup(token string) (interface{}, error) { switch token { case "schema": diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index d376812b5..c0188c25a 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -23,10 +23,12 @@ type T struct { ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } +// MarshalJSON returns the JSON encoding of T. func (doc *T) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(doc) } +// UnmarshalJSON sets T to a copy of data. func (doc *T) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, doc) } @@ -49,8 +51,9 @@ func (doc *T) AddServer(server *Server) { doc.Servers = append(doc.Servers, server) } -func (value *T) Validate(ctx context.Context) error { - if value.OpenAPI == "" { +// Validate returns an error if T does not comply with the OpenAPI spec. +func (doc *T) Validate(ctx context.Context) error { + if doc.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") } @@ -58,14 +61,14 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid components: %v", e) } - if err := value.Components.Validate(ctx); err != nil { + if err := doc.Components.Validate(ctx); err != nil { return wrap(err) } } { wrap := func(e error) error { return fmt.Errorf("invalid info: %v", e) } - if v := value.Info; v != nil { + if v := doc.Info; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } @@ -76,7 +79,7 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid paths: %v", e) } - if v := value.Paths; v != nil { + if v := doc.Paths; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } @@ -87,7 +90,7 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid security: %v", e) } - if v := value.Security; v != nil { + if v := doc.Security; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } @@ -96,7 +99,7 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid servers: %v", e) } - if v := value.Servers; v != nil { + if v := doc.Servers; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } @@ -105,7 +108,7 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid tags: %w", e) } - if v := value.Tags; v != nil { + if v := doc.Tags; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } @@ -114,7 +117,7 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid external docs: %w", e) } - if v := value.ExternalDocs; v != nil { + if v := doc.ExternalDocs; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } diff --git a/openapi3/operation.go b/openapi3/operation.go index 29e70c774..cb9644a77 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -56,14 +56,17 @@ func NewOperation() *Operation { return &Operation{} } +// MarshalJSON returns the JSON encoding of Operation. func (operation *Operation) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(operation) } +// UnmarshalJSON sets Operation to a copy of data. func (operation *Operation) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, operation) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (operation Operation) JSONLookup(token string) (interface{}, error) { switch token { case "requestBody": @@ -122,25 +125,26 @@ func (operation *Operation) AddResponse(status int, response *Response) { } } -func (value *Operation) Validate(ctx context.Context) error { - if v := value.Parameters; v != nil { +// Validate returns an error if Operation does not comply with the OpenAPI spec. +func (operation *Operation) Validate(ctx context.Context) error { + if v := operation.Parameters; v != nil { if err := v.Validate(ctx); err != nil { return err } } - if v := value.RequestBody; v != nil { + if v := operation.RequestBody; v != nil { if err := v.Validate(ctx); err != nil { return err } } - if v := value.Responses; v != nil { + if v := operation.Responses; v != nil { if err := v.Validate(ctx); err != nil { return err } } else { return errors.New("value of responses must be an object") } - if v := value.ExternalDocs; v != nil { + if v := operation.ExternalDocs; v != nil { if err := v.Validate(ctx); err != nil { return fmt.Errorf("invalid external docs: %w", err) } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index e283a98fb..b32898c65 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -14,6 +14,7 @@ type ParametersMap map[string]*ParameterRef var _ jsonpointer.JSONPointable = (*ParametersMap)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (p ParametersMap) JSONLookup(token string) (interface{}, error) { ref, ok := p[token] if ref == nil || ok == false { @@ -31,6 +32,7 @@ type Parameters []*ParameterRef var _ jsonpointer.JSONPointable = (*Parameters)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (p Parameters) JSONLookup(token string) (interface{}, error) { index, err := strconv.Atoi(token) if err != nil { @@ -64,9 +66,10 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter { return nil } -func (value Parameters) Validate(ctx context.Context) error { +// Validate returns an error if Parameters does not comply with the OpenAPI spec. +func (parameters Parameters) Validate(ctx context.Context) error { dupes := make(map[string]struct{}) - for _, item := range value { + for _, item := range parameters { if v := item.Value; v != nil { key := v.In + ":" + v.Name if _, ok := dupes[key]; ok { @@ -161,50 +164,53 @@ func (parameter *Parameter) WithSchema(value *Schema) *Parameter { return parameter } +// MarshalJSON returns the JSON encoding of Parameter. func (parameter *Parameter) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(parameter) } +// UnmarshalJSON sets Parameter to a copy of data. func (parameter *Parameter) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, parameter) } -func (value Parameter) JSONLookup(token string) (interface{}, error) { +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable +func (parameter Parameter) JSONLookup(token string) (interface{}, error) { switch token { case "schema": - if value.Schema != nil { - if value.Schema.Ref != "" { - return &Ref{Ref: value.Schema.Ref}, nil + if parameter.Schema != nil { + if parameter.Schema.Ref != "" { + return &Ref{Ref: parameter.Schema.Ref}, nil } - return value.Schema.Value, nil + return parameter.Schema.Value, nil } case "name": - return value.Name, nil + return parameter.Name, nil case "in": - return value.In, nil + return parameter.In, nil case "description": - return value.Description, nil + return parameter.Description, nil case "style": - return value.Style, nil + return parameter.Style, nil case "explode": - return value.Explode, nil + return parameter.Explode, nil case "allowEmptyValue": - return value.AllowEmptyValue, nil + return parameter.AllowEmptyValue, nil case "allowReserved": - return value.AllowReserved, nil + return parameter.AllowReserved, nil case "deprecated": - return value.Deprecated, nil + return parameter.Deprecated, nil case "required": - return value.Required, nil + return parameter.Required, nil case "example": - return value.Example, nil + return parameter.Example, nil case "examples": - return value.Examples, nil + return parameter.Examples, nil case "content": - return value.Content, nil + return parameter.Content, nil } - v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token) return v, err } @@ -238,11 +244,12 @@ func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error) } } -func (value *Parameter) Validate(ctx context.Context) error { - if value.Name == "" { +// Validate returns an error if Parameter does not comply with the OpenAPI spec. +func (parameter *Parameter) Validate(ctx context.Context) error { + if parameter.Name == "" { return errors.New("parameter name can't be blank") } - in := value.In + in := parameter.In switch in { case ParameterInPath, @@ -250,60 +257,60 @@ func (value *Parameter) Validate(ctx context.Context) error { ParameterInHeader, ParameterInCookie: default: - return fmt.Errorf("parameter can't have 'in' value %q", value.In) + return fmt.Errorf("parameter can't have 'in' value %q", parameter.In) } - if in == ParameterInPath && !value.Required { - return fmt.Errorf("path parameter %q must be required", value.Name) + if in == ParameterInPath && !parameter.Required { + return fmt.Errorf("path parameter %q must be required", parameter.Name) } // Validate a parameter's serialization method. - sm, err := value.SerializationMethod() + sm, err := parameter.SerializationMethod() if err != nil { return err } var smSupported bool switch { - case value.In == ParameterInPath && sm.Style == SerializationSimple && !sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationSimple && sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationLabel && !sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationLabel && sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationMatrix && !sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationMatrix && sm.Explode, - - value.In == ParameterInQuery && sm.Style == SerializationForm && sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationForm && !sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && !sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && !sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationDeepObject && sm.Explode, - - value.In == ParameterInHeader && sm.Style == SerializationSimple && !sm.Explode, - value.In == ParameterInHeader && sm.Style == SerializationSimple && sm.Explode, - - value.In == ParameterInCookie && sm.Style == SerializationForm && !sm.Explode, - value.In == ParameterInCookie && sm.Style == SerializationForm && sm.Explode: + case parameter.In == ParameterInPath && sm.Style == SerializationSimple && !sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationSimple && sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationLabel && !sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationLabel && sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationMatrix && !sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationMatrix && sm.Explode, + + parameter.In == ParameterInQuery && sm.Style == SerializationForm && sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationForm && !sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && !sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && !sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationDeepObject && sm.Explode, + + parameter.In == ParameterInHeader && sm.Style == SerializationSimple && !sm.Explode, + parameter.In == ParameterInHeader && sm.Style == SerializationSimple && sm.Explode, + + parameter.In == ParameterInCookie && sm.Style == SerializationForm && !sm.Explode, + parameter.In == ParameterInCookie && sm.Style == SerializationForm && sm.Explode: smSupported = true } if !smSupported { e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in) - return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, e) + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) } - if (value.Schema == nil) == (value.Content == nil) { + if (parameter.Schema == nil) == (parameter.Content == nil) { e := errors.New("parameter must contain exactly one of content and schema") - return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, e) + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) } - if schema := value.Schema; schema != nil { + if schema := parameter.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { - return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, err) + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err) } } - if content := value.Content; content != nil { + if content := parameter.Content; content != nil { if err := content.Validate(ctx); err != nil { - return fmt.Errorf("parameter %q content is invalid: %v", value.Name, err) + return fmt.Errorf("parameter %q content is invalid: %v", parameter.Name, err) } } return nil diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 4473d639d..6a8cc7336 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -29,10 +29,12 @@ type PathItem struct { Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` } +// MarshalJSON returns the JSON encoding of PathItem. func (pathItem *PathItem) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(pathItem) } +// UnmarshalJSON sets PathItem to a copy of data. func (pathItem *PathItem) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, pathItem) } @@ -119,8 +121,9 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { } } -func (value *PathItem) Validate(ctx context.Context) error { - for _, operation := range value.Operations() { +// Validate returns an error if PathItem does not comply with the OpenAPI spec. +func (pathItem *PathItem) Validate(ctx context.Context) error { + for _, operation := range pathItem.Operations() { if err := operation.Validate(ctx); err != nil { return err } diff --git a/openapi3/paths.go b/openapi3/paths.go index 24ab5f300..b4ebe582a 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -10,16 +10,17 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object type Paths map[string]*PathItem -func (value Paths) Validate(ctx context.Context) error { +// Validate returns an error if Paths does not comply with the OpenAPI spec. +func (paths Paths) Validate(ctx context.Context) error { normalizedPaths := make(map[string]string) - for path, pathItem := range value { + for path, pathItem := range paths { if path == "" || path[0] != '/' { return fmt.Errorf("path %q does not start with a forward slash (/)", path) } if pathItem == nil { - value[path] = &PathItem{} - pathItem = value[path] + paths[path] = &PathItem{} + pathItem = paths[path] } normalizedPath, _, varsInPath := normalizeTemplatedPath(path) @@ -84,7 +85,7 @@ func (value Paths) Validate(ctx context.Context) error { } } - if err := value.validateUniqueOperationIDs(); err != nil { + if err := paths.validateUniqueOperationIDs(); err != nil { return err } @@ -120,9 +121,9 @@ func (paths Paths) Find(key string) *PathItem { return nil } -func (value Paths) validateUniqueOperationIDs() error { +func (paths Paths) validateUniqueOperationIDs() error { operationIDs := make(map[string]string) - for urlPath, pathItem := range value { + for urlPath, pathItem := range paths { if pathItem == nil { continue } diff --git a/openapi3/refs.go b/openapi3/refs.go index 333cd1740..a706834ca 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -22,14 +22,17 @@ type CallbackRef struct { var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) +// MarshalJSON returns the JSON encoding of CallbackRef. func (value *CallbackRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets CallbackRef to a copy of data. func (value *CallbackRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if CallbackRef does not comply with the OpenAPI spec. func (value *CallbackRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -37,6 +40,7 @@ func (value *CallbackRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (value CallbackRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil @@ -55,14 +59,17 @@ type ExampleRef struct { var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) +// MarshalJSON returns the JSON encoding of ExampleRef. func (value *ExampleRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets ExampleRef to a copy of data. func (value *ExampleRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if ExampleRef does not comply with the OpenAPI spec. func (value *ExampleRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -70,6 +77,7 @@ func (value *ExampleRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (value ExampleRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil @@ -88,14 +96,17 @@ type HeaderRef struct { var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) +// MarshalJSON returns the JSON encoding of HeaderRef. func (value *HeaderRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets HeaderRef to a copy of data. func (value *HeaderRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if HeaderRef does not comply with the OpenAPI spec. func (value *HeaderRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -103,6 +114,7 @@ func (value *HeaderRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (value HeaderRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil @@ -119,14 +131,17 @@ type LinkRef struct { Value *Link } +// MarshalJSON returns the JSON encoding of LinkRef. func (value *LinkRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets LinkRef to a copy of data. func (value *LinkRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if LinkRef does not comply with the OpenAPI spec. func (value *LinkRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -143,14 +158,17 @@ type ParameterRef struct { var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) +// MarshalJSON returns the JSON encoding of ParameterRef. func (value *ParameterRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets ParameterRef to a copy of data. func (value *ParameterRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if ParameterRef does not comply with the OpenAPI spec. func (value *ParameterRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -158,6 +176,7 @@ func (value *ParameterRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (value ParameterRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil @@ -176,14 +195,17 @@ type ResponseRef struct { var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) +// MarshalJSON returns the JSON encoding of ResponseRef. func (value *ResponseRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets ResponseRef to a copy of data. func (value *ResponseRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if ResponseRef does not comply with the OpenAPI spec. func (value *ResponseRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -191,6 +213,7 @@ func (value *ResponseRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (value ResponseRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil @@ -209,14 +232,17 @@ type RequestBodyRef struct { var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) +// MarshalJSON returns the JSON encoding of RequestBodyRef. func (value *RequestBodyRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets RequestBodyRef to a copy of data. func (value *RequestBodyRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if RequestBodyRef does not comply with the OpenAPI spec. func (value *RequestBodyRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -224,6 +250,7 @@ func (value *RequestBodyRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil @@ -249,14 +276,17 @@ func NewSchemaRef(ref string, value *Schema) *SchemaRef { } } +// MarshalJSON returns the JSON encoding of SchemaRef. func (value *SchemaRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets SchemaRef to a copy of data. func (value *SchemaRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if SchemaRef does not comply with the OpenAPI spec. func (value *SchemaRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -264,6 +294,7 @@ func (value *SchemaRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (value SchemaRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil @@ -282,14 +313,17 @@ type SecuritySchemeRef struct { var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) +// MarshalJSON returns the JSON encoding of SecuritySchemeRef. func (value *SecuritySchemeRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } +// UnmarshalJSON sets SecuritySchemeRef to a copy of data. func (value *SecuritySchemeRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate returns an error if SecuritySchemeRef does not comply with the OpenAPI spec. func (value *SecuritySchemeRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -297,6 +331,7 @@ func (value *SecuritySchemeRef) Validate(ctx context.Context) error { return foundUnresolvedRef(value.Ref) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (value SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 0be098c0b..559eb7122 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -13,6 +13,7 @@ type RequestBodies map[string]*RequestBodyRef var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (r RequestBodies) JSONLookup(token string) (interface{}, error) { ref, ok := r[token] if ok == false { @@ -92,17 +93,20 @@ func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType { return m[mediaType] } +// MarshalJSON returns the JSON encoding of RequestBody. func (requestBody *RequestBody) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(requestBody) } +// UnmarshalJSON sets RequestBody to a copy of data. func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, requestBody) } -func (value *RequestBody) Validate(ctx context.Context) error { - if value.Content == nil { +// Validate returns an error if RequestBody does not comply with the OpenAPI spec. +func (requestBody *RequestBody) Validate(ctx context.Context) error { + if requestBody.Content == nil { return errors.New("content of the request body is required") } - return value.Content.Validate(ctx) + return requestBody.Content.Validate(ctx) } diff --git a/openapi3/response.go b/openapi3/response.go index 8e22698f6..23e2f4449 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -30,11 +30,12 @@ func (responses Responses) Get(status int) *ResponseRef { return responses[strconv.FormatInt(int64(status), 10)] } -func (value Responses) Validate(ctx context.Context) error { - if len(value) == 0 { +// Validate returns an error if Responses does not comply with the OpenAPI spec. +func (responses Responses) Validate(ctx context.Context) error { + if len(responses) == 0 { return errors.New("the responses object MUST contain at least one response code") } - for _, v := range value { + for _, v := range responses { if err := v.Validate(ctx); err != nil { return err } @@ -42,6 +43,7 @@ func (value Responses) Validate(ctx context.Context) error { return nil } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (responses Responses) JSONLookup(token string) (interface{}, error) { ref, ok := responses[token] if ok == false { @@ -89,31 +91,34 @@ func (response *Response) WithJSONSchemaRef(schema *SchemaRef) *Response { return response } +// MarshalJSON returns the JSON encoding of Response. func (response *Response) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(response) } +// UnmarshalJSON sets Response to a copy of data. func (response *Response) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, response) } -func (value *Response) Validate(ctx context.Context) error { - if value.Description == nil { +// Validate returns an error if Response does not comply with the OpenAPI spec. +func (response *Response) Validate(ctx context.Context) error { + if response.Description == nil { return errors.New("a short description of the response is required") } - if content := value.Content; content != nil { + if content := response.Content; content != nil { if err := content.Validate(ctx); err != nil { return err } } - for _, header := range value.Headers { + for _, header := range response.Headers { if err := header.Validate(ctx); err != nil { return err } } - for _, link := range value.Links { + for _, link := range response.Links { if err := link.Validate(ctx); err != nil { return err } diff --git a/openapi3/schema.go b/openapi3/schema.go index ee21bc21b..45350eced 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -67,6 +67,7 @@ type Schemas map[string]*SchemaRef var _ jsonpointer.JSONPointable = (*Schemas)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (s Schemas) JSONLookup(token string) (interface{}, error) { ref, ok := s[token] if ref == nil || ok == false { @@ -83,6 +84,7 @@ type SchemaRefs []*SchemaRef var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { i, err := strconv.ParseUint(token, 10, 64) if err != nil { @@ -164,14 +166,17 @@ func NewSchema() *Schema { return &Schema{} } +// MarshalJSON returns the JSON encoding of Schema. func (schema *Schema) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(schema) } +// UnmarshalJSON sets Schema to a copy of data. func (schema *Schema) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, schema) } +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (schema Schema) JSONLookup(token string) (interface{}, error) { switch token { case "additionalProperties": @@ -588,8 +593,9 @@ func (schema *Schema) IsEmpty() bool { return true } -func (value *Schema) Validate(ctx context.Context) error { - return value.validate(ctx, []*Schema{}) +// Validate returns an error if Schema does not comply with the OpenAPI spec. +func (schema *Schema) Validate(ctx context.Context) error { + return schema.validate(ctx, []*Schema{}) } func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index df6b6b2d1..42d832552 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -15,8 +15,9 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) * return srs } -func (value SecurityRequirements) Validate(ctx context.Context) error { - for _, item := range value { +// Validate returns an error if SecurityRequirements does not comply with the OpenAPI spec. +func (srs SecurityRequirements) Validate(ctx context.Context) error { + for _, item := range srs { if err := item.Validate(ctx); err != nil { return err } @@ -40,6 +41,7 @@ func (security SecurityRequirement) Authenticate(provider string, scopes ...stri return security } -func (value SecurityRequirement) Validate(ctx context.Context) error { +// Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec. +func (security *SecurityRequirement) Validate(ctx context.Context) error { return nil } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 9b89fb950..10057de6b 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -11,6 +11,7 @@ import ( type SecuritySchemes map[string]*SecuritySchemeRef +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) { ref, ok := s[token] if ref == nil || ok == false { @@ -67,10 +68,12 @@ func NewJWTSecurityScheme() *SecurityScheme { } } +// MarshalJSON returns the JSON encoding of SecurityScheme. func (ss *SecurityScheme) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(ss) } +// UnmarshalJSON sets SecurityScheme to a copy of data. func (ss *SecurityScheme) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, ss) } @@ -105,15 +108,16 @@ func (ss *SecurityScheme) WithBearerFormat(value string) *SecurityScheme { return ss } -func (value *SecurityScheme) Validate(ctx context.Context) error { +// Validate returns an error if SecurityScheme does not comply with the OpenAPI spec. +func (ss *SecurityScheme) Validate(ctx context.Context) error { hasIn := false hasBearerFormat := false hasFlow := false - switch value.Type { + switch ss.Type { case "apiKey": hasIn = true case "http": - scheme := value.Scheme + scheme := ss.Scheme switch scheme { case "bearer": hasBearerFormat = true @@ -124,46 +128,46 @@ func (value *SecurityScheme) Validate(ctx context.Context) error { case "oauth2": hasFlow = true case "openIdConnect": - if value.OpenIdConnectUrl == "" { - return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", value.Name) + if ss.OpenIdConnectUrl == "" { + return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", ss.Name) } default: - return fmt.Errorf("security scheme 'type' can't be %q", value.Type) + return fmt.Errorf("security scheme 'type' can't be %q", ss.Type) } // Validate "in" and "name" if hasIn { - switch value.In { + switch ss.In { case "query", "header", "cookie": default: - return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", value.In) + return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", ss.In) } - if value.Name == "" { + if ss.Name == "" { return errors.New("security scheme of type 'apiKey' should have 'name'") } - } else if len(value.In) > 0 { - return fmt.Errorf("security scheme of type %q can't have 'in'", value.Type) - } else if len(value.Name) > 0 { + } else if len(ss.In) > 0 { + return fmt.Errorf("security scheme of type %q can't have 'in'", ss.Type) + } else if len(ss.Name) > 0 { return errors.New("security scheme of type 'apiKey' can't have 'name'") } // Validate "format" // "bearerFormat" is an arbitrary string so we only check if the scheme supports it - if !hasBearerFormat && len(value.BearerFormat) > 0 { - return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", value.Type) + if !hasBearerFormat && len(ss.BearerFormat) > 0 { + return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", ss.Type) } // Validate "flow" if hasFlow { - flow := value.Flows + flow := ss.Flows if flow == nil { - return fmt.Errorf("security scheme of type %q should have 'flows'", value.Type) + return fmt.Errorf("security scheme of type %q should have 'flows'", ss.Type) } if err := flow.Validate(ctx); err != nil { return fmt.Errorf("security scheme 'flow' is invalid: %v", err) } - } else if value.Flows != nil { - return fmt.Errorf("security scheme of type %q can't have 'flows'", value.Type) + } else if ss.Flows != nil { + return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type) } return nil } @@ -188,14 +192,17 @@ const ( oAuthFlowAuthorizationCode ) +// MarshalJSON returns the JSON encoding of OAuthFlows. func (flows *OAuthFlows) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(flows) } +// UnmarshalJSON sets OAuthFlows to a copy of data. func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flows) } +// Validate returns an error if OAuthFlows does not comply with the OpenAPI spec. func (flows *OAuthFlows) Validate(ctx context.Context) error { if v := flows.Implicit; v != nil { return v.Validate(ctx, oAuthFlowTypeImplicit) @@ -223,14 +230,17 @@ type OAuthFlow struct { Scopes map[string]string `json:"scopes" yaml:"scopes"` } +// MarshalJSON returns the JSON encoding of OAuthFlow. func (flow *OAuthFlow) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(flow) } +// UnmarshalJSON sets OAuthFlow to a copy of data. func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flow) } +// Validate returns an error if OAuthFlow does not comply with the OpenAPI spec. func (flow *OAuthFlow) Validate(ctx context.Context, typ oAuthFlowType) error { if typ == oAuthFlowAuthorizationCode || typ == oAuthFlowTypeImplicit { if v := flow.AuthorizationURL; v == "" { diff --git a/openapi3/server.go b/openapi3/server.go index 94092a6e6..478f8ffb7 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -14,9 +14,9 @@ import ( // Servers is specified by OpenAPI/Swagger standard version 3. type Servers []*Server -// Validate ensures servers are per the OpenAPIv3 specification. -func (value Servers) Validate(ctx context.Context) error { - for _, v := range value { +// Validate returns an error if Servers does not comply with the OpenAPI spec. +func (servers Servers) Validate(ctx context.Context) error { + for _, v := range servers { if err := v.Validate(ctx); err != nil { return err } @@ -48,10 +48,12 @@ type Server struct { Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` } +// MarshalJSON returns the JSON encoding of Server. func (server *Server) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(server) } +// UnmarshalJSON sets Server to a copy of data. func (server *Server) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, server) } @@ -127,19 +129,20 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) { return params, input, true } -func (value *Server) Validate(ctx context.Context) (err error) { - if value.URL == "" { +// Validate returns an error if Server does not comply with the OpenAPI spec. +func (server *Server) Validate(ctx context.Context) (err error) { + if server.URL == "" { return errors.New("value of url must be a non-empty string") } - opening, closing := strings.Count(value.URL, "{"), strings.Count(value.URL, "}") + opening, closing := strings.Count(server.URL, "{"), strings.Count(server.URL, "}") if opening != closing { return errors.New("server URL has mismatched { and }") } - if opening != len(value.Variables) { + if opening != len(server.Variables) { return errors.New("server has undeclared variables") } - for name, v := range value.Variables { - if !strings.Contains(value.URL, fmt.Sprintf("{%s}", name)) { + for name, v := range server.Variables { + if !strings.Contains(server.URL, fmt.Sprintf("{%s}", name)) { return errors.New("server has undeclared variables") } if err = v.Validate(ctx); err != nil { @@ -159,17 +162,20 @@ type ServerVariable struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` } +// MarshalJSON returns the JSON encoding of ServerVariable. func (serverVariable *ServerVariable) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(serverVariable) } +// UnmarshalJSON sets ServerVariable to a copy of data. func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, serverVariable) } -func (value *ServerVariable) Validate(ctx context.Context) error { - if value.Default == "" { - data, err := value.MarshalJSON() +// Validate returns an error if ServerVariable does not comply with the OpenAPI spec. +func (serverVariable *ServerVariable) Validate(ctx context.Context) error { + if serverVariable.Default == "" { + data, err := serverVariable.MarshalJSON() if err != nil { return err } diff --git a/openapi3/tag.go b/openapi3/tag.go index 6aa9a1ea2..8fb5ac36c 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -19,6 +19,7 @@ func (tags Tags) Get(name string) *Tag { return nil } +// Validate returns an error if Tags does not comply with the OpenAPI spec. func (tags Tags) Validate(ctx context.Context) error { for _, v := range tags { if err := v.Validate(ctx); err != nil { @@ -38,14 +39,17 @@ type Tag struct { ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } +// MarshalJSON returns the JSON encoding of Tag. func (t *Tag) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(t) } +// UnmarshalJSON sets Tag to a copy of data. func (t *Tag) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, t) } +// Validate returns an error if Tag does not comply with the OpenAPI spec. func (t *Tag) Validate(ctx context.Context) error { if v := t.ExternalDocs; v != nil { if err := v.Validate(ctx); err != nil { diff --git a/openapi3/xml.go b/openapi3/xml.go index 8fd2abdee..03686ad9a 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -18,14 +18,17 @@ type XML struct { Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"` } -func (value *XML) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(value) +// MarshalJSON returns the JSON encoding of XML. +func (xml *XML) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(xml) } -func (value *XML) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +// UnmarshalJSON sets XML to a copy of data. +func (xml *XML) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, xml) } -func (value *XML) Validate(ctx context.Context) error { +// Validate returns an error if XML does not comply with the OpenAPI spec. +func (xml *XML) Validate(ctx context.Context) error { return nil // TODO } From c35b46e177d3c2f0d5b54782dda7bfa387c5055f Mon Sep 17 00:00:00 2001 From: Christoph Petrausch <263448+hikhvar@users.noreply.github.com> Date: Mon, 30 May 2022 18:10:27 +0200 Subject: [PATCH 154/260] Internalize parameter references in the path as well (#540) --- openapi3/internalize_refs.go | 4 ++++ openapi3/testdata/recursiveRef/parameters/number.yml | 4 ++++ openapi3/testdata/recursiveRef/paths/foo.yml | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 openapi3/testdata/recursiveRef/parameters/number.yml diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index 3a993bfb4..3a6cabb1a 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -286,6 +286,10 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso // inline full operations ops.Ref = "" + for _, param := range ops.Parameters { + doc.addParameterToSpec(param, refNameResolver) + } + for _, op := range ops.Operations() { doc.addRequestBodyToSpec(op.RequestBody, refNameResolver) if op.RequestBody != nil && op.RequestBody.Value != nil { diff --git a/openapi3/testdata/recursiveRef/parameters/number.yml b/openapi3/testdata/recursiveRef/parameters/number.yml new file mode 100644 index 000000000..29f0f2640 --- /dev/null +++ b/openapi3/testdata/recursiveRef/parameters/number.yml @@ -0,0 +1,4 @@ +name: someNumber +in: query +schema: + type: string diff --git a/openapi3/testdata/recursiveRef/paths/foo.yml b/openapi3/testdata/recursiveRef/paths/foo.yml index 1653c7ac7..43e03b7ab 100644 --- a/openapi3/testdata/recursiveRef/paths/foo.yml +++ b/openapi3/testdata/recursiveRef/paths/foo.yml @@ -1,3 +1,5 @@ +parameters: + - $ref: ../parameters/number.yml get: responses: "200": From 770fcc5473f2dfa270d12fba59836523f3ddb195 Mon Sep 17 00:00:00 2001 From: K Zhang Date: Mon, 30 May 2022 12:13:22 -0400 Subject: [PATCH 155/260] fix bad error message on invalid value parse on query parameter (#541) Co-authored-by: Kanda --- openapi3filter/req_resp_decoder.go | 6 +++--- openapi3filter/validation_error_test.go | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 0408d8da3..94edf75a1 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -795,19 +795,19 @@ func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) case "integer": v, err := strconv.ParseFloat(raw, 64) if err != nil { - return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid integer", Cause: err} + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} } return v, nil case "number": v, err := strconv.ParseFloat(raw, 64) if err != nil { - return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid number", Cause: err} + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} } return v, nil case "boolean": v, err := strconv.ParseBool(raw) if err != nil { - return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid number", Cause: err} + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} } return v, nil case "string": diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index aae26e56b..10d46f855 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -187,7 +187,7 @@ func getValidationTests(t *testing.T) []*validationTest { Title: `parameter "status" in query is required`}, }, { - name: "error - wrong query string parameter type", + name: "error - wrong query string parameter type as integer", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByIds?ids=1,notAnInt", nil), }, @@ -195,8 +195,7 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrParamIn: "query", // This is a nested ParseError. The outer error is a KindOther with no details. // So we'd need to look at the inner one which is a KindInvalidFormat. So just check the error body. - wantErrBody: `parameter "ids" in query has an error: path 1: value notAnInt: an invalid integer: ` + - "strconv.ParseFloat: parsing \"notAnInt\": invalid syntax", + wantErrBody: `parameter "ids" in query has an error: path 1: value notAnInt: an invalid integer: invalid syntax`, // TODO: Should we treat query params of the wrong type like a 404 instead of a 400? wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "ids" in query is invalid: notAnInt is an invalid integer`}, From 121fc062e90bb2526bfbc14f858d51a289ad98cb Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 30 May 2022 18:16:49 +0200 Subject: [PATCH 156/260] Follow up to #540 with more tests (#549) --- openapi3/internalize_refs_test.go | 24 +++- .../testdata/callbacks.yml.internalized.yml | 131 ++++++++++++++++++ .../recursiveRef/openapi.yml.internalized.yml | 68 +++++++++ openapi3/testdata/spec.yaml.internalized.yml | 36 +++++ .../testref.openapi.yml.internalized.yml | 19 +++ 5 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 openapi3/testdata/callbacks.yml.internalized.yml create mode 100644 openapi3/testdata/recursiveRef/openapi.yml.internalized.yml create mode 100644 openapi3/testdata/spec.yaml.internalized.yml create mode 100644 openapi3/testdata/testref.openapi.yml.internalized.yml diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go index d6264d428..b1ca846d2 100644 --- a/openapi3/internalize_refs_test.go +++ b/openapi3/internalize_refs_test.go @@ -2,6 +2,7 @@ package openapi3 import ( "context" + "io/ioutil" "regexp" "testing" @@ -9,8 +10,10 @@ import ( ) func TestInternalizeRefs(t *testing.T) { - var regexpRef = regexp.MustCompile(`"\$ref":`) - var regexpRefInternal = regexp.MustCompile(`"\$ref":"\#`) + ctx := context.Background() + + regexpRef := regexp.MustCompile(`"\$ref":`) + regexpRefInternal := regexp.MustCompile(`"\$ref":"#`) tests := []struct { filename string @@ -28,13 +31,15 @@ func TestInternalizeRefs(t *testing.T) { sl.IsExternalRefsAllowed = true doc, err := sl.LoadFromFile(test.filename) require.NoError(t, err, "loading test file") + err = doc.Validate(ctx) + require.NoError(t, err, "validating spec") // Internalize the references - doc.InternalizeRefs(context.Background(), DefaultRefNameResolver) + doc.InternalizeRefs(ctx, nil) // Validate the internalized spec - err = doc.Validate(context.Background()) - require.Nil(t, err, "validating internalized spec") + err = doc.Validate(ctx) + require.NoError(t, err, "validating internalized spec") data, err := doc.MarshalJSON() require.NoError(t, err, "marshalling internalized spec") @@ -48,8 +53,13 @@ func TestInternalizeRefs(t *testing.T) { // load from data, but with the path set to the current directory doc2, err := sl.LoadFromData(data) require.NoError(t, err, "reloading spec") - err = doc2.Validate(context.Background()) - require.Nil(t, err, "validating reloaded spec") + err = doc2.Validate(ctx) + require.NoError(t, err, "validating reloaded spec") + + // compare with expected + data0, err := ioutil.ReadFile(test.filename + ".internalized.yml") + require.NoError(t, err) + require.JSONEq(t, string(data), string(data0)) }) } } diff --git a/openapi3/testdata/callbacks.yml.internalized.yml b/openapi3/testdata/callbacks.yml.internalized.yml new file mode 100644 index 000000000..866cb5ca4 --- /dev/null +++ b/openapi3/testdata/callbacks.yml.internalized.yml @@ -0,0 +1,131 @@ +{ + "components": { + "callbacks": { + "MyCallbackEvent": { + "{$request.query.queryUrl}": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SomeOtherPayload" + } + } + }, + "description": "Callback payload" + }, + "responses": { + "200": { + "description": "callback successfully processed" + } + } + } + } + } + }, + "schemas": { + "SomeOtherPayload": { + "type": "boolean" + }, + "SomePayload": { + "type": "object" + } + } + }, + "info": { + "title": "Callback refd", + "version": "1.2.3" + }, + "openapi": "3.1.0", + "paths": { + "/other": { + "post": { + "callbacks": { + "myEvent": { + "$ref": "#/components/callbacks/MyCallbackEvent" + } + }, + "parameters": [ + { + "description": "bla\nbla\nbla\n", + "in": "query", + "name": "queryUrl", + "required": true, + "schema": { + "example": "https://example.com", + "format": "uri", + "type": "string" + } + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "" + } + } + } + }, + "/trans": { + "post": { + "callbacks": { + "transactionCallback": { + "http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SomePayload" + } + } + }, + "description": "Callback payload" + }, + "responses": { + "200": { + "description": "callback successfully processed" + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "email": { + "format": "email" + }, + "id": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "subscription successfully created" + } + } + } + } + } +} diff --git a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml new file mode 100644 index 000000000..d1260eb14 --- /dev/null +++ b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml @@ -0,0 +1,68 @@ +{ + "components": { + "parameters": { + "number": { + "in": "query", + "name": "someNumber", + "schema": { + "type": "string" + } + } + }, + "schemas": { + "Bar": { + "example": "bar", + "type": "string" + }, + "Foo": { + "properties": { + "bar": { + "$ref": "#/components/schemas/Bar" + } + }, + "type": "object" + }, + "Foo2": { + "properties": { + "foo": { + "$ref": "#/components/schemas/Foo" + } + }, + "type": "object" + } + } + }, + "info": { + "title": "Recursive refs example", + "version": "1.0" + }, + "openapi": "3.0.3", + "paths": { + "/foo": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "foo2": { + "$ref": "#/components/schemas/Foo2" + } + }, + "type": "object" + } + } + }, + "description": "OK" + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/number" + } + ] + } + } +} diff --git a/openapi3/testdata/spec.yaml.internalized.yml b/openapi3/testdata/spec.yaml.internalized.yml new file mode 100644 index 000000000..feca4a00c --- /dev/null +++ b/openapi3/testdata/spec.yaml.internalized.yml @@ -0,0 +1,36 @@ +{ + "components": { + "schemas": { + "Test": { + "properties": { + "test": { + "$ref": "#/components/schemas/b" + } + }, + "type": "object" + }, + "a": { + "type": "string" + }, + "b": { + "description": "I use a local reference.", + "properties": { + "name": { + "$ref": "#/components/schemas/a" + } + }, + "type": "object" + } + } + }, + "info": { + "license": { + "name": "MIT" + }, + "title": "Some Swagger", + "version": "1.0.0" + }, + "openapi": "3.0.1", + "paths": { + } +} diff --git a/openapi3/testdata/testref.openapi.yml.internalized.yml b/openapi3/testdata/testref.openapi.yml.internalized.yml new file mode 100644 index 000000000..e35a50041 --- /dev/null +++ b/openapi3/testdata/testref.openapi.yml.internalized.yml @@ -0,0 +1,19 @@ +{ + "components": { + "schemas": { + "AnotherTestSchema": { + "type": "string" + }, + "CustomTestSchema": { + "type": "string" + } + } + }, + "info": { + "title": "OAI Specification w/ refs in YAML", + "version": "1" + }, + "openapi": "3.0.0", + "paths": { + } +} From 7f8f7680211bfc317d5b5b48e21b1f53fe65850a Mon Sep 17 00:00:00 2001 From: Nic Date: Tue, 31 May 2022 00:35:29 +0800 Subject: [PATCH 157/260] feat: handling `default` in request body and parameter schema (#544) * wip setting defaults for #206 Signed-off-by: Pierre Fenoll * introduce body encoders Signed-off-by: Pierre Fenoll * re-encode only when needed Signed-off-by: Pierre Fenoll * set default for parameter and add more test cases Co-authored-by: Pierre Fenoll --- openapi3/schema.go | 13 + openapi3/schema_validation_settings.go | 12 + openapi3filter/req_resp_decoder.go | 14 +- openapi3filter/req_resp_decoder_test.go | 6 +- openapi3filter/req_resp_encoder.go | 27 + openapi3filter/validate_request.go | 45 +- openapi3filter/validate_response.go | 2 +- openapi3filter/validate_set_default_test.go | 561 ++++++++++++++++++++ 8 files changed, 669 insertions(+), 11 deletions(-) create mode 100644 openapi3filter/req_resp_encoder.go create mode 100644 openapi3filter/validate_set_default_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 45350eced..214f4f635 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1358,6 +1358,19 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return schema.expectedType(settings, TypeObject) } + if settings.asreq || settings.asrep { + for propName, propSchema := range schema.Properties { + if value[propName] == nil { + if dlft := propSchema.Value.Default; dlft != nil { + value[propName] = dlft + if f := settings.defaultsSet; f != nil { + settings.onceSettingDefaults.Do(f) + } + } + } + } + } + var me MultiError // "properties" diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 71db5f237..cb4c142a4 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -1,5 +1,9 @@ package openapi3 +import ( + "sync" +) + // SchemaValidationOption describes options a user has when validating request / response bodies. type SchemaValidationOption func(*schemaValidationSettings) @@ -7,6 +11,9 @@ type schemaValidationSettings struct { failfast bool multiError bool asreq, asrep bool // exclusive (XOR) fields + + onceSettingDefaults sync.Once + defaultsSet func() } // FailFast returns schema validation errors quicker. @@ -25,6 +32,11 @@ func VisitAsResponse() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true } } +// DefaultsSet executes the given callback (once) IFF schema validation set default values. +func DefaultsSet(f func()) SchemaValidationOption { + return func(s *schemaValidationSettings) { s.defaultsSet = f } +} + func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings { settings := &schemaValidationSettings{} for _, opt := range opts { diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 94edf75a1..1b5f0c5d5 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -868,7 +868,11 @@ const prefixUnsupportedCT = "unsupported content type" // decodeBody returns a decoded body. // The function returns ParseError when a body is invalid. -func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { +func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) ( + string, + interface{}, + error, +) { contentType := header.Get(headerCT) if contentType == "" { if _, ok := body.(*multipart.Part); ok { @@ -878,16 +882,16 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, mediaType := parseMediaType(contentType) decoder, ok := bodyDecoders[mediaType] if !ok { - return nil, &ParseError{ + return "", nil, &ParseError{ Kind: KindUnsupportedFormat, Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType), } } value, err := decoder(body, header, schema, encFn) if err != nil { - return nil, err + return "", nil, err } - return value, nil + return mediaType, value, nil } func init() { @@ -1036,7 +1040,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } var value interface{} - if value, err = decodeBody(part, http.Header(part.Header), valueSchema, subEncFn); err != nil { + if _, value, err = decodeBody(part, http.Header(part.Header), valueSchema, subEncFn); err != nil { if v, ok := err.(*ParseError); ok { return nil, &ParseError{path: []interface{}{name}, Cause: v} } diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index de93547b5..c733bd028 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -1280,7 +1280,7 @@ func TestDecodeBody(t *testing.T) { } return tc.encoding[name] } - got, err := decodeBody(tc.body, h, schemaRef, encFn) + _, got, err := decodeBody(tc.body, h, schemaRef, encFn) if tc.wantErr != nil { require.Error(t, err) @@ -1350,7 +1350,7 @@ func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { body := strings.NewReader("foo,bar") schema := openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema()).NewRef() encFn := func(string) *openapi3.Encoding { return nil } - got, err := decodeBody(body, h, schema, encFn) + _, got, err := decodeBody(body, h, schema, encFn) require.NoError(t, err) require.Equal(t, []string{"foo", "bar"}, got) @@ -1360,7 +1360,7 @@ func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { originalDecoder = RegisteredBodyDecoder(contentType) require.Nil(t, originalDecoder) - _, err = decodeBody(body, h, schema, encFn) + _, _, err = decodeBody(body, h, schema, encFn) require.Equal(t, &ParseError{ Kind: KindUnsupportedFormat, Reason: prefixUnsupportedCT + ` "text/csv"`, diff --git a/openapi3filter/req_resp_encoder.go b/openapi3filter/req_resp_encoder.go new file mode 100644 index 000000000..b6429d6d8 --- /dev/null +++ b/openapi3filter/req_resp_encoder.go @@ -0,0 +1,27 @@ +package openapi3filter + +import ( + "encoding/json" + "fmt" +) + +func encodeBody(body interface{}, mediaType string) ([]byte, error) { + encoder, ok := bodyEncoders[mediaType] + if !ok { + return nil, &ParseError{ + Kind: KindUnsupportedFormat, + Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType), + } + } + return encoder(body) +} + +type bodyEncoder func(body interface{}) ([]byte, error) + +var bodyEncoders = map[string]bodyEncoder{ + "application/json": jsonBodyEncoder, +} + +func jsonBodyEncoder(body interface{}) ([]byte, error) { + return json.Marshal(body) +} diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index fae6b09f9..db845c0be 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -142,6 +142,30 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param } schema = parameter.Schema.Value } + + // Set default value if needed + if value == nil && schema != nil && schema.Default != nil { + value = schema.Default + req := input.Request + switch parameter.In { + case openapi3.ParameterInPath: + // TODO: no idea how to handle this + case openapi3.ParameterInQuery: + q := req.URL.Query() + q.Add(parameter.Name, fmt.Sprintf("%v", value)) + req.URL.RawQuery = q.Encode() + case openapi3.ParameterInHeader: + req.Header.Add(parameter.Name, fmt.Sprintf("%v", value)) + case openapi3.ParameterInCookie: + req.AddCookie(&http.Cookie{ + Name: parameter.Name, + Value: fmt.Sprintf("%v", value), + }) + default: + return fmt.Errorf("unsupported parameter's 'in': %s", parameter.In) + } + } + // Validate a parameter's value and presence. if parameter.Required && !found { return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired} @@ -230,7 +254,7 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req } encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] } - value, err := decodeBody(bytes.NewReader(data), req.Header, contentType.Schema, encFn) + mediaType, value, err := decodeBody(bytes.NewReader(data), req.Header, contentType.Schema, encFn) if err != nil { return &RequestError{ Input: input, @@ -240,8 +264,10 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req } } - opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here + defaultsSet := false + opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential opts here opts = append(opts, openapi3.VisitAsRequest()) + opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true })) if options.MultiError { opts = append(opts, openapi3.MultiErrors()) } @@ -255,6 +281,21 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req Err: err, } } + + if defaultsSet { + var err error + if data, err = encodeBody(value, mediaType); err != nil { + return &RequestError{ + Input: input, + RequestBody: requestBody, + Reason: "rewriting failed", + Err: err, + } + } + // Put the data back into the input + req.Body = ioutil.NopCloser(bytes.NewReader(data)) + } + return nil } diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index 7cb713ace..f19123e53 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -111,7 +111,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error input.SetBodyBytes(data) encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] } - value, err := decodeBody(bytes.NewBuffer(data), input.Header, contentType.Schema, encFn) + _, value, err := decodeBody(bytes.NewBuffer(data), input.Header, contentType.Schema, encFn) if err != nil { return &ResponseError{ Input: input, diff --git a/openapi3filter/validate_set_default_test.go b/openapi3filter/validate_set_default_test.go new file mode 100644 index 000000000..bacffe529 --- /dev/null +++ b/openapi3filter/validate_set_default_test.go @@ -0,0 +1,561 @@ +package openapi3filter + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" + "github.com/stretchr/testify/require" +) + +func TestValidatingRequestParameterAndSetDefault(t *testing.T) { + const spec = `{ + "openapi": "3.0.3", + "info": { + "version": "1.0.0", + "title": "title", + "description": "desc", + "contact": { + "email": "email" + } + }, + "paths": { + "/accounts": { + "get": { + "description": "Create a new account", + "parameters": [ + { + "in": "query", + "name": "q1", + "schema": { + "type": "string", + "default": "Q" + } + }, + { + "in": "query", + "name": "q2", + "schema": { + "type": "string", + "default": "Q" + } + }, + { + "in": "query", + "name": "q3", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "h1", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "in": "header", + "name": "h2", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "in": "header", + "name": "h3", + "schema": { + "type": "boolean" + } + }, + { + "in": "cookie", + "name": "c1", + "schema": { + "type": "integer", + "default": 128 + } + }, + { + "in": "cookie", + "name": "c2", + "schema": { + "type": "integer", + "default": 128 + } + }, + { + "in": "cookie", + "name": "c3", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "201": { + "description": "Successfully created a new account" + }, + "400": { + "description": "The server could not understand the request due to invalid syntax", + } + } + } + } + } +} +` + + sl := openapi3.NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(sl.Context) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(doc) + require.NoError(t, err) + + httpReq, err := http.NewRequest(http.MethodGet, "/accounts", nil) + require.NoError(t, err) + + params := &url.Values{ + "q2": []string{"from_request"}, + } + httpReq.URL.RawQuery = params.Encode() + httpReq.Header.Set("h2", "false") + httpReq.AddCookie(&http.Cookie{Name: "c2", Value: "1024"}) + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + err = ValidateRequest(sl.Context, &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + }) + require.NoError(t, err) + + // Unset default values in URL were set + require.Equal(t, "Q", httpReq.URL.Query().Get("q1")) + // Unset default values in headers were set + require.Equal(t, "true", httpReq.Header.Get("h1")) + // Unset default values in cookies were set + cookie, err := httpReq.Cookie("c1") + require.NoError(t, err) + require.Equal(t, "128", cookie.Value) + + // All values from request were retained + require.Equal(t, "from_request", httpReq.URL.Query().Get("q2")) + require.Equal(t, "false", httpReq.Header.Get("h2")) + cookie, err = httpReq.Cookie("c2") + require.NoError(t, err) + require.Equal(t, "1024", cookie.Value) + + // Not set value to parameters without default value + require.Equal(t, "", httpReq.URL.Query().Get("q3")) + require.Equal(t, "", httpReq.Header.Get("h3")) + _, err = httpReq.Cookie("c3") + require.Equal(t, http.ErrNoCookie, err) +} + +func TestValidateRequestBodyAndSetDefault(t *testing.T) { + const spec = `{ + "openapi": "3.0.3", + "info": { + "version": "1.0.0", + "title": "title", + "description": "desc", + "contact": { + "email": "email" + } + }, + "paths": { + "/accounts": { + "post": { + "description": "Create a new account", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "pattern": "[0-9a-v]+$", + "minLength": 20, + "maxLength": 20 + }, + "name": { + "type": "string", + "default": "default" + }, + "code": { + "type": "integer", + "default": 123 + }, + "all": { + "type": "boolean", + "default": false + }, + "page": { + "type": "object", + "properties": { + "num": { + "type": "integer", + "default": 1 + }, + "size": { + "type": "integer", + "default": 10 + }, + "order": { + "type": "string", + "enum": ["asc", "desc"], + "default": "desc" + } + } + }, + "filters": { + "type": "array", + "nullable": true, + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "default": "name" + }, + "op": { + "type": "string", + "enum": ["eq", "ne"], + "default": "eq" + }, + "value": { + "type": "integer", + "default": 123 + } + } + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Successfully created a new account" + }, + "400": { + "description": "The server could not understand the request due to invalid syntax", + } + } + } + } + } +}` + sl := openapi3.NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(sl.Context) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(doc) + require.NoError(t, err) + + type page struct { + Num int `json:"num,omitempty"` + Size int `json:"size,omitempty"` + Order string `json:"order,omitempty"` + } + type filter struct { + Field string `json:"field,omitempty"` + OP string `json:"op,omitempty"` + Value int `json:"value,omitempty"` + } + type body struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Code int `json:"code,omitempty"` + All bool `json:"all,omitempty"` + Page *page `json:"page,omitempty"` + Filters []filter `json:"filters,omitempty"` + } + + testCases := []struct { + name string + body body + bodyAssertion func(t *testing.T, body string) + }{ + { + name: "only id", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, `{"id":"bt6kdc3d0cvp6u8u3ft0", "name": "default", "code": 123, "all": false}`, body) + }, + }, + { + name: "id & name", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Name: "non-default", + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, `{"id":"bt6kdc3d0cvp6u8u3ft0", "name": "non-default", "code": 123, "all": false}`, body) + }, + }, + { + name: "id & name & code", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Name: "non-default", + Code: 456, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, `{"id":"bt6kdc3d0cvp6u8u3ft0", "name": "non-default", "code": 456, "all": false}`, body) + }, + }, + { + name: "id & name & code & all", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Name: "non-default", + Code: 456, + All: true, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, `{"id":"bt6kdc3d0cvp6u8u3ft0", "name": "non-default", "code": 456, "all": true}`, body) + }, + }, + { + name: "id & page(num)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Page: &page{ + Num: 10, + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "page": { + "num": 10, + "size": 10, + "order": "desc" + } +} + `, body) + }, + }, + { + name: "id & page(num & order)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Page: &page{ + Num: 10, + Order: "asc", + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "page": { + "num": 10, + "size": 10, + "order": "asc" + } +} + `, body) + }, + }, + { + name: "id & page & filters(one element and contains field)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Page: &page{ + Num: 10, + Order: "asc", + }, + Filters: []filter{ + { + Field: "code", + }, + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "page": { + "num": 10, + "size": 10, + "order": "asc" + }, + "filters": [ + { + "field": "code", + "op": "eq", + "value": 123 + } + ] +} + `, body) + }, + }, + { + name: "id & page & filters(one element and contains field & op & value)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Page: &page{ + Num: 10, + Order: "asc", + }, + Filters: []filter{ + { + Field: "code", + OP: "ne", + Value: 456, + }, + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "page": { + "num": 10, + "size": 10, + "order": "asc" + }, + "filters": [ + { + "field": "code", + "op": "ne", + "value": 456 + } + ] +} + `, body) + }, + }, + { + name: "id & page & filters(multiple elements)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Page: &page{ + Num: 10, + Order: "asc", + }, + Filters: []filter{ + { + Value: 456, + }, + { + OP: "ne", + }, + { + Field: "code", + Value: 456, + }, + { + OP: "ne", + Value: 789, + }, + { + Field: "code", + OP: "ne", + Value: 456, + }, + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "page": { + "num": 10, + "size": 10, + "order": "asc" + }, + "filters": [ + { + "field": "name", + "op": "eq", + "value": 456 + }, + { + "field": "name", + "op": "ne", + "value": 123 + }, + { + "field": "code", + "op": "eq", + "value": 456 + }, + { + "field": "name", + "op": "ne", + "value": 789 + }, + { + "field": "code", + "op": "ne", + "value": 456 + } + ] +} + `, body) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b, err := json.Marshal(tc.body) + require.NoError(t, err) + httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b)) + require.NoError(t, err) + httpReq.Header.Add(headerCT, "application/json") + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + err = ValidateRequest(sl.Context, &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + }) + require.NoError(t, err) + + validatedReqBody, err := ioutil.ReadAll(httpReq.Body) + require.NoError(t, err) + tc.bodyAssertion(t, string(validatedReqBody)) + }) + } +} From 221a29220d72ba3384a380e316e38f262b97e99f Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 30 May 2022 18:56:56 +0200 Subject: [PATCH 158/260] following up on #544: do not pass through on unhandled case (#550) --- openapi3filter/req_resp_encoder.go | 6 +----- openapi3filter/validate_request.go | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openapi3filter/req_resp_encoder.go b/openapi3filter/req_resp_encoder.go index b6429d6d8..dd410f588 100644 --- a/openapi3filter/req_resp_encoder.go +++ b/openapi3filter/req_resp_encoder.go @@ -19,9 +19,5 @@ func encodeBody(body interface{}, mediaType string) ([]byte, error) { type bodyEncoder func(body interface{}) ([]byte, error) var bodyEncoders = map[string]bodyEncoder{ - "application/json": jsonBodyEncoder, -} - -func jsonBodyEncoder(body interface{}) ([]byte, error) { - return json.Marshal(body) + "application/json": json.Marshal, } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index db845c0be..4f2232645 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -148,8 +148,7 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param value = schema.Default req := input.Request switch parameter.In { - case openapi3.ParameterInPath: - // TODO: no idea how to handle this + // case openapi3.ParameterInPath: TODO: no idea how to handle this case openapi3.ParameterInQuery: q := req.URL.Query() q.Add(parameter.Name, fmt.Sprintf("%v", value)) From 39add0a97a2fcf9cbb9dbd8018554539cb7c8aed Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 31 May 2022 16:56:49 +0200 Subject: [PATCH 159/260] Fix for CVE-2022-28948 (#552) * CI: check-goimports Signed-off-by: Pierre Fenoll * reorder imports per new CI check Signed-off-by: Pierre Fenoll * switch from github.com/ghodss/yaml to github.com/invopop/yaml Signed-off-by: Pierre Fenoll * remove all direct dependencies on gopkg.in/yaml.v2 Signed-off-by: Pierre Fenoll * upgrade gopkg.in/yaml.v2 to latest published tag Signed-off-by: Pierre Fenoll * upgrade gopkg.in/yaml.v3 to latest published tag Signed-off-by: Pierre Fenoll --- .github/workflows/go.yml | 12 ++++++++++++ go.mod | 5 +++-- go.sum | 11 +++++++---- openapi2/openapi2_test.go | 3 ++- openapi2conv/issue187_test.go | 7 ++++--- openapi2conv/issue440_test.go | 3 ++- openapi3/example.go | 3 ++- openapi3/extension_test.go | 3 ++- openapi3/header.go | 3 ++- openapi3/link.go | 3 ++- openapi3/loader.go | 2 +- openapi3/media_type.go | 3 ++- openapi3/openapi3_test.go | 2 +- openapi3/operation.go | 3 ++- openapi3/parameter.go | 3 ++- openapi3/race_test.go | 3 ++- openapi3/refs.go | 3 ++- openapi3/request_body.go | 3 ++- openapi3/response.go | 3 ++- openapi3/schema.go | 3 ++- openapi3/security_scheme.go | 3 ++- openapi3/unique_items_checker_test.go | 3 ++- openapi3filter/req_resp_decoder.go | 2 +- openapi3filter/req_resp_decoder_test.go | 3 ++- openapi3filter/validate_readonly_test.go | 3 ++- openapi3filter/validate_request_test.go | 5 +++-- openapi3filter/validate_set_default_test.go | 3 ++- openapi3filter/validation_discriminator_test.go | 3 ++- openapi3filter/validation_error_test.go | 3 ++- openapi3filter/validation_test.go | 3 ++- openapi3gen/openapi3gen_test.go | 3 ++- routers/gorillamux/router.go | 3 ++- routers/gorillamux/router_test.go | 3 ++- routers/issue356_test.go | 3 ++- routers/legacy/issue444_test.go | 3 ++- routers/legacy/router_test.go | 3 ++- 36 files changed, 88 insertions(+), 42 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5cdecc97b..6c192f969 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -142,3 +142,15 @@ jobs: T Tag XML + + check-goimports: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v3 + with: + go-version: '>=1.17.0' + - run: go install github.com/incu6us/goimports-reviser/v2@v2.5.1 + - run: which goimports-reviser + - run: find . -type f -iname '*.go' ! -iname '*.pb.go' -exec goimports-reviser -file-path {} \; + - run: git --no-pager diff --exit-code diff --git a/go.mod b/go.mod index df32d6a7e..50aba584b 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module github.com/getkin/kin-openapi go 1.16 require ( - github.com/ghodss/yaml v1.0.0 github.com/go-openapi/jsonpointer v0.19.5 github.com/gorilla/mux v1.8.0 + github.com/invopop/yaml v0.1.0 github.com/stretchr/testify v1.5.1 - gopkg.in/yaml.v2 v2.3.0 + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 2b289d716..a123aaff6 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,14 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -27,5 +27,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/openapi2/openapi2_test.go b/openapi2/openapi2_test.go index 8d3efdf8a..65e92d601 100644 --- a/openapi2/openapi2_test.go +++ b/openapi2/openapi2_test.go @@ -6,8 +6,9 @@ import ( "io/ioutil" "reflect" + "github.com/invopop/yaml" + "github.com/getkin/kin-openapi/openapi2" - "github.com/ghodss/yaml" ) func Example() { diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index a7016893a..93914d9f9 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -5,10 +5,11 @@ import ( "encoding/json" "testing" + "github.com/invopop/yaml" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" - "github.com/ghodss/yaml" - "github.com/stretchr/testify/require" ) func v2v3JSON(spec2 []byte) (doc3 *openapi3.T, err error) { @@ -162,7 +163,7 @@ paths: "200": description: description ` - require.Equal(t, string(spec3), expected) + require.YAMLEq(t, string(spec3), expected) err = doc3.Validate(context.Background()) require.NoError(t, err) diff --git a/openapi2conv/issue440_test.go b/openapi2conv/issue440_test.go index 24f7a29e9..2478384ff 100644 --- a/openapi2conv/issue440_test.go +++ b/openapi2conv/issue440_test.go @@ -6,9 +6,10 @@ import ( "os" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" - "github.com/stretchr/testify/require" ) func TestIssue440(t *testing.T) { diff --git a/openapi3/example.go b/openapi3/example.go index 080845cad..ee40d9e37 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -4,8 +4,9 @@ import ( "context" "fmt" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) type Examples map[string]*ExampleRef diff --git a/openapi3/extension_test.go b/openapi3/extension_test.go index 9d009024e..a99537892 100644 --- a/openapi3/extension_test.go +++ b/openapi3/extension_test.go @@ -5,8 +5,9 @@ import ( "fmt" "testing" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/jsoninfo" ) func ExampleExtensionProps_DecodeWith() { diff --git a/openapi3/header.go b/openapi3/header.go index 84ffd8866..75e5dd1e2 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) type Headers map[string]*HeaderRef diff --git a/openapi3/link.go b/openapi3/link.go index 2f0bc57c9..7f0c49d4d 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) type Links map[string]*LinkRef diff --git a/openapi3/loader.go b/openapi3/loader.go index 8af733c3b..e2f131e40 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -12,7 +12,7 @@ import ( "strconv" "strings" - "github.com/ghodss/yaml" + "github.com/invopop/yaml" ) func foundUnresolvedRef(ref string) error { diff --git a/openapi3/media_type.go b/openapi3/media_type.go index dd33b99b2..fc95244c6 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -3,8 +3,9 @@ package openapi3 import ( "context" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) // MediaType is specified by OpenAPI/Swagger 3.0 standard. diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 69a2f959b..7736310cc 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/ghodss/yaml" + "github.com/invopop/yaml" "github.com/stretchr/testify/require" ) diff --git a/openapi3/operation.go b/openapi3/operation.go index cb9644a77..58750ffbf 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -6,8 +6,9 @@ import ( "fmt" "strconv" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. diff --git a/openapi3/parameter.go b/openapi3/parameter.go index b32898c65..77834847d 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -6,8 +6,9 @@ import ( "fmt" "strconv" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) type ParametersMap map[string]*ParameterRef diff --git a/openapi3/race_test.go b/openapi3/race_test.go index 4ac31c38e..c617cfe49 100644 --- a/openapi3/race_test.go +++ b/openapi3/race_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" ) func TestRaceyPatternSchema(t *testing.T) { diff --git a/openapi3/refs.go b/openapi3/refs.go index a706834ca..0bf737ff7 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -3,8 +3,9 @@ package openapi3 import ( "context" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) // Ref is specified by OpenAPI/Swagger 3.0 standard. diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 559eb7122..c97563a11 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) type RequestBodies map[string]*RequestBodyRef diff --git a/openapi3/response.go b/openapi3/response.go index 23e2f4449..31ea257d1 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -6,8 +6,9 @@ import ( "fmt" "strconv" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) // Responses is specified by OpenAPI/Swagger 3.0 standard. diff --git a/openapi3/schema.go b/openapi3/schema.go index 214f4f635..4c8df1cba 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -12,8 +12,9 @@ import ( "strconv" "unicode/utf16" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) const ( diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 10057de6b..52c3ef218 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + + "github.com/getkin/kin-openapi/jsoninfo" ) type SecuritySchemes map[string]*SecuritySchemeRef diff --git a/openapi3/unique_items_checker_test.go b/openapi3/unique_items_checker_test.go index 85147c67a..270b797e1 100644 --- a/openapi3/unique_items_checker_test.go +++ b/openapi3/unique_items_checker_test.go @@ -4,8 +4,9 @@ import ( "strings" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" ) func TestRegisterArrayUniqueItemsChecker(t *testing.T) { diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 1b5f0c5d5..a54b3bb47 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -14,7 +14,7 @@ import ( "strconv" "strings" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/getkin/kin-openapi/openapi3" ) diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index c733bd028..8bd62b1c5 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -15,9 +15,10 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" - "github.com/stretchr/testify/require" ) func TestDecodeParameter(t *testing.T) { diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go index 454a927e9..1152ec886 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -6,9 +6,10 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" - "github.com/stretchr/testify/require" ) func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index b43f6c813..957b6925e 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -10,11 +10,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" "github.com/getkin/kin-openapi/routers/gorillamux" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func setupTestRouter(t *testing.T, spec string) routers.Router { diff --git a/openapi3filter/validate_set_default_test.go b/openapi3filter/validate_set_default_test.go index bacffe529..40714051a 100644 --- a/openapi3filter/validate_set_default_test.go +++ b/openapi3filter/validate_set_default_test.go @@ -8,9 +8,10 @@ import ( "net/url" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" - "github.com/stretchr/testify/require" ) func TestValidatingRequestParameterAndSetDefault(t *testing.T) { diff --git a/openapi3filter/validation_discriminator_test.go b/openapi3filter/validation_discriminator_test.go index c7d614403..adabf409d 100644 --- a/openapi3filter/validation_discriminator_test.go +++ b/openapi3filter/validation_discriminator_test.go @@ -5,9 +5,10 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" - "github.com/stretchr/testify/require" ) func TestValidationWithDiscriminatorSelection(t *testing.T) { diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 10d46f855..b9151b878 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -10,9 +10,10 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" - "github.com/stretchr/testify/require" ) func newPetstoreRequest(t *testing.T, method, path string, body io.Reader) *http.Request { diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index 2f7cf80e6..cd1fa8990 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -14,9 +14,10 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" - "github.com/stretchr/testify/require" ) type ExampleRequest struct { diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index a85f5da4a..a4c2d52e9 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -10,9 +10,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3gen" - "github.com/stretchr/testify/require" ) func ExampleGenerator_SchemaRefs() { diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 83bbf829e..67be47452 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -12,9 +12,10 @@ import ( "sort" "strings" + "github.com/gorilla/mux" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" - "github.com/gorilla/mux" ) var _ routers.Router = &Router{} diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index 90f5c3dba..31bf416ed 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -6,9 +6,10 @@ import ( "sort" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" - "github.com/stretchr/testify/require" ) func TestRouter(t *testing.T) { diff --git a/routers/issue356_test.go b/routers/issue356_test.go index 307e52aea..3e4b9fa1d 100644 --- a/routers/issue356_test.go +++ b/routers/issue356_test.go @@ -8,12 +8,13 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" "github.com/getkin/kin-openapi/routers" "github.com/getkin/kin-openapi/routers/gorillamux" "github.com/getkin/kin-openapi/routers/legacy" - "github.com/stretchr/testify/require" ) func TestIssue356(t *testing.T) { diff --git a/routers/legacy/issue444_test.go b/routers/legacy/issue444_test.go index 222ecbba4..c1e9b14f2 100644 --- a/routers/legacy/issue444_test.go +++ b/routers/legacy/issue444_test.go @@ -6,10 +6,11 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" - "github.com/stretchr/testify/require" ) func TestIssue444(t *testing.T) { diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go index bfc7e11e5..d4779f58a 100644 --- a/routers/legacy/router_test.go +++ b/routers/legacy/router_test.go @@ -6,9 +6,10 @@ import ( "sort" "testing" + "github.com/stretchr/testify/require" + "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" - "github.com/stretchr/testify/require" ) func TestRouter(t *testing.T) { From 12540af49b7b2c6964371ab6faae41c0c628c3af Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 31 May 2022 17:24:44 +0200 Subject: [PATCH 160/260] TestIssue430: fix racey behavior (#553) --- .github/workflows/go.yml | 1 + openapi3/schema_formats.go | 11 ++++++----- openapi3/schema_formats_test.go | 3 +++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6c192f969..70b6db6d9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -66,6 +66,7 @@ jobs: env: GOARCH: '386' - run: go test ./... + - run: go test -count=2 ./... - run: go test -v -run TestRaceyPatternSchema -race ./... env: CGO_ENABLED: '1' diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 29fbd51fb..17b7564cf 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -12,18 +12,19 @@ const ( FormatOfStringForUUIDOfRFC4122 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$` ) -//FormatCallback custom check on exotic formats -type FormatCallback func(Val string) error +// FormatCallback performs custom checks on exotic formats +type FormatCallback func(value string) error +// Format represents a format validator registered by either DefineStringFormat or DefineStringFormatCallback type Format struct { regexp *regexp.Regexp callback FormatCallback } -//SchemaStringFormats allows for validating strings format -var SchemaStringFormats = make(map[string]Format, 8) +// SchemaStringFormats allows for validating string formats +var SchemaStringFormats = make(map[string]Format, 4) -//DefineStringFormat Defines a new regexp pattern for a given format +// DefineStringFormat defines a new regexp pattern for a given format func DefineStringFormat(name string, pattern string) { re, err := regexp.Compile(pattern) if err != nil { diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go index 14733c8a1..5cceb8cf0 100644 --- a/openapi3/schema_formats_test.go +++ b/openapi3/schema_formats_test.go @@ -13,6 +13,9 @@ func TestIssue430(t *testing.T) { NewStringSchema().WithFormat("ipv6"), ) + delete(SchemaStringFormats, "ipv4") + delete(SchemaStringFormats, "ipv6") + err := schema.Validate(context.Background()) require.NoError(t, err) From 142adad5c92ca9f90d809aa7e841516c8c2435e6 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 31 May 2022 17:53:19 +0200 Subject: [PATCH 161/260] Handle port number variable of servers given to gorillamux.NewRouter (#524) --- routers/gorillamux/router.go | 88 ++++++++++++++++++++++++------- routers/gorillamux/router_test.go | 25 +++++++++ 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 67be47452..bf551a751 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -9,6 +9,7 @@ package gorillamux import ( "net/http" "net/url" + "regexp" "sort" "strings" @@ -22,32 +23,75 @@ var _ routers.Router = &Router{} // Router helps link http.Request.s and an OpenAPIv3 spec type Router struct { - muxes []*mux.Route + muxes []routeMux routes []*routers.Route } +type varsf func(vars map[string]string) + +type routeMux struct { + muxRoute *mux.Route + varsUpdater varsf +} + +var singleVariableMatcher = regexp.MustCompile(`^\{([^{}]+)\}$`) + +// TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request)) + // NewRouter creates a gorilla/mux router. // Assumes spec is .Validate()d -// TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request)) +// Note that a variable for the port number MUST have a default value and only this value will match as the port (see issue #367). func NewRouter(doc *openapi3.T) (routers.Router, error) { type srv struct { - schemes []string - host, base string - server *openapi3.Server + schemes []string + host, base string + server *openapi3.Server + varsUpdater varsf } servers := make([]srv, 0, len(doc.Servers)) for _, server := range doc.Servers { serverURL := server.URL + if submatch := singleVariableMatcher.FindStringSubmatch(serverURL); submatch != nil { + sVar := submatch[1] + sVal := server.Variables[sVar].Default + serverURL = strings.ReplaceAll(serverURL, "{"+sVar+"}", sVal) + var varsUpdater varsf + if lhs := strings.TrimSuffix(serverURL, server.Variables[sVar].Default); lhs != "" { + varsUpdater = func(vars map[string]string) { vars[sVar] = lhs } + } + servers = append(servers, srv{ + base: server.Variables[sVar].Default, + server: server, + varsUpdater: varsUpdater, + }) + continue + } + var schemes []string - var u *url.URL - var err error if strings.Contains(serverURL, "://") { scheme0 := strings.Split(serverURL, "://")[0] schemes = permutePart(scheme0, server) - u, err = url.Parse(bEncode(strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1))) - } else { - u, err = url.Parse(bEncode(serverURL)) + serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1) } + + // If a variable represents the port "http://domain.tld:{port}/bla" + // then url.Parse() cannot parse "http://domain.tld:`bEncode({port})`/bla" + // and mux is not able to set the {port} variable + // So we just use the default value for this variable. + // See https://github.com/getkin/kin-openapi/issues/367 + var varsUpdater varsf + if lhs := strings.Index(serverURL, ":{"); lhs > 0 { + rest := serverURL[lhs+len(":{"):] + rhs := strings.Index(rest, "}") + portVariable := rest[:rhs] + portValue := server.Variables[portVariable].Default + serverURL = strings.ReplaceAll(serverURL, "{"+portVariable+"}", portValue) + varsUpdater = func(vars map[string]string) { + vars[portVariable] = portValue + } + } + + u, err := url.Parse(bEncode(serverURL)) if err != nil { return nil, err } @@ -56,10 +100,11 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { path = path[:len(path)-1] } servers = append(servers, srv{ - host: bDecode(u.Host), //u.Hostname()? - base: path, - schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624 - server: server, + host: bDecode(u.Host), //u.Hostname()? + base: path, + schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624 + server: server, + varsUpdater: varsUpdater, }) } if len(servers) == 0 { @@ -88,7 +133,10 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { if err := muxRoute.GetError(); err != nil { return nil, err } - r.muxes = append(r.muxes, muxRoute) + r.muxes = append(r.muxes, routeMux{ + muxRoute: muxRoute, + varsUpdater: s.varsUpdater, + }) r.routes = append(r.routes, &routers.Route{ Spec: doc, Server: s.server, @@ -104,16 +152,20 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { // FindRoute extracts the route and parameters of an http.Request func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) { - for i, muxRoute := range r.muxes { + for i, m := range r.muxes { var match mux.RouteMatch - if muxRoute.Match(req, &match) { + if m.muxRoute.Match(req, &match) { if err := match.MatchErr; err != nil { // What then? } + vars := match.Vars + if f := m.varsUpdater; f != nil { + f(vars) + } route := *r.routes[i] route.Method = req.Method route.Operation = route.Spec.Paths[route.Path].GetOperation(route.Method) - return &route, match.Vars, nil + return &route, vars, nil } switch match.MatchErr { case nil: diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index 31bf416ed..1898db4ac 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -73,6 +73,7 @@ func TestRouter(t *testing.T) { } expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { + t.Helper() req, err := http.NewRequest(method, uri, nil) require.NoError(t, err) route, pathParams, err := r.FindRoute(req) @@ -164,6 +165,9 @@ func TestRouter(t *testing.T) { "d1": {Default: "example", Enum: []string{"example"}}, "scheme": {Default: "https", Enum: []string{"https", "http"}}, }}, + {URL: "http://127.0.0.1:{port}/api/v1", Variables: map[string]*openapi3.ServerVariable{ + "port": {Default: "8000"}, + }}, } err = doc.Validate(context.Background()) require.NoError(t, err) @@ -180,6 +184,20 @@ func TestRouter(t *testing.T) { "d1": "domain1", // "scheme": "https", TODO: https://github.com/gorilla/mux/issues/624 }) + expect(r, http.MethodGet, "http://127.0.0.1:8000/api/v1/hello", helloGET, map[string]string{ + "port": "8000", + }) + + doc.Servers = []*openapi3.Server{ + {URL: "{server}", Variables: map[string]*openapi3.ServerVariable{ + "server": {Default: "/api/v1"}, + }}, + } + err = doc.Validate(context.Background()) + require.NoError(t, err) + r, err = NewRouter(doc) + require.NoError(t, err) + expect(r, http.MethodGet, "https://myserver/api/v1/hello", helloGET, nil) { uri := "https://www.example.com/api/v1/onlyGET" @@ -224,6 +242,11 @@ func TestServerPath(t *testing.T) { func TestRelativeURL(t *testing.T) { helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} doc := &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "rel", + Version: "1", + }, Servers: openapi3.Servers{ &openapi3.Server{ URL: "/api/v1", @@ -235,6 +258,8 @@ func TestRelativeURL(t *testing.T) { }, }, } + err := doc.Validate(context.Background()) + require.NoError(t, err) router, err := NewRouter(doc) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "https://example.com/api/v1/hello", nil) From bcecaeee449cfdeaa5696dad0348ee579bbd758c Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 31 May 2022 18:19:07 +0200 Subject: [PATCH 162/260] update README.md with newer router/validator example (#554) --- README.md | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index a36e37f2e..da288d08d 100644 --- a/README.md +++ b/README.md @@ -65,23 +65,22 @@ route, pathParams, _ := router.FindRoute(httpRequest) package main import ( - "bytes" "context" - "encoding/json" - "log" + "fmt" "net/http" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" - legacyrouter "github.com/getkin/kin-openapi/routers/legacy" + "github.com/getkin/kin-openapi/routers/gorillamux" ) func main() { ctx := context.Background() - loader := openapi3.Loader{Context: ctx} - doc, _ := loader.LoadFromFile("openapi3_spec.json") - _ = doc.Validate(ctx) - router, _ := legacyrouter.NewRouter(doc) + loader := &openapi3.Loader{Context: ctx, IsExternalRefsAllowed: true} + doc, _ := loader.LoadFromFile(".../My-OpenAPIv3-API.yml") + // Validate document + _ := doc.Validate(ctx) + router, _ := gorillamux.NewRouter(doc) httpReq, _ := http.NewRequest(http.MethodGet, "/items", nil) // Find route @@ -93,31 +92,22 @@ func main() { PathParams: pathParams, Route: route, } - if err := openapi3filter.ValidateRequest(ctx, requestValidationInput); err != nil { - panic(err) - } + _ := openapi3filter.ValidateRequest(ctx, requestValidationInput) - var ( - respStatus = 200 - respContentType = "application/json" - respBody = bytes.NewBufferString(`{}`) - ) + // Handle that request + // --> YOUR CODE GOES HERE <-- + responseHeaders := http.Header{"Content-Type": []string{"application/json"}} + responseCode := 200 + responseBody := []byte(`{}`) - log.Println("Response:", respStatus) + // Validate response responseValidationInput := &openapi3filter.ResponseValidationInput{ RequestValidationInput: requestValidationInput, - Status: respStatus, - Header: http.Header{"Content-Type": []string{respContentType}}, - } - if respBody != nil { - data, _ := json.Marshal(respBody) - responseValidationInput.SetBodyBytes(data) - } - - // Validate response. - if err := openapi3filter.ValidateResponse(ctx, responseValidationInput); err != nil { - panic(err) + Status: responseCode, + Header: responseHeaders, } + responseValidationInput.SetBodyBytes(responseBody) + _ := openapi3filter.ValidateResponse(ctx, responseValidationInput) } ``` From 416e53fc4aa8c290f8d8683f21183506e6fdff19 Mon Sep 17 00:00:00 2001 From: slessard Date: Tue, 31 May 2022 23:44:05 -0700 Subject: [PATCH 163/260] Unit tests (#556) --- routers/gorillamux/router_test.go | 41 ++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index 1898db4ac..f8800baed 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -234,7 +234,22 @@ func TestServerPath(t *testing.T) { _, err = NewRouter(&openapi3.T{Servers: openapi3.Servers{ server, &openapi3.Server{URL: "http://example.com/"}, - &openapi3.Server{URL: "http://example.com/path"}}, + &openapi3.Server{URL: "http://example.com/path"}, + newServerWithVariables( + "{scheme}://localhost", + map[string]string{ + "scheme": "https", + }), + newServerWithVariables( + "{url}", + map[string]string{ + "url": "http://example.com/path", + }), + newServerWithVariables( + "http://example.com:{port}/path", + map[string]string{ + "port": "8088", + })}, }) require.NoError(t, err) } @@ -268,3 +283,27 @@ func TestRelativeURL(t *testing.T) { require.NoError(t, err) require.Equal(t, "/hello", route.Path) } + +func newServerWithVariables(url string, variables map[string]string) *openapi3.Server { + var serverVariables = map[string]*openapi3.ServerVariable{} + + for key, value := range variables { + serverVariables[key] = newServerVariable(value) + } + + return &openapi3.Server{ + ExtensionProps: openapi3.ExtensionProps{}, + URL: url, + Description: "", + Variables: serverVariables, + } +} + +func newServerVariable(defaultValue string) *openapi3.ServerVariable { + return &openapi3.ServerVariable{ + ExtensionProps: openapi3.ExtensionProps{}, + Enum: nil, + Default: defaultValue, + Description: "", + } +} From 648d6b9a170b6162c79927aeba39bda2fe37386a Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 1 Jun 2022 19:42:04 +0200 Subject: [PATCH 164/260] add gitlab.com/jamietanna/httptest-openapi to README.md (#557) --- README.md | 7 ++++--- openapi3filter/validation_handler.go | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index da288d08d..a829e5983 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ Licensed under the [MIT License](./LICENSE). The project has received pull requests from many people. Thanks to everyone! Here's some projects that depend on _kin-openapi_: - * [https://github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" + * [github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" - * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPIv3 spec document + * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - "Generate Go client and server boilerplate from OpenAPI 3 specifications" * [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs" * [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "...a CLI for interacting with REST-ish HTTP APIs with some nice features built-in" - * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Goa is a framework for building micro-services and APIs in Go using a unique design-first approach." + * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Design-based APIs and microservices in Go" * [github.com/hashicorp/nomad-openapi](https://github.com/hashicorp/nomad-openapi) - "Nomad is an easy-to-use, flexible, and performant workload orchestrator that can deploy a mix of microservice, batch, containerized, and non-containerized applications. Nomad is easy to operate and scale and has native Consul and Vault integrations." + * [gitlab.com/jamietanna/httptest-openapi](https://gitlab.com/jamietanna/httptest-openapi) ([*blog post*](https://www.jvt.me/posts/2022/05/22/go-openapi-contract-test/)) - "Go OpenAPI Contract Verification for use with `net/http`" * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request) ## Alternatives diff --git a/openapi3filter/validation_handler.go b/openapi3filter/validation_handler.go index eeb1ca1ea..d4bb1efa0 100644 --- a/openapi3filter/validation_handler.go +++ b/openapi3filter/validation_handler.go @@ -9,8 +9,12 @@ import ( legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) +// AuthenticationFunc allows for custom security requirement validation. +// A non-nil error fails authentication according to https://spec.openapis.org/oas/v3.1.0#security-requirement-object +// See ValidateSecurityRequirements type AuthenticationFunc func(context.Context, *AuthenticationInput) error +// NoopAuthenticationFunc is an AuthenticationFunc func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error { return nil } var _ AuthenticationFunc = NoopAuthenticationFunc From 32445850faa0551385d614e844b344006c0a8f22 Mon Sep 17 00:00:00 2001 From: Idan Frimark <40820488+FrimIdan@users.noreply.github.com> Date: Sun, 12 Jun 2022 13:19:45 +0300 Subject: [PATCH 165/260] fix: add deprecated field to openapi2.Operation (#559) --- openapi2/openapi2.go | 1 + openapi2conv/issue558_test.go | 37 +++++++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 2 ++ 3 files changed, 40 insertions(+) create mode 100644 openapi2conv/issue558_test.go diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index c5042882d..dcc5ddb66 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -148,6 +148,7 @@ type Operation struct { openapi3.ExtensionProps Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` diff --git a/openapi2conv/issue558_test.go b/openapi2conv/issue558_test.go new file mode 100644 index 000000000..78661bf78 --- /dev/null +++ b/openapi2conv/issue558_test.go @@ -0,0 +1,37 @@ +package openapi2conv + +import ( + "testing" + + "github.com/invopop/yaml" + "github.com/stretchr/testify/require" +) + +func TestPR558(t *testing.T) { + spec := ` +swagger: '2.0' +info: + version: 1.0.0 + title: title +paths: + /test: + get: + deprecated: true + parameters: + - in: body + schema: + type: object + responses: + '200': + description: description +` + doc3, err := v2v3YAML([]byte(spec)) + require.NoError(t, err) + require.NotEmpty(t, doc3.Paths["/test"].Get.Deprecated) + _, err = yaml.Marshal(doc3) + require.NoError(t, err) + + doc2, err := FromV3(doc3) + require.NoError(t, err) + require.NotEmpty(t, doc2.Paths["/test"].Get.Deprecated) +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 387a05ad0..d34dd31eb 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -155,6 +155,7 @@ func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem * OperationID: operation.OperationID, Summary: operation.Summary, Description: operation.Description, + Deprecated: operation.Deprecated, Tags: operation.Tags, ExtensionProps: operation.ExtensionProps, } @@ -922,6 +923,7 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 OperationID: operation.OperationID, Summary: operation.Summary, Description: operation.Description, + Deprecated: operation.Deprecated, Tags: operation.Tags, ExtensionProps: operation.ExtensionProps, } From fd4bae81cff0aae76fbea0f57ee8be5b1d9ec4f0 Mon Sep 17 00:00:00 2001 From: Nir <35661734+nirhaas@users.noreply.github.com> Date: Sun, 31 Jul 2022 16:50:11 +0300 Subject: [PATCH 166/260] fix: openapi2conv respects produces field (#575) --- openapi2conv/issue573_test.go | 48 +++++++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 18 ++++++++++--- 2 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 openapi2conv/issue573_test.go diff --git a/openapi2conv/issue573_test.go b/openapi2conv/issue573_test.go new file mode 100644 index 000000000..cefac409e --- /dev/null +++ b/openapi2conv/issue573_test.go @@ -0,0 +1,48 @@ +package openapi2conv + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue573(t *testing.T) { + spec := []byte(`paths: + /ping: + get: + produces: + - application/toml + - application/xml + responses: + 200: + schema: + type: object + properties: + username: + type: string + description: The user name. + post: + responses: + 200: + schema: + type: object + properties: + username: + type: string + description: The user name.`) + + v3, err := v2v3YAML(spec) + require.NoError(t, err) + + // Make sure the response content appears for each mime-type originally + // appeared in "produces". + pingGetContent := v3.Paths["/ping"].Get.Responses["200"].Value.Content + require.Len(t, pingGetContent, 2) + require.Contains(t, pingGetContent, "application/toml") + require.Contains(t, pingGetContent, "application/xml") + + // Is "produces" is not explicitly specified, default to "application/json". + pingPostContent := v3.Paths["/ping"].Post.Responses["200"].Value.Content + require.Len(t, pingPostContent, 1) + require.Contains(t, pingPostContent, "application/json") +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index d34dd31eb..53b4b40cc 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -84,7 +84,7 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { if responses := doc2.Responses; len(responses) != 0 { doc3.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses)) for k, response := range responses { - r, err := ToV3Response(response) + r, err := ToV3Response(response, doc2.Produces) if err != nil { return nil, err } @@ -193,7 +193,7 @@ func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem * if responses := operation.Responses; responses != nil { doc3Responses := make(openapi3.Responses, len(responses)) for k, response := range responses { - doc3, err := ToV3Response(response) + doc3, err := ToV3Response(response, operation.Produces) if err != nil { return nil, err } @@ -413,7 +413,7 @@ func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[ return nil, nil } -func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) { +func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error) { if ref := response.Ref; ref != "" { return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil } @@ -422,8 +422,18 @@ func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) { Description: &response.Description, ExtensionProps: response.ExtensionProps, } + + // Default to "application/json" if "produces" is not specified. + if len(produces) == 0 { + produces = []string{"application/json"} + } + if schemaRef := response.Schema; schemaRef != nil { - result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef)) + schema := ToV3SchemaRef(schemaRef) + result.Content = make(openapi3.Content, len(produces)) + for _, mime := range produces { + result.Content[mime] = openapi3.NewMediaType().WithSchemaRef(schema) + } } if headers := response.Headers; len(headers) > 0 { result.Headers = ToV3Headers(headers) From 00d1ae8b91a0bb3b818ce375cb6315970c3cb2d6 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 26 Aug 2022 20:19:55 +0200 Subject: [PATCH 167/260] Use go1.19 formatting (#584) --- jsoninfo/marshal.go | 4 ++-- jsoninfo/marshal_test.go | 7 ++++--- jsoninfo/unmarshal.go | 4 ++-- openapi3/errors.go | 6 +++--- openapi3/internalize_refs.go | 2 +- openapi3/paths.go | 8 ++++---- routers/legacy/pathpattern/node.go | 14 +++++++------- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/jsoninfo/marshal.go b/jsoninfo/marshal.go index 2a98d68fb..6e946d877 100644 --- a/jsoninfo/marshal.go +++ b/jsoninfo/marshal.go @@ -7,8 +7,8 @@ import ( ) // MarshalStrictStruct function: -// * Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag. -// * Correctly handles StrictStruct semantics. +// - Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag. +// - Correctly handles StrictStruct semantics. func MarshalStrictStruct(value StrictStruct) ([]byte, error) { encoder := NewObjectEncoder() if err := value.EncodeWith(encoder, value); err != nil { diff --git a/jsoninfo/marshal_test.go b/jsoninfo/marshal_test.go index 05a6ac31b..10551542d 100644 --- a/jsoninfo/marshal_test.go +++ b/jsoninfo/marshal_test.go @@ -64,9 +64,10 @@ type EmbeddedType1 struct { } // Example describes expected outcome of: -// 1.Marshal JSON -// 2.Unmarshal value -// 3.Marshal value +// +// 1.Marshal JSON +// 2.Unmarshal value +// 3.Marshal value type Example struct { NoMarshal bool NoUnmarshal bool diff --git a/jsoninfo/unmarshal.go b/jsoninfo/unmarshal.go index ce3c337a3..eb6e758ac 100644 --- a/jsoninfo/unmarshal.go +++ b/jsoninfo/unmarshal.go @@ -7,8 +7,8 @@ import ( ) // UnmarshalStrictStruct function: -// * Unmarshals struct fields, ignoring UnmarshalJSON(...) and fields without 'json' tag. -// * Correctly handles StrictStruct +// - Unmarshals struct fields, ignoring UnmarshalJSON(...) and fields without 'json' tag. +// - Correctly handles StrictStruct func UnmarshalStrictStruct(data []byte, value StrictStruct) error { decoder, err := NewObjectDecoder(data) if err != nil { diff --git a/openapi3/errors.go b/openapi3/errors.go index ce52cd483..da0970abc 100644 --- a/openapi3/errors.go +++ b/openapi3/errors.go @@ -18,8 +18,8 @@ func (me MultiError) Error() string { return buff.String() } -//Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()` -//It will also return true if any of the contained errors match target +// Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()` +// It will also return true if any of the contained errors match target func (me MultiError) Is(target error) bool { if _, ok := target.(MultiError); ok { return true @@ -32,7 +32,7 @@ func (me MultiError) Is(target error) bool { return false } -//As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type +// As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type func (me MultiError) As(target interface{}) bool { for _, e := range me { if errors.As(e, target) { diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index 3a6cabb1a..3e19b79a2 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -322,7 +322,7 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso // // Example: // -// doc.InternalizeRefs(context.Background(), nil) +// doc.InternalizeRefs(context.Background(), nil) func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) { if refNameResolver == nil { refNameResolver = DefaultRefNameResolver diff --git a/openapi3/paths.go b/openapi3/paths.go index b4ebe582a..be7f3dc42 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -98,10 +98,10 @@ func (paths Paths) Validate(ctx context.Context) error { // // For example: // -// paths := openapi3.Paths { -// "/person/{personName}": &openapi3.PathItem{}, -// } -// pathItem := path.Find("/person/{name}") +// paths := openapi3.Paths { +// "/person/{personName}": &openapi3.PathItem{}, +// } +// pathItem := path.Find("/person/{name}") // // would return the correct path item. func (paths Paths) Find(key string) *PathItem { diff --git a/routers/legacy/pathpattern/node.go b/routers/legacy/pathpattern/node.go index 862199864..011dda358 100644 --- a/routers/legacy/pathpattern/node.go +++ b/routers/legacy/pathpattern/node.go @@ -1,11 +1,11 @@ // Package pathpattern implements path matching. // // Examples of supported patterns: -// * "/" -// * "/abc"" -// * "/abc/{variable}" (matches until next '/' or end-of-string) -// * "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has noot) -// * "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions) +// - "/" +// - "/abc"" +// - "/abc/{variable}" (matches until next '/' or end-of-string) +// - "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has noot) +// - "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions) package pathpattern import ( @@ -28,8 +28,8 @@ type Options struct { // PathFromHost converts a host pattern to a path pattern. // // Examples: -// * PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain" -// * PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some" +// - PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain" +// - PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some" func PathFromHost(host string, specialDashes bool) string { buf := make([]byte, 0, len(host)) end := len(host) From 81548297bf637768cc4eecb6b0e3f9a462bef4a7 Mon Sep 17 00:00:00 2001 From: Masumi Kanai Date: Mon, 29 Aug 2022 23:28:37 +0900 Subject: [PATCH 168/260] Fix `resolveSchemaRef()` to load correctly an other spec. file referenced by `$ref` (#583) --- ...oad_cicular_ref_with_external_file_test.go | 82 +++++++++++++++++++ openapi3/loader.go | 19 ++++- openapi3/testdata/circularRef/base.yml | 16 ++++ openapi3/testdata/circularRef/other.yml | 10 +++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 openapi3/load_cicular_ref_with_external_file_test.go create mode 100644 openapi3/testdata/circularRef/base.yml create mode 100644 openapi3/testdata/circularRef/other.yml diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go new file mode 100644 index 000000000..85b127e93 --- /dev/null +++ b/openapi3/load_cicular_ref_with_external_file_test.go @@ -0,0 +1,82 @@ +//go:build go1.16 +// +build go1.16 + +package openapi3_test + +import ( + "embed" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +//go:embed testdata/circularRef/* +var circularResSpecs embed.FS + +func TestLoadCircularRefFromFile(t *testing.T) { + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, uri *url.URL) ([]byte, error) { + return circularResSpecs.ReadFile(uri.Path) + } + + got, err := loader.LoadFromFile("testdata/circularRef/base.yml") + if err != nil { + t.Error(err) + } + + foo := &openapi3.SchemaRef{ + Ref: "", + Value: &openapi3.Schema{ + ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, + Properties: map[string]*openapi3.SchemaRef{ + "foo2": { // reference to an external file + Ref: "other.yml#/components/schemas/Foo2", + Value: &openapi3.Schema{ + ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, + Properties: map[string]*openapi3.SchemaRef{ + "id": { + Value: &openapi3.Schema{ + Type: "string", + ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, + }}, + }, + }, + }, + }, + }, + } + bar := &openapi3.SchemaRef{ + Ref: "", + Value: &openapi3.Schema{ + ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, + Properties: map[string]*openapi3.SchemaRef{}, + }, + } + // circular reference + bar.Value.Properties["foo"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Foo", Value: foo.Value} + foo.Value.Properties["bar"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Bar", Value: bar.Value} + + want := &openapi3.T{ + OpenAPI: "3.0.3", + Info: &openapi3.Info{ + Title: "Recursive cyclic refs example", + Version: "1.0", + + ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, + }, + Components: openapi3.Components{ + ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, + Schemas: map[string]*openapi3.SchemaRef{ + "Foo": foo, + "Bar": bar, + }, + }, + ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, + } + + require.Equal(t, want, got) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index e2f131e40..c981a3382 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -699,7 +699,8 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + foundPath := loader.getResolvedRefPath(ref, &resolved, documentPath, componentPath) + documentPath = loader.documentPathForRecursiveRef(documentPath, foundPath) } } value := component.Value @@ -746,6 +747,22 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return nil } +func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) string { + referencedFilename := strings.Split(ref, "#")[0] + if referencedFilename == "" { + if cur != nil { + return path.Base(cur.Path) + } + return "" + } + // ref. to external file + if resolved.Ref != "" { + return resolved.Ref + } + // found dest spec. file + return path.Dir(found.Path)[len(loader.rootDir):] +} + func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedSecurityScheme == nil { diff --git a/openapi3/testdata/circularRef/base.yml b/openapi3/testdata/circularRef/base.yml new file mode 100644 index 000000000..ff8240eb0 --- /dev/null +++ b/openapi3/testdata/circularRef/base.yml @@ -0,0 +1,16 @@ +openapi: "3.0.3" +info: + title: Recursive cyclic refs example + version: "1.0" +components: + schemas: + Foo: + properties: + foo2: + $ref: "other.yml#/components/schemas/Foo2" + bar: + $ref: "#/components/schemas/Bar" + Bar: + properties: + foo: + $ref: "#/components/schemas/Foo" diff --git a/openapi3/testdata/circularRef/other.yml b/openapi3/testdata/circularRef/other.yml new file mode 100644 index 000000000..29b72d98c --- /dev/null +++ b/openapi3/testdata/circularRef/other.yml @@ -0,0 +1,10 @@ +openapi: "3.0.3" +info: + title: Recursive cyclic refs example + version: "1.0" +components: + schemas: + Foo2: + properties: + id: + type: string From 2470727b7446a3ed3578a74c34a3771cf4039af4 Mon Sep 17 00:00:00 2001 From: wtertius Date: Mon, 29 Aug 2022 17:36:10 +0300 Subject: [PATCH 169/260] Protect from recursion in openapi3.InternaliseRefs (#578) Co-authored-by: Dmitriy Lukiyanchuk --- openapi3/internalize_refs.go | 7 +++- openapi3/openapi3.go | 2 + .../testdata/recursiveRef/components/Cat.yml | 4 ++ openapi3/testdata/recursiveRef/openapi.yml | 2 + .../recursiveRef/openapi.yml.internalized.yml | 8 ++++ openapi3/visited.go | 41 +++++++++++++++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 openapi3/testdata/recursiveRef/components/Cat.yml create mode 100644 openapi3/visited.go diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index 3e19b79a2..1733a495e 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -200,7 +200,7 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver) } func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) { - if s == nil { + if s == nil || doc.isVisitedSchema(s) { return } @@ -229,6 +229,9 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) { func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver) { for _, h := range hs { doc.addHeaderToSpec(h, refNameResolver) + if doc.isVisitedHeader(h.Value) { + continue + } doc.derefParameter(h.Value.Parameter, refNameResolver) } } @@ -324,6 +327,8 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso // // doc.InternalizeRefs(context.Background(), nil) func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) { + doc.resetVisited() + if refNameResolver == nil { refNameResolver = DefaultRefNameResolver } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index c0188c25a..cb9183d47 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -21,6 +21,8 @@ type T struct { Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + + visited visitedComponent } // MarshalJSON returns the JSON encoding of T. diff --git a/openapi3/testdata/recursiveRef/components/Cat.yml b/openapi3/testdata/recursiveRef/components/Cat.yml new file mode 100644 index 000000000..c476aa1a5 --- /dev/null +++ b/openapi3/testdata/recursiveRef/components/Cat.yml @@ -0,0 +1,4 @@ +type: object +properties: + cat: + $ref: ../openapi.yml#/components/schemas/Cat diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml index 3559c8e85..675722a60 100644 --- a/openapi3/testdata/recursiveRef/openapi.yml +++ b/openapi3/testdata/recursiveRef/openapi.yml @@ -13,3 +13,5 @@ components: $ref: ./components/Foo/Foo2.yml Bar: $ref: ./components/Bar.yml + Cat: + $ref: ./components/Cat.yml diff --git a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml index d1260eb14..073059025 100644 --- a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml +++ b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml @@ -29,6 +29,14 @@ } }, "type": "object" + }, + "Cat": { + "properties": { + "cat": { + "$ref": "#/components/schemas/Cat" + } + }, + "type": "object" } } }, diff --git a/openapi3/visited.go b/openapi3/visited.go new file mode 100644 index 000000000..67f970e36 --- /dev/null +++ b/openapi3/visited.go @@ -0,0 +1,41 @@ +package openapi3 + +func newVisited() visitedComponent { + return visitedComponent{ + header: make(map[*Header]struct{}), + schema: make(map[*Schema]struct{}), + } +} + +type visitedComponent struct { + header map[*Header]struct{} + schema map[*Schema]struct{} +} + +// resetVisited clears visitedComponent map +// should be called before recursion over doc *T +func (doc *T) resetVisited() { + doc.visited = newVisited() +} + +// isVisitedHeader returns `true` if the *Header pointer was already visited +// otherwise it returns `false` +func (doc *T) isVisitedHeader(h *Header) bool { + if _, ok := doc.visited.header[h]; ok { + return true + } + + doc.visited.header[h] = struct{}{} + return false +} + +// isVisitedHeader returns `true` if the *Schema pointer was already visited +// otherwise it returns `false` +func (doc *T) isVisitedSchema(s *Schema) bool { + if _, ok := doc.visited.schema[s]; ok { + return true + } + + doc.visited.schema[s] = struct{}{} + return false +} From efe7ae95881bf7597716d5fa273a216f3b5ae5f9 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 29 Aug 2022 16:36:36 +0200 Subject: [PATCH 170/260] cleanup after #583 (#585) --- ...oad_cicular_ref_with_external_file_test.go | 36 +++++++------------ openapi3/loader.go | 3 +- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go index 85b127e93..978ef8f38 100644 --- a/openapi3/load_cicular_ref_with_external_file_test.go +++ b/openapi3/load_cicular_ref_with_external_file_test.go @@ -5,6 +5,7 @@ package openapi3_test import ( "embed" + "encoding/json" "net/url" "testing" @@ -24,38 +25,24 @@ func TestLoadCircularRefFromFile(t *testing.T) { } got, err := loader.LoadFromFile("testdata/circularRef/base.yml") - if err != nil { - t.Error(err) - } + require.NoError(t, err) foo := &openapi3.SchemaRef{ - Ref: "", Value: &openapi3.Schema{ - ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, Properties: map[string]*openapi3.SchemaRef{ - "foo2": { // reference to an external file - Ref: "other.yml#/components/schemas/Foo2", + "foo2": { + Ref: "other.yml#/components/schemas/Foo2", // reference to an external file Value: &openapi3.Schema{ - ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, Properties: map[string]*openapi3.SchemaRef{ "id": { - Value: &openapi3.Schema{ - Type: "string", - ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, - }}, + Value: &openapi3.Schema{Type: "string"}}, }, }, }, }, }, } - bar := &openapi3.SchemaRef{ - Ref: "", - Value: &openapi3.Schema{ - ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, - Properties: map[string]*openapi3.SchemaRef{}, - }, - } + bar := &openapi3.SchemaRef{Value: &openapi3.Schema{Properties: make(map[string]*openapi3.SchemaRef)}} // circular reference bar.Value.Properties["foo"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Foo", Value: foo.Value} foo.Value.Properties["bar"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Bar", Value: bar.Value} @@ -65,18 +52,19 @@ func TestLoadCircularRefFromFile(t *testing.T) { Info: &openapi3.Info{ Title: "Recursive cyclic refs example", Version: "1.0", - - ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, }, Components: openapi3.Components{ - ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, Schemas: map[string]*openapi3.SchemaRef{ "Foo": foo, "Bar": bar, }, }, - ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}}, } - require.Equal(t, want, got) + jsoner := func(doc *openapi3.T) string { + data, err := json.Marshal(doc) + require.NoError(t, err) + return string(data) + } + require.JSONEq(t, jsoner(want), jsoner(got)) } diff --git a/openapi3/loader.go b/openapi3/loader.go index c981a3382..806b819df 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -748,8 +748,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat } func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) string { - referencedFilename := strings.Split(ref, "#")[0] - if referencedFilename == "" { + if referencedFilename := strings.Split(ref, "#")[0]; referencedFilename == "" { if cur != nil { return path.Base(cur.Path) } From 40bb5a1e2d2b3b22545d5aec7005fa0e6fd2f06d Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 29 Aug 2022 16:39:51 +0200 Subject: [PATCH 171/260] upgrade CI tools (#586) --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 70b6db6d9..d34061a04 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -66,7 +66,7 @@ jobs: env: GOARCH: '386' - run: go test ./... - - run: go test -count=2 ./... + - run: go test -count=2 -covermode=atomic ./... - run: go test -v -run TestRaceyPatternSchema -race ./... env: CGO_ENABLED: '1' @@ -151,7 +151,7 @@ jobs: - uses: actions/setup-go@v3 with: go-version: '>=1.17.0' - - run: go install github.com/incu6us/goimports-reviser/v2@v2.5.1 + - run: go install github.com/incu6us/goimports-reviser/v2@latest - run: which goimports-reviser - run: find . -type f -iname '*.go' ! -iname '*.pb.go' -exec goimports-reviser -file-path {} \; - run: git --no-pager diff --exit-code From 14af893c192c2effe120dc18f1db20d2d078f8cc Mon Sep 17 00:00:00 2001 From: Christian Boitel <40855349+cboitel@users.noreply.github.com> Date: Wed, 31 Aug 2022 11:01:15 +0200 Subject: [PATCH 172/260] #482 integer support broken with yaml (#577) Co-authored-by: Christian Boitel --- openapi3/schema.go | 47 +++++++++++++- openapi3/schema_test.go | 133 ++++++++++++++++++++++++++++++---------- 2 files changed, 147 insertions(+), 33 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 4c8df1cba..17f547e66 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -24,6 +24,12 @@ const ( TypeNumber = "number" TypeObject = "object" TypeString = "string" + + // constants for integer formats + formatMinInt32 = float64(math.MinInt32) + formatMaxInt32 = float64(math.MaxInt32) + formatMinInt64 = float64(math.MinInt64) + formatMaxInt64 = float64(math.MaxInt64) ) var ( @@ -808,6 +814,12 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf switch value := value.(type) { case bool: return schema.visitJSONBoolean(settings, value) + case int: + return schema.visitJSONNumber(settings, float64(value)) + case int32: + return schema.visitJSONNumber(settings, float64(value)) + case int64: + return schema.visitJSONNumber(settings, float64(value)) case float64: return schema.visitJSONNumber(settings, value) case string: @@ -1019,7 +1031,7 @@ func (schema *Schema) VisitJSONNumber(value float64) error { func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error { var me MultiError schemaType := schema.Type - if schemaType == "integer" { + if schemaType == TypeInteger { if bigFloat := big.NewFloat(value); !bigFloat.IsInt() { if settings.failfast { return errSchema @@ -1039,6 +1051,39 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value return schema.expectedType(settings, "number, integer") } + // formats + if schemaType == TypeInteger && schema.Format != "" { + formatMin := float64(0) + formatMax := float64(0) + switch schema.Format { + case "int32": + formatMin = formatMinInt32 + formatMax = formatMaxInt32 + case "int64": + formatMin = formatMinInt64 + formatMax = formatMaxInt64 + default: + if !SchemaFormatValidationDisabled { + return unsupportedFormat(schema.Format) + } + } + if formatMin != 0 && formatMax != 0 && !(formatMin <= value && value <= formatMax) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "format", + Reason: fmt.Sprintf("number must be an %s", schema.Format), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + } + // "exclusiveMinimum" if v := schema.ExclusiveMin; v && !(*schema.Min < value) { if settings.failfast { diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index abdfb3491..593fa17aa 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -4,13 +4,13 @@ import ( "context" "encoding/base64" "encoding/json" - "fmt" "math" "reflect" "strings" "testing" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) type schemaExample struct { @@ -45,23 +45,25 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) { require.NoError(t, err) require.Equal(t, dataUnserialized, dataSchema) } - for _, value := range example.AllValid { - err := validateSchema(t, schema, value) - require.NoError(t, err) - } - for _, value := range example.AllInvalid { - err := validateSchema(t, schema, value) - require.Error(t, err) + for validateFuncIndex, validateFunc := range validateSchemaFuncs { + for index, value := range example.AllValid { + err := validateFunc(t, schema, value) + require.NoErrorf(t, err, "ValidateFunc #%d, AllValid #%d: %#v", validateFuncIndex, index, value) + } + for index, value := range example.AllInvalid { + err := validateFunc(t, schema, value) + require.Errorf(t, err, "ValidateFunc #%d, AllInvalid #%d: %#v", validateFuncIndex, index, value) + } } // NaN and Inf aren't valid JSON but are handled - for _, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} { + for index, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} { err := schema.VisitJSON(value) - require.Error(t, err) + require.Errorf(t, err, "NaNAndInf #%d: %#v", index, value) } } } -func validateSchema(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error { +func validateSchemaJSON(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error { data, err := json.Marshal(value) require.NoError(t, err) var val interface{} @@ -70,6 +72,22 @@ func validateSchema(t *testing.T, schema *Schema, value interface{}, opts ...Sch return schema.VisitJSON(val, opts...) } +func validateSchemaYAML(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error { + data, err := yaml.Marshal(value) + require.NoError(t, err) + var val interface{} + err = yaml.Unmarshal(data, &val) + require.NoError(t, err) + return schema.VisitJSON(val, opts...) +} + +type validateSchemaFunc func(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error + +var validateSchemaFuncs = []validateSchemaFunc{ + validateSchemaJSON, + validateSchemaYAML, +} + var schemaExamples = []schemaExample{ { Title: "EMPTY SCHEMA", @@ -234,7 +252,56 @@ var schemaExamples = []schemaExample{ map[string]interface{}{}, }, }, - + { + Title: "INTEGER OPTIONAL INT64 FORMAT", + Schema: NewInt64Schema(), + Serialization: map[string]interface{}{ + "type": "integer", + "format": "int64", + }, + AllValid: []interface{}{ + 1, + 256, + 65536, + int64(math.MaxInt32) + 10, + int64(math.MinInt32) - 10, + }, + AllInvalid: []interface{}{ + nil, + false, + 3.5, + true, + "", + []interface{}{}, + map[string]interface{}{}, + }, + }, + { + Title: "INTEGER OPTIONAL INT32 FORMAT", + Schema: NewInt32Schema(), + Serialization: map[string]interface{}{ + "type": "integer", + "format": "int32", + }, + AllValid: []interface{}{ + 1, + 256, + 65536, + int64(math.MaxInt32), + int64(math.MaxInt32), + }, + AllInvalid: []interface{}{ + nil, + false, + 3.5, + int64(math.MaxInt32) + 10, + int64(math.MinInt32) - 10, + true, + "", + []interface{}{}, + map[string]interface{}{}, + }, + }, { Title: "STRING", Schema: NewStringSchema(). @@ -350,7 +417,7 @@ var schemaExamples = []schemaExample{ AllInvalid: []interface{}{ nil, " ", - "\n", + "\n\n", // a \n is ok for JSON but not for YAML decoder/encoder "%", }, }, @@ -1074,28 +1141,30 @@ func TestSchemasMultiError(t *testing.T) { func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*testing.T) { return func(t *testing.T) { schema := example.Schema - for i, value := range example.Values { - err := validateSchema(t, schema, value, MultiErrors()) - require.Error(t, err) - require.IsType(t, MultiError{}, err) + for validateFuncIndex, validateFunc := range validateSchemaFuncs { + for i, value := range example.Values { + err := validateFunc(t, schema, value, MultiErrors()) + require.Errorf(t, err, "ValidateFunc #%d, value #%d: %#", validateFuncIndex, i, value) + require.IsType(t, MultiError{}, err) - merr, _ := err.(MultiError) - expected := example.ExpectedErrors[i] - require.True(t, len(merr) > 0) - require.Len(t, merr, len(expected)) - for _, e := range merr { - require.IsType(t, &SchemaError{}, e) - var found bool - scherr, _ := e.(*SchemaError) - for _, expectedErr := range expected { - expectedScherr, _ := expectedErr.(*SchemaError) - if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) && - expectedScherr.SchemaField == scherr.SchemaField { - found = true - break + merr, _ := err.(MultiError) + expected := example.ExpectedErrors[i] + require.True(t, len(merr) > 0) + require.Len(t, merr, len(expected)) + for _, e := range merr { + require.IsType(t, &SchemaError{}, e) + var found bool + scherr, _ := e.(*SchemaError) + for _, expectedErr := range expected { + expectedScherr, _ := expectedErr.(*SchemaError) + if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) && + expectedScherr.SchemaField == scherr.SchemaField { + found = true + break + } } + require.Truef(t, found, "ValidateFunc #%d, value #%d: missing %s error on %s", validateFunc, i, scherr.SchemaField, strings.Join(scherr.JSONPointer(), ".")) } - require.True(t, found, fmt.Sprintf("missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))) } } } From 5a61040f9c83432717fde755d16649eca5e21038 Mon Sep 17 00:00:00 2001 From: Amarjeet Rai Date: Wed, 31 Aug 2022 10:06:33 +0100 Subject: [PATCH 173/260] Match on overridden servers at the path level, fixes #564 (#565) Co-authored-by: Pierre Fenoll --- routers/gorillamux/router.go | 150 +++++++++++++++++------------- routers/gorillamux/router_test.go | 41 ++++++++ 2 files changed, 125 insertions(+), 66 deletions(-) diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index bf551a751..811ba7d16 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -34,6 +34,13 @@ type routeMux struct { varsUpdater varsf } +type srv struct { + schemes []string + host, base string + server *openapi3.Server + varsUpdater varsf +} + var singleVariableMatcher = regexp.MustCompile(`^\{([^{}]+)\}$`) // TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request)) @@ -42,78 +49,22 @@ var singleVariableMatcher = regexp.MustCompile(`^\{([^{}]+)\}$`) // Assumes spec is .Validate()d // Note that a variable for the port number MUST have a default value and only this value will match as the port (see issue #367). func NewRouter(doc *openapi3.T) (routers.Router, error) { - type srv struct { - schemes []string - host, base string - server *openapi3.Server - varsUpdater varsf + servers, err := makeServers(doc.Servers) + if err != nil { + return nil, err } - servers := make([]srv, 0, len(doc.Servers)) - for _, server := range doc.Servers { - serverURL := server.URL - if submatch := singleVariableMatcher.FindStringSubmatch(serverURL); submatch != nil { - sVar := submatch[1] - sVal := server.Variables[sVar].Default - serverURL = strings.ReplaceAll(serverURL, "{"+sVar+"}", sVal) - var varsUpdater varsf - if lhs := strings.TrimSuffix(serverURL, server.Variables[sVar].Default); lhs != "" { - varsUpdater = func(vars map[string]string) { vars[sVar] = lhs } - } - servers = append(servers, srv{ - base: server.Variables[sVar].Default, - server: server, - varsUpdater: varsUpdater, - }) - continue - } - - var schemes []string - if strings.Contains(serverURL, "://") { - scheme0 := strings.Split(serverURL, "://")[0] - schemes = permutePart(scheme0, server) - serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1) - } - // If a variable represents the port "http://domain.tld:{port}/bla" - // then url.Parse() cannot parse "http://domain.tld:`bEncode({port})`/bla" - // and mux is not able to set the {port} variable - // So we just use the default value for this variable. - // See https://github.com/getkin/kin-openapi/issues/367 - var varsUpdater varsf - if lhs := strings.Index(serverURL, ":{"); lhs > 0 { - rest := serverURL[lhs+len(":{"):] - rhs := strings.Index(rest, "}") - portVariable := rest[:rhs] - portValue := server.Variables[portVariable].Default - serverURL = strings.ReplaceAll(serverURL, "{"+portVariable+"}", portValue) - varsUpdater = func(vars map[string]string) { - vars[portVariable] = portValue - } - } - - u, err := url.Parse(bEncode(serverURL)) - if err != nil { - return nil, err - } - path := bDecode(u.EscapedPath()) - if len(path) > 0 && path[len(path)-1] == '/' { - path = path[:len(path)-1] - } - servers = append(servers, srv{ - host: bDecode(u.Host), //u.Hostname()? - base: path, - schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624 - server: server, - varsUpdater: varsUpdater, - }) - } - if len(servers) == 0 { - servers = append(servers, srv{}) - } muxRouter := mux.NewRouter().UseEncodedPath() r := &Router{} for _, path := range orderedPaths(doc.Paths) { + servers := servers + pathItem := doc.Paths[path] + if len(pathItem.Servers) > 0 { + if servers, err = makeServers(pathItem.Servers); err != nil { + return nil, err + } + } operations := pathItem.Operations() methods := make([]string, 0, len(operations)) @@ -177,6 +128,73 @@ func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string return nil, nil, routers.ErrPathNotFound } +func makeServers(in openapi3.Servers) ([]srv, error) { + servers := make([]srv, 0, len(in)) + for _, server := range in { + serverURL := server.URL + if submatch := singleVariableMatcher.FindStringSubmatch(serverURL); submatch != nil { + sVar := submatch[1] + sVal := server.Variables[sVar].Default + serverURL = strings.ReplaceAll(serverURL, "{"+sVar+"}", sVal) + var varsUpdater varsf + if lhs := strings.TrimSuffix(serverURL, server.Variables[sVar].Default); lhs != "" { + varsUpdater = func(vars map[string]string) { vars[sVar] = lhs } + } + servers = append(servers, srv{ + base: server.Variables[sVar].Default, + server: server, + varsUpdater: varsUpdater, + }) + continue + } + + var schemes []string + if strings.Contains(serverURL, "://") { + scheme0 := strings.Split(serverURL, "://")[0] + schemes = permutePart(scheme0, server) + serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1) + } + + // If a variable represents the port "http://domain.tld:{port}/bla" + // then url.Parse() cannot parse "http://domain.tld:`bEncode({port})`/bla" + // and mux is not able to set the {port} variable + // So we just use the default value for this variable. + // See https://github.com/getkin/kin-openapi/issues/367 + var varsUpdater varsf + if lhs := strings.Index(serverURL, ":{"); lhs > 0 { + rest := serverURL[lhs+len(":{"):] + rhs := strings.Index(rest, "}") + portVariable := rest[:rhs] + portValue := server.Variables[portVariable].Default + serverURL = strings.ReplaceAll(serverURL, "{"+portVariable+"}", portValue) + varsUpdater = func(vars map[string]string) { + vars[portVariable] = portValue + } + } + + u, err := url.Parse(bEncode(serverURL)) + if err != nil { + return nil, err + } + path := bDecode(u.EscapedPath()) + if len(path) > 0 && path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + servers = append(servers, srv{ + host: bDecode(u.Host), //u.Hostname()? + base: path, + schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624 + server: server, + varsUpdater: varsUpdater, + }) + } + if len(servers) == 0 { + servers = append(servers, srv{}) + } + + return servers, nil +} + func orderedPaths(paths map[string]*openapi3.PathItem) []string { // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathsObject // When matching URLs, concrete (non-templated) paths would be matched diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index f8800baed..104056e18 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -254,6 +254,47 @@ func TestServerPath(t *testing.T) { require.NoError(t, err) } +func TestServerOverrideAtPathLevel(t *testing.T) { + helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} + doc := &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "rel", + Version: "1", + }, + Servers: openapi3.Servers{ + &openapi3.Server{ + URL: "https://example.com", + }, + }, + Paths: openapi3.Paths{ + "/hello": &openapi3.PathItem{ + Servers: openapi3.Servers{ + &openapi3.Server{ + URL: "https://another.com", + }, + }, + Get: helloGET, + }, + }, + } + err := doc.Validate(context.Background()) + require.NoError(t, err) + router, err := NewRouter(doc) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, "https://another.com/hello", nil) + require.NoError(t, err) + route, _, err := router.FindRoute(req) + require.Equal(t, "/hello", route.Path) + + req, err = http.NewRequest(http.MethodGet, "https://example.com/hello", nil) + require.NoError(t, err) + route, _, err = router.FindRoute(req) + require.Nil(t, route) + require.Error(t, err) +} + func TestRelativeURL(t *testing.T) { helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} doc := &openapi3.T{ From de022f1edf9940694a81e12ef05771a318c3fac7 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge <108070248+TristanSpeakEasy@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:48:28 +0100 Subject: [PATCH 174/260] feat: support validation options specifically for disabling pattern validation (#590) --- README.md | 3 ++ openapi3/loader.go | 6 ++-- openapi3/openapi3.go | 9 ++++- openapi3/schema.go | 18 +++++----- openapi3/schema_test.go | 7 ++-- openapi3/schema_validation_settings.go | 19 ++++++++-- openapi3/testdata/issue409.yml | 21 +++++++++++ openapi3/validation_issue409_test.go | 50 ++++++++++++++++++++++++++ openapi3/validation_options.go | 40 +++++++++++++++++++++ 9 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 openapi3/testdata/issue409.yml create mode 100644 openapi3/validation_issue409_test.go create mode 100644 openapi3/validation_options.go diff --git a/README.md b/README.md index a829e5983..ae87783a2 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,9 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.101.0 +* `openapi3.SchemaFormatValidationDisabled` has been removed in favour of an option `openapi3.EnableSchemaFormatValidation()` passed to `openapi3.T.Validate`. The default behaviour is also now to not validate formats, as the OpenAPI spec mentions the `format` is an open value. + ### v0.84.0 * The prototype of `openapi3gen.NewSchemaRefForValue` changed: * It no longer returns a map but that is still accessible under the field `(*Generator).SchemaRefs`. diff --git a/openapi3/loader.go b/openapi3/loader.go index 806b819df..87c9f8684 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -166,6 +166,10 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR // ResolveRefsIn expands references if for instance spec was just unmarshalled func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { + if loader.Context == nil { + loader.Context = context.Background() + } + if loader.visitedPathItemRefs == nil { loader.resetVisitedPathItemRefs() } @@ -406,7 +410,6 @@ func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef return current } return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)} - } func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { @@ -837,7 +840,6 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP } func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) { - if component == nil { return errors.New("invalid callback: value MUST be an object") } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index cb9183d47..20549c2b7 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -54,7 +54,14 @@ func (doc *T) AddServer(server *Server) { } // Validate returns an error if T does not comply with the OpenAPI spec. -func (doc *T) Validate(ctx context.Context) error { +// Validations Options can be provided to modify the validation behavior. +func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { + validationOpts := &ValidationOptions{} + for _, opt := range opts { + opt(validationOpts) + } + ctx = WithValidationOptions(ctx, validationOpts) + if doc.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") } diff --git a/openapi3/schema.go b/openapi3/schema.go index 17f547e66..57f63fbd8 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -36,9 +36,6 @@ var ( // SchemaErrorDetailsDisabled disables printing of details about schema errors. SchemaErrorDetailsDisabled = false - //SchemaFormatValidationDisabled disables validation of schema type formats. - SchemaFormatValidationDisabled = false - errSchema = errors.New("input does not match the schema") // ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema @@ -403,6 +400,7 @@ func (schema *Schema) WithMax(value float64) *Schema { schema.Max = &value return schema } + func (schema *Schema) WithExclusiveMin(value bool) *Schema { schema.ExclusiveMin = value return schema @@ -606,6 +604,8 @@ func (schema *Schema) Validate(ctx context.Context) error { } func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { + validationOpts := getValidationOptions(ctx) + for _, existing := range stack { if existing == schema { return @@ -666,7 +666,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "float", "double": default: - if !SchemaFormatValidationDisabled { + if validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -676,7 +676,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "int32", "int64": default: - if !SchemaFormatValidationDisabled { + if validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -698,12 +698,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled { + if _, ok := SchemaStringFormats[format]; !ok && validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } } - if schema.Pattern != "" { + if schema.Pattern != "" && !validationOpts.SchemaPatternValidationDisabled { if err = schema.compilePattern(); err != nil { return err } @@ -1063,7 +1063,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value formatMin = formatMinInt64 formatMax = formatMaxInt64 default: - if !SchemaFormatValidationDisabled { + if settings.formatValidationEnabled { return unsupportedFormat(schema.Format) } } @@ -1237,7 +1237,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } // "pattern" - if schema.Pattern != "" && schema.compiledPattern == nil { + if schema.Pattern != "" && schema.compiledPattern == nil && !settings.patternValidationDisabled { var err error if err = schema.compilePattern(); err != nil { if !settings.multiError { diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 593fa17aa..4c14dcb10 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1028,7 +1028,10 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) - err := schema.Validate(context.Background()) + ctx := WithValidationOptions(context.Background(), &ValidationOptions{ + SchemaFormatValidationEnabled: true, + }) + err := schema.Validate(ctx) require.Error(t, err) } } @@ -1308,6 +1311,6 @@ func TestValidationFailsOnInvalidPattern(t *testing.T) { Type: "string", } - var err = schema.Validate(context.Background()) + err := schema.Validate(context.Background()) require.Error(t, err) } diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index cb4c142a4..854ae8480 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -8,9 +8,11 @@ import ( type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { - failfast bool - multiError bool - asreq, asrep bool // exclusive (XOR) fields + failfast bool + multiError bool + asreq, asrep bool // exclusive (XOR) fields + formatValidationEnabled bool + patternValidationDisabled bool onceSettingDefaults sync.Once defaultsSet func() @@ -28,10 +30,21 @@ func MultiErrors() SchemaValidationOption { func VisitAsRequest() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false } } + func VisitAsResponse() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true } } +// EnableFormatValidation setting makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. +func EnableFormatValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.formatValidationEnabled = true } +} + +// DisablePatternValidation setting makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. +func DisablePatternValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.patternValidationDisabled = true } +} + // DefaultsSet executes the given callback (once) IFF schema validation set default values. func DefaultsSet(f func()) SchemaValidationOption { return func(s *schemaValidationSettings) { s.defaultsSet = f } diff --git a/openapi3/testdata/issue409.yml b/openapi3/testdata/issue409.yml new file mode 100644 index 000000000..88394904e --- /dev/null +++ b/openapi3/testdata/issue409.yml @@ -0,0 +1,21 @@ +openapi: 3.0.3 +info: + description: Contains Patterns that can't be compiled by the go regexp engine + title: Issue 409 + version: 0.0.1 +paths: + /v1/apis/{apiID}: + get: + description: Get a list of all Apis and there versions for a given workspace + operationId: getApisV1 + parameters: + - description: The ID of the API + in: path + name: apiID + required: true + schema: + type: string + pattern: ^[a-zA-Z0-9]{0,4096}$ + responses: + "200": + description: OK diff --git a/openapi3/validation_issue409_test.go b/openapi3/validation_issue409_test.go new file mode 100644 index 000000000..561594fca --- /dev/null +++ b/openapi3/validation_issue409_test.go @@ -0,0 +1,50 @@ +package openapi3_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestIssue409PatternIgnored(t *testing.T) { + l := openapi3.NewLoader() + s, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = s.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) +} + +func TestIssue409PatternNotIgnored(t *testing.T) { + l := openapi3.NewLoader() + s, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = s.Validate(l.Context) + assert.Error(t, err) +} + +func TestIssue409HygienicUseOfCtx(t *testing.T) { + l := openapi3.NewLoader() + doc, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) + err = doc.Validate(l.Context) + assert.Error(t, err) + + // and the other way + + l = openapi3.NewLoader() + doc, err = l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = doc.Validate(l.Context) + assert.Error(t, err) + err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) +} diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go new file mode 100644 index 000000000..f6038ed10 --- /dev/null +++ b/openapi3/validation_options.go @@ -0,0 +1,40 @@ +package openapi3 + +import "context" + +// ValidationOption allows the modification of how the OpenAPI document is validated. +type ValidationOption func(options *ValidationOptions) + +// ValidationOptions provide configuration for validating OpenAPI documents. +type ValidationOptions struct { + SchemaFormatValidationEnabled bool + SchemaPatternValidationDisabled bool +} + +type validationOptionsKey struct{} + +// EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. +func EnableSchemaFormatValidation() ValidationOption { + return func(options *ValidationOptions) { + options.SchemaFormatValidationEnabled = true + } +} + +// DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. +func DisableSchemaPatternValidation() ValidationOption { + return func(options *ValidationOptions) { + options.SchemaPatternValidationDisabled = true + } +} + +// WithValidationOptions allows adding validation options to a context object that can be used when validationg any OpenAPI type. +func WithValidationOptions(ctx context.Context, options *ValidationOptions) context.Context { + return context.WithValue(ctx, validationOptionsKey{}, options) +} + +func getValidationOptions(ctx context.Context) *ValidationOptions { + if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok { + return options + } + return &ValidationOptions{} +} From 46603c3b350c28a74763bda34fd4fe2142a76145 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 14 Sep 2022 17:00:37 +0200 Subject: [PATCH 175/260] Add sponsor logo (#595) --- .github/sponsors/speakeasy.png | Bin 0 -> 12425 bytes README.md | 10 ++++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .github/sponsors/speakeasy.png diff --git a/.github/sponsors/speakeasy.png b/.github/sponsors/speakeasy.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d84210765503fc7a7f107f4b5f6da5f3b0e8c1 GIT binary patch literal 12425 zcmd72_dnaw7e8*bN{!a4T_g4?YHzg(sz$A75i|DQimF+}o>6Ad|r=7l5?Kt-t)Szdy?FH&v{1cYO4|x(h_1}VG*mVDd}Nh;riXl zUgIEaG0Y2ix?0jBLQ*>yRZ|Iy&rKJp)4}5KD&1 zH)*kj@bFIH_mhV-TaRC*D_YuF%T4@z)XB6(WVl~bf_FEryP>d_P>K7V?aLmw=)5QecfVgJ>>nm(}_q)|7LXJf6b3hH9%cq90Z_n z8voBnbEG;C{u=Tl%8ZVV__BxxPR^H+3X1<)vnge-F-T*+AdC32Y~NnG!~Vetqac{{Unx{n z0gC@udh{6ZpFrJRkNHTP4H=|93k0D1lH89$!tZ?*F=o<$D>Pi7LTtPIO~bDg(Ul)#_;d zb%#~$Z3*=`*jWlOl)t1Xpd9LMvI<^xBU}4#% z@WM^?uAmPBgztLMzTeCX3gB29R2J5jBWH%=V%br99qg>KXMOk=fIbDmOyYl!8Oq5i z6qx+)_DqB^;`6e;|CDERo@D<*6ZHW4-)KcK-TC^@6*WkP{ndzP0VKq}1=`Rp&~bbw zUD9~|(qEjm1g{jj-KzMUHa?Oytb4;dV4?R)R#X2&+ZPnptkva3=_pMn$>CMFYE<~j zI{>Lkxo_)PhG#-*t1}Uri|&V~5hGz?@xTFBNwDSVQQK87`!k!enT|1&;>gLLB>uP# z?^dUih|n?pKfcE*Ogs+i)Mja#=1z4~&iVi=6TXtBZ7vu)dhcA@qSPiAl>2e#TC_FS zrUNw&_j=y({HGOd^Pw`&bmPRQJlgGulnW%aM#dM%u7_G$+#$Bj(^|){$(Oy&Ac3+3 z;vCGt0DKd*M3P!6b#^ru!ays;DtcwIhl;hLZ3MFb$sS2D{}MjSL@X^J8=P-ftdB&_ zF5}iNXP$oCp}w%4TO0=GlHs$t3$u{K;69bb*gXidE%Wd?-E+`*CPwpfN7E~L=E~CL znlWj6sNRMNck#8rZr}46us|e<(`Uw8lQA!?%#qM9#}2o!UWS;yHAybvE_;i-@q|e) zw$&X;hXudK+rtJp(sYRs{SzWpw@ygt>Bo&YgZz73R(m_~i_YXf`p_5^GC7-T$su$+ zP$`A>_L7glI3h@Z&R?)(G(VnP#;Z?3h~|?zapK9v2S6=z;@XHnw!-@1i-2}?xhyN4 z?jA36kfJ9?81f_hdTj2MTy_V&ELRX>G-r{+M<7yt`MRuJ;7#k2NQ!&`OB^r4H|DOeS)WPFXMO5 zhn5z2ID{?hRCCn|^6Z3A9rS{?Hn8i55C5v!1Q8C+q_&U|mUFm7DPqgGXMQ&I^ux#G z_U_NqcJ1ow8f9qHiJH}jQuX)U;`{!s!6fl7TWm59KXSpdH44bx1!T@Y2c&Y>|Jrq; zW?bq$?#}Su+5Gl0mA;P0d=676<}>&%b*~9%(>y&fEG!c1v6_Dc zIuQQdt7VGW_j#IR<(OVeOnm6&E|BU&P~j+wQ>u0_l8*0CarGo)MR7m#yNS=uCB3+?=ycz88$^V3?mLY_quJw=P)Qg)GQ#X-nbHF?X*Xy3 z6t;y(#zg$1nj}XR5+pqALmpWN-1=<{pcZ?Fs5dm9f`BBwK69BVxqW}oo0jmk8y^@; zyG$kd$-8T5?Usfg8iovpwN-K?zCypHzI$D03yd67pq_at5O3Rn?b zDyH9)kIb!3(@L)1jIDvdgC3t-ub_#RNZpPsg+4|eEar7M{tDi5(tQE!P7h?Gi2V#7 z%;d?yFI?cLuTGQxJpBXdD{z=%PQ1wS6k;h&vy`|d5-Q5QVahL%Z_KBrpssy|PhK^|Uzx1>H zCGjyhdy>F|K6N($O7!Z579o|$+f!%7QH8V%NqbV3d*z&t6U%ALTku~t6;T~aF}^(m zzDbPqJj>w#fj9jdPbx!h-C~#ZU!~5FIedI$bf~C(P%?H*tNdIKC47jZqkd9H3wF7rxCIgB0t8G>wviEKo4 zWx6G1mKQ4;y3HiQ;BLL>O4{vhS=%^(!0SJoTQ0YC>aG>#G|&iIz@&lnNF%ZcPbJLR z!qkDi64;*sjL~$0Ym(!Vw;JN5mKQ$49(gN7Ggf2(h1N$i!lFUP>_fi2HGATYu+L?Y z+MdJQi$A868l0jcxt;vnX{CP28G9Dbp(Ly%=6uJ`BLM=uY8A#vd5pP8)1 zFL)mY=7l0w4uK9Cftv9crTwS@1fB0qNh9yuv)7RP@^=LKsv7OnFQeWV0&}%;6sC;| zI84o1b2uw0k6$-au}?CM3t29r+%hMr$cf>dJFPJEPDRcf+Hdr{(O`@%tou#LC(%Et z*T27l3R0wB8ynLcGN@!nKmqc0E6tfdc^pKO0RG-=L+ps&Cu{QF_Xl*XDyvh$jls^i zot|kb^T+aG_}^u8k*(>%>daxJ?H(?~KM0K zR22tmptKJ?0Cl``?j|J=xE4s~bfgYeKhdiE`2GA3O1d;h_~yW7yAh@W6&tbsAo-MU z>G~5FC9P?t-5&Cp%_Dc^5%%P2N>K&BS6*S`^(fak@I!LcFskRddz)tW#fb!sKhx#fWx)CK6p z`Y>x%Y8p@2PNUaDU5kagUN1(UN?LDKgZx85p!7;b4424a!To-P*s2v^@@{m$5i?cE z0GO`WRx0Ew&_V?~O2Z20p&s}7S&>hZ)fOYB$;Ootn~SVsS2%22U=i>5UGUcbRmi|KFM*}>|)XG2TD)Q|8;D}7x>_cX(VhJVE*5Av1=MYFF>4=!@z7N6@B+80T2 zuh;F>$EOlyZhPlLzdazh_lThV#G0=<;+}E zt|@vml=4>7v7He4l2zna^i{+@syk!3P2=H^q0@CjA>XsTY1a_`stSV+`{5D~%%D#9 zcR1iEx*hi@nptjjju#b|v}QcZiMLGrDWfQ-P7G2MlT0%dYlP#1S%m?E(~3+xJlj#X zj7Y{4>~~K^73y&JMmvNfjh{I*)~4aGCZ|Ri790AnH<;^Qc3q3;5r{DRw0&=`ZpFj^ zE4l0=90OM>>YR2KSdY%L@+P|&Y8t@bUkXNpa=6+5NFUk=xtuL^ z^L3g;0TA~OE4$xj+q|Mas#j(XIIKzZcVE;{=?au<769$RsC?TqI>Us$4D#}S%zsej zy&zQCh0NC*nl@CWslUxcC}krx-=73w0_Itbj+{(&o-BaE-;lA#K59 z75iNJNnYOV^V^(u(^%=-qtT?BkTq@eL=Gb4Rf56uclB@z37ZVijQB<*eE_FNf(De$ z?Pt%%t`i-;c^W#qs!S(%QI#G5GmsVYW7>^mM;*r;I+gS}?#ZE3B2l*X*BXosm-0U! z>;#{2XR{fv(U0G7W-lO`mJO!16l~6RJH3Yu z;5JT1ggcO%`5rsNPm_o$VZ1UfVg!eD)k-`SP^nb?jib@a`EeX7Gn6PM#mcYT6iUQJ;6?vOnv z@<}kG4CVf`E!T-wzsa`4H`;}4G73{4Uj$L~f(KW#FL0zom`uC`Nc8_{n-CO{ zVl^RNa?>5-yiQ9{*&KI{m^xp)sXO-j)Z-1bBkKKQ>rQCuqp6WfTw(24k(=7p2AKrY zy{WsJsL@+U%yh^^4#obO+%6c;26z7|JN#x!jC?p3Y~&STf4XBc6hMP9Xz_WYSM z?x6fE4)m_2E#9L*|CvhOo7${a{`{qU{9^RsPynVQ^KnOhH_%_XnV;Plo<$|rBC{aV2fD<2N(~S( z&jlYfso+C3oL2vfSpgEV$~;JWP7}iF<;!G;x8c$+a#aW9JltIJJZtq9&<FkqO{fDSYV#tVVj+Xz-S|zIS84_>e1yacTlq9#}&E3d*#q%hCdDOh_(~cQa zV``G~_ptpXAEL)3k2D~6f6cT;w4h_G^Qi%z25=7uAtse+9CMvY7-N?U%1QzrI735> zU&e-y&6yrARtn7_@<6G5;iWP`i-E6=xK z`n|z`_lO}X!pt;EqPp&8PRof11Kt*O>TJ#xCN8Z%xZy)e5AHosXZ$J_ZBF1P zQJ#gDlIzuKCwZ+*o+2YPY|x>Wi^pus)wH0NG`ID-UuRr55XMcaZ?jNutS<^2EL2zi zqP#!l0?)(rp47pI+5@;7S%?vpvo{;Tv7dsK8O;D}b3K(h`a}0`c7h6?U+d{}7vBR< z(QIeO5veUcYDa%KRk^Rrp4WDv6mlMemJAGD3%CXdJ>AYl!Z8sloPSV<hcAlHtArAX z(#(4*MWZfN!cbFt{?o(37iQ?6>|CB2Qtwyj)fGv+Bqz78)=*&kfyP?^{s$wuiPwfw zEyID;)74p#J|uD-NEZB<1_pBhpf_EOIx(VjJ=rgdo|eJe&T5@Qr>>sHwt6 zHAW`Z$et(0{wSagqWKCtS}w4v9_;oL8XumK&ig z1JDQ0pr0HGjuK)x9)jhiOqM2M+3Vn~hM;%`&pOvqD{s7XrGGzs7P$|Xa zQ>6BlV_XcF7br3P(u6FfM_rC%1RTFAbX&!0_n_v>Bzj17T>AU{tDUZVppVRr3p;WP*tI&-uvBgbj;8(Mj~p+77=L^v z6f;{Ept7BQ_Aa=^+wVFdquIysW%Zu)5ysI1{E@F(2bLsqnttJe=Tsy&aRIPr*Lhuz zFAFLW;!>!VtMm}!GFdyO2Td95WB_5yW?F%x#|ThfKzcQ23Aj>FB!^ipfkLs6=}Ls8 z&2zmwFM<}I4bnnlqTZ>f8EMBzd{`NNl1QLiOCUCow zACa9Jeox#yp$yw{r5m-L(eX0|hPD+Il|U+ve#F%F z_k5N(y$~2(^)ohiUx=|Z5o{%&JS3uXETUkF4hL*vsPL=hh`30gf@a>>i68b$%^q@r zUSP{ZRgn&${ex~fma&I9N3T09v6GJjYQxz3*}Qak=)XNaxp!1#T5%pz zT4d}m3RV4Zv88Z@a?{Lm@XbpM*i=@Xa}RQ&NsxD+)uE>4SbK+1SNmPh_q~INOMu+; zp~hOEiJhBn^TV%3zr}jdyKAbuGDb;jz-J3`{>@L!!1U)f0!t+m8BC7Q9eA8JD^%u1 zZQwpD;#OLq6JocY!gNmo?cl_B$ya+4e(_(G94V_w`E_Ls7W{P z)EUHGB9|(#(|jD48mRhebtPYyTW9D(+G5UCWShe)K~U|egj&e-1LAD)cO2*oEsZsj zh9UEiq?4kKI3&rM0t`{4kiEWq43Fp|q>g0GcX;?5FwG7X#@9%MFCoFOYck(^S{i zTo|#}36FKpSqn6Y^~fia6wM4+aDgLDEU0T1&4J}?)`q)Ljry&QCa!tBbvIV!?Ke!> z>V!XMUI~=0TGQ2dn|_n-<(Lo9RG4yDrB5! zV_OCfuljv~k5(4Vr_bb+D*wjbpl)Ww*&IFMSP}bMOz&+r&RYn5pCA3?^Ks4NgeI83 zPhf7TY&2lVo4%b7_I^HL9nK+=w)&>r6L!%fp@mn@Ttm-k`pXtUq*d}meOYoq7; z?eUAm_v#e@Qk03$b$QT*n1>_Zgq74oZESY`!ZwW?5R*p2U}rKJMP?=mnt+rPcC09U z;v@5_g22{^ZnpLNv~90PIg9VX%`TM$TP21Xdq{Hkq6N_#0>#L+H=p+>(nL}`9BQBK zf2>0{`>U9TQbNEhAUmM&trHJaIXiAnwqF=o@6c`GvK58-Y%WX^BvT?c>t5m-0-*f$ z4Jj)qoW1ehl?k1Z;-Ht)e&?3j{fbqIe=k<`r7O zS)UJrRfxQZlZk+jDlMVMv|g)~A17w>v>4=)gDVz&^flhW=i16lC+Ee>+WRiS3#b)R z8GhOsgI-kEZAj9ZvI<$JD8Fy_PETr^Ea3Upp_PDiwms`$@;RV9;%sO^>+@bRns$ef~h{$q-foy>H&%r zEAqIIB#$fMseYXXwBOxQ@?nAQ3>hd-92cH3&_pjB^JyC8r)w^(=lwI4vH-iO?>8q; zLucoBXEXdq7vnsktA$P7*2b;J{9BNqt9%}k29w9WE8Qc#z8tKvS4=@r_#?AtKBlu@ zmb+MJ(`0XIMaKmU9wFd5KZrg7l2WR0$dovLA^ezvXR&6cktE)99{oG%n1m2=sZZ`a zC@hftOf+w%?iE0kgvFRZr|faPIOm}siBEO=%}?K*(^jJi8Y_TblBHpFj?)c4l2vL@ z5Y6SvP#mr{m^vO*@!KQLe*XAnp2@4yv~Uar|NJf!%;11c(BFBIWOW>SjUhkcT{2t? zR%EdjJk7A&D(ZK$P|fT~=!}Xh9ZHYhS9%M-$7g^IIqvRDPx)6xoO>1s4 zX7KYO3+dM0Y4mQ^BR2Ij5)~I00O8fy6}HxF-@S>M>b z-M&wX{S>`D1FD+zO@8A}vLWiHdy?TUv?_a+lB0X2ca*P&*8$CC2kyp7FyMntC2&86 zLvp;6r4JUij}JQa_p3;u(otD>tjX`3k!8a|l@%FzRN>~FU`%q`@PjA*U8M5ET7{)g zHjGCWUxf_~#>PvZ7d56=i;^REYTr`ea`pYRs-Py%>B6UoFNCcfdMzRsT^13a=|s*X zN53$kTOsQ?eF_i`yONcB`Ew+hslJHLU?-D*kOnlJO#b1ZC@X>1+2a0Ude7c+@7}tA z%EE)UU*{fLH&3InH&(8S@c&E>S=A&%B`d#8^{^{)`7vR`5b($CF)hjm3`;U4^puuv zS!+#i*(_cS7=Z_BDh(ErtGoi}>VP|H^o??~ds^QWc-<0re4*k|Kqmvq_L_%$a0zMT zJw?Wd@$CtAzhE14Y()ULrxd@Vt=R`u|l|s$$g_JBWtMCZRx*Z<*s@|)O#t| z6xydyzpB2He?7JruTHQ3;#+zmtK)(rUF3DnMY4yl@o**<=d0e|UZJ``m;E1SbuDhi zb6rXTo&nmp$ydt_Fl;00Aph~vk47w8RfI+fqoIL7zj<)|1DIQ4t!=x*T>%ArPkbqg)Z(ZPq?G9>%o=>HoZkQIHIE0zK5Ex`$x>vo$q&za{@qJ5r zto^K`0wYfrRs!%vws zUhc%cEA&VcfQ_VUb&T2ar{kTI_YKkCt9pkQ2+Ar5sjbX}%^fcP=~z{0_Afn=38>Y0 z^XQhUJw~MIWn0|ivkRZsc`%F++yJ1c{U!J&RWMlQe|&#GMS1Yr{4S4+SfGj@ zU$Qn;ezI((r+8O3u3d0_9D$j+e_O7Ujn@pXcat*q&QfCc-9hV)j>}vtI=F(odIOIH z0~W=0Y@O%5XRj6{8Sux_fZk#Y-xc&IzF!1L7ZYm9kjnNSoh)BFe@2OI$8>Oe{HFwt z>8^{O8rT;+K08|C=acTG$y)gx<*ox?kWrJ98JQhMz$CC z(%U-oMvXijIHng8)hV;(;temFNOwTFi5?6MRp*1=eD2iqbB8CR(yN;qK0&cEY!?ln zhU6+FV%uLY3<=rq4l&&f@N@T`Jp8rtDd@%} z+H4IfBM-An?x0~@ob1xYhk2nkS*|st8NTsT%6~{-yXW$!uMf0Jx+cDy9`V9xl2c>} zWXC9_ePBbP$c{sJPRp7G(DA*x5uk^_8$45VnA(DFc(hKW*?ff;aM{QniQD`-^oq2V zjSZ)g8C~HZ5eg3{b#WOB3#1y&1e3M$$FMM0GZB@WmXXocUh#r9ku(>xBW&$ZCSl$5 znycyLK+*t-agNdZoB}~aRNkRaAL(n`2-&Obd4XADXQn z#&7;O-DV;II;8MdZiDhdkd_YNC&~dyL&y7-;y-;OSFA4Z-p@IFcWQ=qi$S3lc@%Tg zOX~Chi+jnzM)&P4L-vk@OA5gagIugp z8s3z5ni0-+z}fnx*LE7PW>Kw&d;;F$a`@UObv#=1XgmdXB$%o#vvFeRBVX!h!=-D| zWb$3osSmJb=pEVD<-3nWek~Z2zE$F}I(y=j_R`4&xyAL{A)|TWfDn!Y^z{@Yu{W zrlC3w!a=>8lG~@C%pxnvM^Z01F>+N^028;XdQv%PM!#-sS5W%ASL(ylMaWBBr3Q14 z+20Kb^#=Jmd{lGZz|2!K!nHu1?<&J8=6uIt1KuM?I}f+EI-;Y0q|BlQ5W|*3Cu`w* zWuBL)$(b%sq?rJetrj&vC9@x~YuXTP-~LDGSLFLmLBFpW3$nG(CXEAgMx-)nI7MC7 zerz0~x~;lSVj8T2j&83IB59VD-hSW~$6u$;M{oWVQVvv`cZr@%9`Es6iRf?i^DWV4 z7L3QMi0+zA>k$^K%_kFnk2d=tPJhh5KU@r(D{AoYRRZ~EiE{OjkLAF#o884pf?>QV z>HEwH9xASj4ce|1jWLc6TBm?F6$St{+={5{&gS?}&A|!d zz36sReddeB#&dt1^<<76E~Ef$F=CI|bpwMwU~@Ty4+ z#J8)eVSzI~96%cOr`iPSQqpEXmJdm*-YQa}|LEOfLcIa6nEQ4eqdh*pP4MbWwU@zV z?mUB0V7%gua;0dEQgP&$dV?R9MtIHL{7k$CBNW98BQx zA|&G_4GJEdw0QxWt=xx+wdhZpYDY|Jk4HiGmgJL(MQlo%)YL@b!blscbx31M)``;jRYD-ZmmInw0^Tk`MAs(wQQ@nv zG^)|-R}FHkEzU>V+1*7TaYwVX8QCyIb5|_I9l8Ib@#9R!oL86cu_MKZL@tEK#N1Yi zj!fK^Ag|I1;)H650grLYe7d)W=ss+kniMexc6x32X=Vi?c3}bGid~T(WBf)~YQ&>_ z__MS_7R5QGKA_6S+hRS+z3!-X3efb?o(~I^@gbQuod%1dEIru|M5E!!>vT1UxYVQ; zT-}y0@Kl!AZDa6UwBNlP@jC{~e6pqV8Y*DPpvr=OoYQ;|Q+|Pv+vlA_b=L%#-puRP zeyUn-<^5w|_}negyj6y}vsK3B{ZP|h6EO^RU`e~5w5ISgOL7p{%ocG_kfHiU{Rd}= zdPFMkuq`sAyS-cymUL68TWk9|+M^C90T0os?nZv}Q)lGS>BmXm-%hT{sP1>zzTEB1 zR0M66WXmEyT59flQ&rZ%^B7)$zk4U;viN=xdbkff-r#0X_u2&_=rZB0^Ke_R^*;z75Yd7r`1M5vbFN3)Vo5Y zfXeF&dUeL<8lr>9cdQR_j%jXFa1sp&8sEL$Ab?9dJh1FyXRVK~Aa8MRZ(|NS$ zYBpG-%rW^Zj57tyL-(o(x&r`g+ zBewwPUXN5ABKd4p?HyP(TMYvF`0ETWo7uea_UtUqANp7_8e38us=aoPu|G&^@W$ck zpzOu^>7pmFrnnQ;OfR#V?0CA?!K5?l&fq=hG}RbN=GTku&uVKb&tu8X1x;s&;n6zdzHRI1LHW?aC&p3k+(|O zKK^)>h(CfgyNGEEe(dsmA?IxlujVKcS@eD>kXa;Zmwwaw+mEgQOws0L&h-8^b*q+x z)F0cM*Ywu6#`a;{({6VAP1B+d)O|O5&XRYT6bDDBkC`YPK)@(Fic>k7_;X0wEkr<9 z=oEak>Uksp*%_C~Ole@jx+=X}*riK;vtdNMx~LVkj*6KR@bGy4<>s`=|O02Ib z2lgxmM0&EIOUC}ljhqxIxg}7ct-O~Gr!miSF!j#L>Q@nxDP?uqN4HrrC>p~n0lL=- zuANK(-y4*eU+r99E6+3nl+MqCu?iqwM{yuJk{DQu^qe@{XRj7B~Fkr8Rk#-Dxb<``DgkjbXVXj4B3$Z6vC_t82`~p@xUd3{bl3;uyBOxUo-hc8;zrk zbLRhQpEWrA>SO*t`fcc&RPCdTIN0d%zy8l!j^;!%L)iam4@@oqi2SU7n*4^1^QV6} z)^ElC|9s*nT!kZae_64izwUC1T)nvr|J96HqDX{YYOkC`*#6$fjsf6TC-{84i1G1X zy^##skuig}AN$Ev?kdutgSw8n6prE8A|m*YnKofK`1jqeZ+}OQEKJ_*QaO80?&=yJ zZ|iSgzR1*$5-VCeV6CCibhFXk!9OB*{*+1f8jVoP&TX&;akKxEIibJvv>d<(f}3h2 z0sd<8ke?i+f4wlVs2%+kuvo$X|F1*Uz#~IL64if<_D7lH>Uqrmz_I0&eKVE+MsJKO zca5vihm-8bbEdzK75d=r>yQ?LNnkL>E_`5D_}___kFb$UwpCmD6e&C_ZE~09Ap6_; z4%Rp_+`ke3+`WP=lIGWTiGJGhkLb>%Cu}rrcF`>-{kG6W%wfCaQ_}Tz&(Olt{r_^h z`!c9p@3_(>*Tlh11p0^@ZF9nCfOkw}f5tQqkEmd0Z&}ITsQ#O6EUb~k?oFn-;O7qN Q>i>b%m9>>>6yJpXA3sR6Jpcdz literal 0 HcmV?d00001 diff --git a/README.md b/README.md index ae87783a2..850876fbd 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,14 @@ A [Go](https://golang.org) project for handling [OpenAPI](https://www.openapis.o Licensed under the [MIT License](./LICENSE). -## Contributors and users -The project has received pull requests from many people. Thanks to everyone! +## Contributors, users and sponsors +The project has received pull requests [from many people](https://github.com/getkin/kin-openapi/graphs/contributors). Thanks to everyone! + +Be sure to [give back to this project](https://github.com/sponsors/fenollp) like our sponsors: + +

+ Speakeasy +

Here's some projects that depend on _kin-openapi_: * [github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" From 68016e06befbe1aa795cf3e310d932b9abb412ba Mon Sep 17 00:00:00 2001 From: danicc097 <71724149+danicc097@users.noreply.github.com> Date: Fri, 16 Sep 2022 11:29:30 +0200 Subject: [PATCH 176/260] Examples validation (#592) Co-authored-by: Pierre Fenoll --- openapi3/components.go | 2 +- openapi3/example.go | 10 +- openapi3/example_validation.go | 5 + openapi3/example_validation_test.go | 426 ++++++++++++++++++++++++++++ openapi3/media_type.go | 23 ++ openapi3/parameter.go | 32 ++- openapi3/schema.go | 6 + openapi3/validation_options.go | 8 + 8 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 openapi3/example_validation.go create mode 100644 openapi3/example_validation_test.go diff --git a/openapi3/components.go b/openapi3/components.go index 2f19943db..e9af26911 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -99,7 +99,7 @@ func (components *Components) Validate(ctx context.Context) (err error) { return } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("%s: %s", k, err) } } diff --git a/openapi3/example.go b/openapi3/example.go index ee40d9e37..e63c78fa6 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -2,6 +2,7 @@ package openapi3 import ( "context" + "errors" "fmt" "github.com/go-openapi/jsonpointer" @@ -55,5 +56,12 @@ func (example *Example) UnmarshalJSON(data []byte) error { // Validate returns an error if Example does not comply with the OpenAPI spec. func (example *Example) Validate(ctx context.Context) error { - return nil // TODO + if example.Value != nil && example.ExternalValue != "" { + return errors.New("value and externalValue are mutually exclusive") + } + if example.Value == nil && example.ExternalValue == "" { + return errors.New("example has no value or externalValue field") + } + + return nil } diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go new file mode 100644 index 000000000..4c75e360b --- /dev/null +++ b/openapi3/example_validation.go @@ -0,0 +1,5 @@ +package openapi3 + +func validateExampleValue(input interface{}, schema *Schema) error { + return schema.VisitJSON(input, MultiErrors()) +} diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go new file mode 100644 index 000000000..85e158e6b --- /dev/null +++ b/openapi3/example_validation_test.go @@ -0,0 +1,426 @@ +package openapi3 + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExamplesSchemaValidation(t *testing.T) { + type testCase struct { + name string + requestSchemaExample string + responseSchemaExample string + mediaTypeRequestExample string + parametersExample string + componentExamples string + errContains string + } + + testCases := []testCase{ + { + name: "invalid_parameter_examples", + parametersExample: ` + examples: + param1example: + value: abcd + `, + errContains: "invalid paths: param1example", + }, + { + name: "valid_parameter_examples", + parametersExample: ` + examples: + param1example: + value: 1 + `, + }, + { + name: "invalid_parameter_example", + parametersExample: ` + example: abcd + `, + errContains: "invalid paths", + }, + { + name: "valid_parameter_example", + parametersExample: ` + example: 1 + `, + }, + { + name: "invalid_component_examples", + mediaTypeRequestExample: ` + examples: + BadUser: + $ref: '#/components/examples/BadUser' + `, + componentExamples: ` + examples: + BadUser: + value: + username: "]bad[" + email: bad + password: short + `, + errContains: "invalid paths: BadUser", + }, + { + name: "valid_component_examples", + mediaTypeRequestExample: ` + examples: + BadUser: + $ref: '#/components/examples/BadUser' + `, + componentExamples: ` + examples: + BadUser: + value: + username: good + email: good@mail.com + password: password + `, + }, + { + name: "invalid_mediatype_examples", + mediaTypeRequestExample: ` + example: + username: "]bad[" + email: bad + password: short + `, + errContains: "invalid paths", + }, + { + name: "valid_mediatype_examples", + mediaTypeRequestExample: ` + example: + username: good + email: good@mail.com + password: password + `, + }, + { + name: "invalid_schema_request_example", + requestSchemaExample: ` + example: + username: good + email: good@email.com + # missing password + `, + errContains: "invalid schema example", + }, + { + name: "valid_schema_request_example", + requestSchemaExample: ` + example: + username: good + email: good@email.com + password: password + `, + }, + { + name: "invalid_schema_response_example", + responseSchemaExample: ` + example: + user_id: 1 + # missing access_token + `, + errContains: "invalid schema example", + }, + { + name: "valid_schema_response_example", + responseSchemaExample: ` + example: + user_id: 1 + access_token: "abcd" + `, + }, + } + + testOptions := []struct { + name string + disableExamplesValidation bool + }{ + { + name: "examples_validation_disabled", + disableExamplesValidation: true, + }, + { + name: "examples_validation_enabled", + disableExamplesValidation: false, + }, + } + + t.Parallel() + + for _, testOption := range testOptions { + testOption := testOption + t.Run(testOption.name, func(t *testing.T) { + t.Parallel() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + loader := NewLoader() + + spec := bytes.Buffer{} + spec.WriteString(` +openapi: 3.0.3 +info: + title: An API + version: 1.2.3.4 +paths: + /user: + post: + description: User creation. + operationId: createUser + parameters: + - name: param1 + in: 'query' + schema: + format: int64 + type: integer`) + spec.WriteString(tc.parametersExample) + spec.WriteString(` + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUserRequest" +`) + spec.WriteString(tc.mediaTypeRequestExample) + spec.WriteString(` + description: Created user object + required: true + responses: + '204': + description: "success" + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUserResponse" +components: + schemas: + CreateUserRequest:`) + spec.WriteString(tc.requestSchemaExample) + spec.WriteString(` + required: + - username + - email + - password + properties: + username: + type: string + pattern: "^[ a-zA-Z0-9_-]+$" + minLength: 3 + email: + type: string + pattern: "^[A-Za-z0-9+_.-]+@(.+)$" + password: + type: string + minLength: 7 + type: object + CreateUserResponse:`) + spec.WriteString(tc.responseSchemaExample) + spec.WriteString(` + description: represents the response to a User creation + required: + - access_token + - user_id + properties: + access_token: + type: string + user_id: + format: int64 + type: integer + type: object +`) + spec.WriteString(tc.componentExamples) + + doc, err := loader.LoadFromData(spec.Bytes()) + require.NoError(t, err) + + if testOption.disableExamplesValidation { + err = doc.Validate(loader.Context, DisableExamplesValidation()) + } else { + err = doc.Validate(loader.Context) + } + + if tc.errContains != "" && !testOption.disableExamplesValidation { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errContains) + } else { + require.NoError(t, err) + } + }) + } + }) + } +} + +func TestExampleObjectValidation(t *testing.T) { + type testCase struct { + name string + mediaTypeRequestExample string + componentExamples string + errContains string + } + + testCases := []testCase{ + { + name: "example_examples_mutually_exclusive", + mediaTypeRequestExample: ` + examples: + BadUser: + $ref: '#/components/examples/BadUser' + example: + username: good + email: real@email.com + password: validpassword +`, + errContains: "example and examples are mutually exclusive", + componentExamples: ` + examples: + BadUser: + value: + username: "]bad[" + email: bad + password: short +`, + }, + { + name: "example_without_value", + componentExamples: ` + examples: + BadUser: + description: empty user example +`, + errContains: "example has no value or externalValue field", + }, + { + name: "value_externalValue_mutual_exclusion", + componentExamples: ` + examples: + BadUser: + value: + username: good + email: real@email.com + password: validpassword + externalValue: 'http://example.com/examples/example' +`, + errContains: "value and externalValue are mutually exclusive", + }, + } + + testOptions := []struct { + name string + disableExamplesValidation bool + }{ + { + name: "examples_validation_disabled", + disableExamplesValidation: true, + }, + { + name: "examples_validation_enabled", + disableExamplesValidation: false, + }, + } + + t.Parallel() + + for _, testOption := range testOptions { + testOption := testOption + t.Run(testOption.name, func(t *testing.T) { + t.Parallel() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + loader := NewLoader() + + spec := bytes.Buffer{} + spec.WriteString(` +openapi: 3.0.3 +info: + title: An API + version: 1.2.3.4 +paths: + /user: + post: + description: User creation. + operationId: createUser + parameters: + - name: param1 + in: 'query' + schema: + format: int64 + type: integer + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUserRequest" +`) + spec.WriteString(tc.mediaTypeRequestExample) + spec.WriteString(` + description: Created user object + required: true + responses: + '204': + description: "success" + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUserResponse" +components: + schemas: + CreateUserRequest: + required: + - username + - email + - password + properties: + username: + type: string + pattern: "^[ a-zA-Z0-9_-]+$" + minLength: 3 + email: + type: string + pattern: "^[A-Za-z0-9+_.-]+@(.+)$" + password: + type: string + minLength: 7 + type: object + CreateUserResponse: + description: represents the response to a User creation + required: + - access_token + - user_id + properties: + access_token: + type: string + user_id: + format: int64 + type: integer + type: object +`) + spec.WriteString(tc.componentExamples) + + doc, err := loader.LoadFromData(spec.Bytes()) + require.NoError(t, err) + + if testOption.disableExamplesValidation { + err = doc.Validate(loader.Context, DisableExamplesValidation()) + } else { + err = doc.Validate(loader.Context) + } + + if tc.errContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errContains) + } else { + require.NoError(t, err) + } + }) + } + }) + } +} diff --git a/openapi3/media_type.go b/openapi3/media_type.go index fc95244c6..01df12ad0 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -2,6 +2,8 @@ package openapi3 import ( "context" + "errors" + "fmt" "github.com/go-openapi/jsonpointer" @@ -80,7 +82,28 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { if err := schema.Validate(ctx); err != nil { return err } + if mediaType.Example != nil && mediaType.Examples != nil { + return errors.New("example and examples are mutually exclusive") + } + if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled { + return nil + } + if example := mediaType.Example; example != nil { + if err := validateExampleValue(example, schema.Value); err != nil { + return err + } + } else if examples := mediaType.Examples; examples != nil { + for k, v := range examples { + if err := v.Validate(ctx); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + } + } } + return nil } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 77834847d..f5d7d1f2f 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -303,16 +303,38 @@ func (parameter *Parameter) Validate(ctx context.Context) error { e := errors.New("parameter must contain exactly one of content and schema") return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) } - if schema := parameter.Schema; schema != nil { - if err := schema.Validate(ctx); err != nil { - return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err) - } - } if content := parameter.Content; content != nil { if err := content.Validate(ctx); err != nil { return fmt.Errorf("parameter %q content is invalid: %v", parameter.Name, err) } } + + if schema := parameter.Schema; schema != nil { + if err := schema.Validate(ctx); err != nil { + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err) + } + if parameter.Example != nil && parameter.Examples != nil { + return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name) + } + if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled { + return nil + } + if example := parameter.Example; example != nil { + if err := validateExampleValue(example, schema.Value); err != nil { + return err + } + } else if examples := parameter.Examples; examples != nil { + for k, v := range examples { + if err := v.Validate(ctx); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + } + } + } + return nil } diff --git a/openapi3/schema.go b/openapi3/schema.go index 57f63fbd8..c59070ee1 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -753,6 +753,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } + if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled { + if err := validateExampleValue(x, schema); err != nil { + return fmt.Errorf("invalid schema example: %s", err) + } + } + return } diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index f6038ed10..5c0d01d2f 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -9,6 +9,7 @@ type ValidationOption func(options *ValidationOptions) type ValidationOptions struct { SchemaFormatValidationEnabled bool SchemaPatternValidationDisabled bool + ExamplesValidationDisabled bool } type validationOptionsKey struct{} @@ -27,6 +28,13 @@ func DisableSchemaPatternValidation() ValidationOption { } } +// DisableExamplesValidation disables all example schema validation. +func DisableExamplesValidation() ValidationOption { + return func(options *ValidationOptions) { + options.ExamplesValidationDisabled = true + } +} + // WithValidationOptions allows adding validation options to a context object that can be used when validationg any OpenAPI type. func WithValidationOptions(ctx context.Context, options *ValidationOptions) context.Context { return context.WithValue(ctx, validationOptionsKey{}, options) From d12860caf67e8be20bec1d0b57b82ffb83f4e140 Mon Sep 17 00:00:00 2001 From: Sergey Vilgelm Date: Mon, 19 Sep 2022 00:55:30 -0700 Subject: [PATCH 177/260] use %w to wrap the errors (#596) --- jsoninfo/unmarshal.go | 6 +++--- openapi3/components.go | 2 +- openapi3/header.go | 8 ++++---- openapi3/loader.go | 8 ++++---- openapi3/loader_test.go | 27 +++++++++++++++------------ openapi3/media_type.go | 4 ++-- openapi3/openapi3.go | 10 +++++----- openapi3/parameter.go | 12 ++++++------ openapi3/schema.go | 2 +- openapi3/schema_formats.go | 2 +- openapi3/security_scheme.go | 2 +- openapi3filter/req_resp_decoder.go | 12 ++++++------ routers/legacy/router.go | 2 +- 13 files changed, 50 insertions(+), 47 deletions(-) diff --git a/jsoninfo/unmarshal.go b/jsoninfo/unmarshal.go index eb6e758ac..16886ad83 100644 --- a/jsoninfo/unmarshal.go +++ b/jsoninfo/unmarshal.go @@ -25,7 +25,7 @@ type ObjectDecoder struct { func NewObjectDecoder(data []byte) (*ObjectDecoder, error) { var remainingFields map[string]json.RawMessage if err := json.Unmarshal(data, &remainingFields); err != nil { - return nil, fmt.Errorf("failed to unmarshal extension properties: %v (%s)", err, data) + return nil, fmt.Errorf("failed to unmarshal extension properties: %w (%s)", err, data) } return &ObjectDecoder{ Data: data, @@ -87,7 +87,7 @@ func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) continue } } - return fmt.Errorf("failed to unmarshal property %q (%s): %v", + return fmt.Errorf("failed to unmarshal property %q (%s): %w", field.JSONName, fieldValue.Type().String(), err) } if !isPtr { @@ -109,7 +109,7 @@ func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) continue } } - return fmt.Errorf("failed to unmarshal property %q (%s): %v", + return fmt.Errorf("failed to unmarshal property %q (%s): %w", field.JSONName, fieldPtr.Type().String(), err) } diff --git a/openapi3/components.go b/openapi3/components.go index e9af26911..ce7a86990 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -99,7 +99,7 @@ func (components *Components) Validate(ctx context.Context) (err error) { return } if err = v.Validate(ctx); err != nil { - return fmt.Errorf("%s: %s", k, err) + return fmt.Errorf("%s: %w", k, err) } } diff --git a/openapi3/header.go b/openapi3/header.go index 75e5dd1e2..c71d3f2a8 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -71,22 +71,22 @@ func (header *Header) Validate(ctx context.Context) error { sm.Style == SerializationSimple && !sm.Explode || sm.Style == SerializationSimple && sm.Explode; !smSupported { e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a header parameter", sm.Style, sm.Explode) - return fmt.Errorf("header schema is invalid: %v", e) + return fmt.Errorf("header schema is invalid: %w", e) } if (header.Schema == nil) == (header.Content == nil) { e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", header) - return fmt.Errorf("header schema is invalid: %v", e) + return fmt.Errorf("header schema is invalid: %w", e) } if schema := header.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { - return fmt.Errorf("header schema is invalid: %v", err) + return fmt.Errorf("header schema is invalid: %w", err) } } if content := header.Content; content != nil { if err := content.Validate(ctx); err != nil { - return fmt.Errorf("header content is invalid: %v", err) + return fmt.Errorf("header content is invalid: %w", err) } } return nil diff --git a/openapi3/loader.go b/openapi3/loader.go index 87c9f8684..e9366dcb5 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -101,7 +101,7 @@ func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, el resolvedPath, err := resolvePath(rootPath, parsedURL) if err != nil { - return nil, fmt.Errorf("could not resolve path: %v", err) + return nil, fmt.Errorf("could not resolve path: %w", err) } data, err := loader.readURL(resolvedPath) @@ -285,7 +285,7 @@ func (loader *Loader) resolveComponent( if cursor, err = drillIntoField(cursor, pathPart); err != nil { e := failedToResolveRefFragmentPart(ref, pathPart) - return nil, fmt.Errorf("%s: %s", e.Error(), err.Error()) + return nil, fmt.Errorf("%s: %w", e, err) } if cursor == nil { return nil, failedToResolveRefFragmentPart(ref, pathPart) @@ -430,11 +430,11 @@ func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, var resolvedPath *url.URL if resolvedPath, err = resolvePath(path, parsedURL); err != nil { - return nil, "", nil, fmt.Errorf("error resolving path: %v", err) + return nil, "", nil, fmt.Errorf("error resolving path: %w", err) } if doc, err = loader.loadFromURIInternal(resolvedPath); err != nil { - return nil, "", nil, fmt.Errorf("error resolving reference %q: %v", ref, err) + return nil, "", nil, fmt.Errorf("error resolving reference %q: %w", ref, err) } return doc, "#" + fragment, resolvedPath, nil diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index e33b75d72..64a923c39 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -1,7 +1,6 @@ package openapi3 import ( - "errors" "fmt" "net" "net/http" @@ -524,23 +523,27 @@ paths: {} servers: - @@@ ` - for value, expected := range map[string]error{ - `{url: /}`: nil, - `{url: "http://{x}.{y}.example.com"}`: errors.New("invalid servers: server has undeclared variables"), - `{url: "http://{x}.y}.example.com"}`: errors.New("invalid servers: server URL has mismatched { and }"), - `{url: "http://{x.example.com"}`: errors.New("invalid servers: server URL has mismatched { and }"), - `{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`: nil, - `{url: "http://{x}.example.com", variables: {x: {default: "www", enum: ["www"]}}}`: nil, - `{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New(`invalid servers: field default is required in {"enum":["www"]}`), - `{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), - `{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), + for value, expected := range map[string]string{ + `{url: /}`: "", + `{url: "http://{x}.{y}.example.com"}`: "invalid servers: server has undeclared variables", + `{url: "http://{x}.y}.example.com"}`: "invalid servers: server URL has mismatched { and }", + `{url: "http://{x.example.com"}`: "invalid servers: server URL has mismatched { and }", + `{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`: "", + `{url: "http://{x}.example.com", variables: {x: {default: "www", enum: ["www"]}}}`: "", + `{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`: `invalid servers: field default is required in {"enum":["www"]}`, + `{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`: "invalid servers: server has undeclared variables", + `{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`: "invalid servers: server has undeclared variables", } { t.Run(value, func(t *testing.T) { loader := NewLoader() doc, err := loader.LoadFromData([]byte(strings.Replace(spec, "@@@", value, 1))) require.NoError(t, err) err = doc.Validate(loader.Context) - require.Equal(t, expected, err) + if expected == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, expected) + } }) } } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 01df12ad0..b1a3417eb 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -95,10 +95,10 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { } else if examples := mediaType.Examples; examples != nil { for k, v := range examples { if err := v.Validate(ctx); err != nil { - return fmt.Errorf("%s: %s", k, err) + return fmt.Errorf("%s: %w", k, err) } if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { - return fmt.Errorf("%s: %s", k, err) + return fmt.Errorf("%s: %w", k, err) } } } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 20549c2b7..09b6c2c64 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -69,14 +69,14 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { // NOTE: only mention info/components/paths/... key in this func's errors. { - wrap := func(e error) error { return fmt.Errorf("invalid components: %v", e) } + wrap := func(e error) error { return fmt.Errorf("invalid components: %w", e) } if err := doc.Components.Validate(ctx); err != nil { return wrap(err) } } { - wrap := func(e error) error { return fmt.Errorf("invalid info: %v", e) } + wrap := func(e error) error { return fmt.Errorf("invalid info: %w", e) } if v := doc.Info; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) @@ -87,7 +87,7 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { } { - wrap := func(e error) error { return fmt.Errorf("invalid paths: %v", e) } + wrap := func(e error) error { return fmt.Errorf("invalid paths: %w", e) } if v := doc.Paths; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) @@ -98,7 +98,7 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { } { - wrap := func(e error) error { return fmt.Errorf("invalid security: %v", e) } + wrap := func(e error) error { return fmt.Errorf("invalid security: %w", e) } if v := doc.Security; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) @@ -107,7 +107,7 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { } { - wrap := func(e error) error { return fmt.Errorf("invalid servers: %v", e) } + wrap := func(e error) error { return fmt.Errorf("invalid servers: %w", e) } if v := doc.Servers; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) diff --git a/openapi3/parameter.go b/openapi3/parameter.go index f5d7d1f2f..64092538f 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -296,23 +296,23 @@ func (parameter *Parameter) Validate(ctx context.Context) error { } if !smSupported { e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in) - return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) + return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, e) } if (parameter.Schema == nil) == (parameter.Content == nil) { e := errors.New("parameter must contain exactly one of content and schema") - return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) + return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, e) } if content := parameter.Content; content != nil { if err := content.Validate(ctx); err != nil { - return fmt.Errorf("parameter %q content is invalid: %v", parameter.Name, err) + return fmt.Errorf("parameter %q content is invalid: %w", parameter.Name, err) } } if schema := parameter.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { - return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err) + return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, err) } if parameter.Example != nil && parameter.Examples != nil { return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name) @@ -327,10 +327,10 @@ func (parameter *Parameter) Validate(ctx context.Context) error { } else if examples := parameter.Examples; examples != nil { for k, v := range examples { if err := v.Validate(ctx); err != nil { - return fmt.Errorf("%s: %s", k, err) + return fmt.Errorf("%s: %w", k, err) } if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { - return fmt.Errorf("%s: %s", k, err) + return fmt.Errorf("%s: %w", k, err) } } } diff --git a/openapi3/schema.go b/openapi3/schema.go index c59070ee1..ded97f02a 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -755,7 +755,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled { if err := validateExampleValue(x, schema); err != nil { - return fmt.Errorf("invalid schema example: %s", err) + return fmt.Errorf("invalid schema example: %w", err) } } diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 17b7564cf..51e245411 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -28,7 +28,7 @@ var SchemaStringFormats = make(map[string]Format, 4) func DefineStringFormat(name string, pattern string) { re, err := regexp.Compile(pattern) if err != nil { - err := fmt.Errorf("format %q has invalid pattern %q: %v", name, pattern, err) + err := fmt.Errorf("format %q has invalid pattern %q: %w", name, pattern, err) panic(err) } SchemaStringFormats[name] = Format{regexp: re} diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 52c3ef218..790b21a73 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -165,7 +165,7 @@ func (ss *SecurityScheme) Validate(ctx context.Context) error { return fmt.Errorf("security scheme of type %q should have 'flows'", ss.Type) } if err := flow.Validate(ctx); err != nil { - return fmt.Errorf("security scheme 'flow' is invalid: %v", err) + return fmt.Errorf("security scheme 'flow' is invalid: %w", err) } } else if ss.Flows != nil { return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index a54b3bb47..73eb73e2b 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -654,7 +654,7 @@ func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializ return nil, found, nil } if err != nil { - return nil, found, fmt.Errorf("decoding param %q: %s", param, err) + return nil, found, fmt.Errorf("decoding param %q: %w", param, err) } val, err := parsePrimitive(cookie.Value, schema) @@ -673,7 +673,7 @@ func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.Serializatio return nil, found, nil } if err != nil { - return nil, found, fmt.Errorf("decoding param %q: %s", param, err) + return nil, found, fmt.Errorf("decoding param %q: %w", param, err) } val, err := parseArray(strings.Split(cookie.Value, ","), schema) return val, found, err @@ -691,7 +691,7 @@ func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.Serializati return nil, found, nil } if err != nil { - return nil, found, fmt.Errorf("decoding param %q: %s", param, err) + return nil, found, fmt.Errorf("decoding param %q: %w", param, err) } props, err := propsFromString(cookie.Value, ",", ",") if err != nil { @@ -753,7 +753,7 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string if v, ok := err.(*ParseError); ok { return nil, &ParseError{path: []interface{}{propName}, Cause: v} } - return nil, fmt.Errorf("property %q: %s", propName, err) + return nil, fmt.Errorf("property %q: %w", propName, err) } obj[propName] = value } @@ -771,7 +771,7 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err if v, ok := err.(*ParseError); ok { return nil, &ParseError{path: []interface{}{i}, Cause: v} } - return nil, fmt.Errorf("item %d: %s", i, err) + return nil, fmt.Errorf("item %d: %w", i, err) } // If the items are nil, then the array is nil. There shouldn't be case where some values are actual primitive @@ -1044,7 +1044,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S if v, ok := err.(*ParseError); ok { return nil, &ParseError{path: []interface{}{name}, Cause: v} } - return nil, fmt.Errorf("part %s: %s", name, err) + return nil, fmt.Errorf("part %s: %w", name, err) } values[name] = append(values[name], value) } diff --git a/routers/legacy/router.go b/routers/legacy/router.go index f1f47d9ed..fb8d4621e 100644 --- a/routers/legacy/router.go +++ b/routers/legacy/router.go @@ -60,7 +60,7 @@ type Router struct { // All operations of the document will be added to the router. func NewRouter(doc *openapi3.T) (routers.Router, error) { if err := doc.Validate(context.Background()); err != nil { - return nil, fmt.Errorf("validating OpenAPI failed: %v", err) + return nil, fmt.Errorf("validating OpenAPI failed: %w", err) } router := &Router{doc: doc} root := router.node() From 6610338051cc51016df6dbc462ef332fc0550560 Mon Sep 17 00:00:00 2001 From: sorintm <112782063+sorintm@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:08:00 +0100 Subject: [PATCH 178/260] Expose request/response validation options in the middleware Validator (#608) --- openapi3filter/middleware.go | 10 ++++++++++ openapi3filter/middleware_test.go | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/openapi3filter/middleware.go b/openapi3filter/middleware.go index 3709faf9b..3bcb9db43 100644 --- a/openapi3filter/middleware.go +++ b/openapi3filter/middleware.go @@ -16,6 +16,7 @@ type Validator struct { errFunc ErrFunc logFunc LogFunc strict bool + options Options } // ErrFunc handles errors that may occur during validation. @@ -106,6 +107,13 @@ func Strict(strict bool) ValidatorOption { } } +// ValidationOptions sets request/response validation options on the validator. +func ValidationOptions(options Options) ValidatorOption { + return func(v *Validator) { + v.options = options + } +} + // Middleware returns an http.Handler which wraps the given handler with // request and response validation. func (v *Validator) Middleware(h http.Handler) http.Handler { @@ -120,6 +128,7 @@ func (v *Validator) Middleware(h http.Handler) http.Handler { Request: r, PathParams: pathParams, Route: route, + Options: &v.options, } if err = ValidateRequest(r.Context(), requestValidationInput); err != nil { v.logFunc("invalid request", err) @@ -141,6 +150,7 @@ func (v *Validator) Middleware(h http.Handler) http.Handler { Status: wr.statusCode(), Header: wr.Header(), Body: ioutil.NopCloser(bytes.NewBuffer(wr.bodyContents())), + Options: &v.options, }); err != nil { v.logFunc("invalid response", err) if v.strict { diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go index c6a5e9bc2..ff6059c9d 100644 --- a/openapi3filter/middleware_test.go +++ b/openapi3filter/middleware_test.go @@ -328,6 +328,27 @@ func TestValidator(t *testing.T) { body: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, }, strict: false, + }, { + name: "POST response status code not in spec (return 200, spec only has 201)", + handler: validatorTestHandler{ + postBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, + errStatusCode: 200, + errBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, + }.withDefaults(), + options: []openapi3filter.ValidatorOption{openapi3filter.ValidationOptions(openapi3filter.Options{ + IncludeResponseStatus: true, + })}, + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10}`, + contentType: "application/json", + }, + response: testResponse{ + statusCode: 200, + body: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, + }, + strict: false, }} for i, test := range tests { t.Logf("test#%d: %s", i, test.name) From a8f69f469eb9c4c423fefa06433c90edc0bfaa45 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge <108070248+TristanSpeakEasy@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:10:18 +0100 Subject: [PATCH 179/260] fix: detects circular references that can't be handled at the moment to avoid infinite loops loading documents (#607) --- openapi3/issue542_test.go | 15 ++++ openapi3/issue570_test.go | 15 ++++ openapi3/loader.go | 60 +++++++++---- openapi3/testdata/issue542.yml | 43 +++++++++ openapi3/testdata/issue570.json | 155 ++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 19 deletions(-) create mode 100644 openapi3/issue542_test.go create mode 100644 openapi3/issue570_test.go create mode 100644 openapi3/testdata/issue542.yml create mode 100644 openapi3/testdata/issue570.json diff --git a/openapi3/issue542_test.go b/openapi3/issue542_test.go new file mode 100644 index 000000000..7e0cb88c9 --- /dev/null +++ b/openapi3/issue542_test.go @@ -0,0 +1,15 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue542(t *testing.T) { + sl := NewLoader() + + _, err := sl.LoadFromFile("testdata/issue542.yml") + require.Error(t, err) + require.Contains(t, err.Error(), CircularReferenceError) +} diff --git a/openapi3/issue570_test.go b/openapi3/issue570_test.go new file mode 100644 index 000000000..f3c527e3b --- /dev/null +++ b/openapi3/issue570_test.go @@ -0,0 +1,15 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIssue570(t *testing.T) { + loader := NewLoader() + _, err := loader.LoadFromFile("testdata/issue570.json") + require.Error(t, err) + assert.Contains(t, err.Error(), CircularReferenceError) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index e9366dcb5..eb1ebbd6c 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -15,6 +15,8 @@ import ( "github.com/invopop/yaml" ) +var CircularReferenceError = "kin-openapi bug found: circular schema reference not handled" + func foundUnresolvedRef(ref string) error { return fmt.Errorf("found unresolved ref: %q", ref) } @@ -197,7 +199,7 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { } } for _, component := range components.Schemas { - if err = loader.resolveSchemaRef(doc, component, location); err != nil { + if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil { return } } @@ -480,7 +482,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat } if schema := value.Schema; schema != nil { - if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } } @@ -532,13 +534,13 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum } for _, contentType := range value.Content { if schema := contentType.Schema; schema != nil { - if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } } } if schema := value.Schema; schema != nil { - if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } } @@ -592,7 +594,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { - if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } } @@ -656,7 +658,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { - if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } contentType.Schema = schema @@ -670,8 +672,12 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen return nil } -func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPath *url.URL) (err error) { - if component != nil && component.Value != nil { +func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPath *url.URL, visited []string) (err error) { + if component == nil { + return errors.New("invalid schema: value MUST be an object") + } + + if component.Value != nil { if loader.visitedSchema == nil { loader.visitedSchema = make(map[*Schema]struct{}) } @@ -681,9 +687,6 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat loader.visitedSchema[component.Value] = struct{}{} } - if component == nil { - return errors.New("invalid schema: value MUST be an object") - } ref := component.Ref if ref != "" { if isSingleRefElement(ref) { @@ -693,12 +696,18 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat } component.Value = &schema } else { + if visitedLimit(visited, ref, 3) { + visited = append(visited, ref) + return fmt.Errorf("%s - %s", CircularReferenceError, strings.Join(visited, " -> ")) + } + visited = append(visited, ref) + var resolved SchemaRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } - if err := loader.resolveSchemaRef(doc, &resolved, componentPath); err != nil { + if err := loader.resolveSchemaRef(doc, &resolved, componentPath, visited); err != nil { return err } component.Value = resolved.Value @@ -713,37 +722,37 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat // ResolveRefs referred schemas if v := value.Items; v != nil { - if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, v := range value.Properties { - if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.AdditionalProperties; v != nil { - if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.Not; v != nil { - if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, v := range value.AllOf { - if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, v := range value.AnyOf { - if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, v := range value.OneOf { - if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { + if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } @@ -1046,3 +1055,16 @@ func (loader *Loader) resolvePathItemRefContinued(doc *T, pathItem *PathItem, do func unescapeRefString(ref string) string { return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1) } + +func visitedLimit(visited []string, ref string, limit int) bool { + visitedCount := 0 + for _, v := range visited { + if v == ref { + visitedCount++ + if visitedCount >= limit { + return true + } + } + } + return false +} diff --git a/openapi3/testdata/issue542.yml b/openapi3/testdata/issue542.yml new file mode 100644 index 000000000..887702557 --- /dev/null +++ b/openapi3/testdata/issue542.yml @@ -0,0 +1,43 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: {} +#paths: +# /pets: +# patch: +# requestBody: +# content: +# application/json: +# schema: +# oneOf: +# - $ref: '#/components/schemas/Cat' +# - $ref: '#/components/schemas/Kitten' +# discriminator: +# propertyName: pet_type +# responses: +# '200': +# description: Updated +components: + schemas: + Cat: + anyOf: + - $ref: "#/components/schemas/Kitten" + - type: object + # properties: + # hunts: + # type: boolean + # age: + # type: integer + # offspring: + Kitten: + $ref: "#/components/schemas/Cat" #ko + +# type: string #ok + +# allOf: #ko +# - $ref: '#/components/schemas/Cat' diff --git a/openapi3/testdata/issue570.json b/openapi3/testdata/issue570.json new file mode 100644 index 000000000..ed3a7509b --- /dev/null +++ b/openapi3/testdata/issue570.json @@ -0,0 +1,155 @@ +{ + "swagger": "2.0", + "info": { + "version": "internal", + "title": "Rubrik INTERNAL REST API", + "description": "Copyright © 2017-2021 Rubrik Inc.\n\n# Introduction\n\nThis is the INTERNAL REST API for Rubrik. We don't guarantee support or backward compatibility. Use at your own risk.\n\n# Changelog\n\n Revisions are listed with the most recent revision first.\n ### Changes to Internal API in Rubrik version 6.0\n ## Breaking changes:\n * Renamed field `node` to `nodeId` for object `NetworkInterface` used by\n `GET /cluster/{id}/network_interface`.\n * Removed `compliance24HourStatus` in `DataSourceTableRequest` for\n `POST /report/data_source/table`.\n Use `complianceStatus`, `awaitingFirstFull`, and `snapshotRange`\n as replacements.\n * Changed the sort_by attribute of `GET /vcd/vapp` to use\n `VcdVappObjectSortAttribute`.\n This attribute no longer uses the `VappCount` or `ConnectionStatus`\n parameters from the previously used `VcdHierarchyObjectSortAttribute`.\n\n ## Feature additions/improvements:\n * Added the `GET /sla_domain/{id}/protected_objects` endpoint to return\n objects explicitly protected by the SLA Domain with direct assignments.\n * Added new field `nodeName` for object `NetworkInterface` used by\n `GET /cluster/{id}/network_interface`.\n * Added the `POST /cluster/{id}/remove_nodes` endpoint to trigger a bulk\n node removal job.\n * Added new optional field `numChannels` to `ExportOracleDbConfig` object\n specifying the number of channels used during Oracle clone or same-host\n recovery.\n * Added new optional fields `forceFull` to the object\n `HypervVirtualMachineSummary` used by `GET /hyperv/vm`. This field is also\n used in `HypervVirtualMachineDetail` used by `GET /hyperv/vm/{id}` and\n `PATCH /hyperv/vm/{id}`.\n * Added the `GET /cluster/{id}/bootstrap_config` endpoint to enable Rubrik CDM\n to retrieve Rubrik cluster configuration information for the cluster nodes.\n * Added new optional field clusterUuid to the ClusterConfig object used\n by `POST /cluster/{id}/bootstrap` and `POST /cluster/{id}/setupnetwork`.\n * Added new optional fields `dataGuardGroupId` and `dataGuardGroupName` to\n the object `OracleHierarchyObjectSummary` used by\n `GET /oracle/hierarchy/{id}`, `GET /oracle/hierarchy/{id}/children`, and\n `GET /oracle/hierarchy/{id}/descendants`.\n * Added new optional fields `dataGuardGroupId` and `dataGuardGroupName` to\n the object `OracleDbSummary` used by `GET /oracle/db`.\n * Added new optional fields `dataGuardGroupId` and `dataGuardGroupName` to\n the object `OracleDbDetail` used by `GET /oracle/db/{id}` and\n `PATCH /oracle/db/{id}`.\n * Added a new optional field `immutabilityLockSummary` to the object\n `ArchivalLocationSummary` returned by GET `/archive/location` and\n GET `/organization/{id}/archive/location`\n * Added new optional fields `dbUniqueName` and `databaseRole` to the object\n `OracleHierarchyObjectSummary` used by `GET /oracle/hierarchy/{id}`,\n `GET /oracle/hierarchy/{id}/children`, and\n `GET /oracle/hierarchy/{id}/descendants`.\n * Added new required fields `dbUniqueName` and `databaseRole` to the object\n `OracleDbSummary` used by `GET /oracle/db`.\n * Added a new required field `databaseRole` to the object `OracleDbDetail`\n used by `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}`.\n * Added a new optional field `subnet` to `ManagedVolumeUpdate`, used by \n `PATCH /managed_volume/{id}` for updating the subnet to which the node IPs\n will belong during an SLA MV backup.\n * Added new optional field `numChannels` to `RecoverOracleDbConfig`\n and `MountOracleDbConfig` objects specifying the number of channels used\n during Oracle recovery.\n * Added a new optional field `immutabilityLockSummary` to the object\n `ObjectStoreLocationSummary` and `ObjectStoreUpdateDefinition` used by\n `GET/POST /archive/object_store` and `GET/POST /archive/object_store/{id}`\n * Added a new optional field `errorMessage` to `SupportTunnelInfo` object \n used by `GET /node/{id}/support_tunnel` and\n `PATCH /node/{id}/support_tunnel`.\n * Added new optional field `cloudStorageLocation` to the `ClusterConfig`\n object used by `POST /cluster/{id}/bootstrap`.\n * Added new enum `Disabled` to `DataLocationOwnershipStatus`\n used by `ArchivalLocationSummary`\n * Added a new optional field `installTarball` to the `ClusterConfig`\n object used by `POST /cluster/{id}/bootstrap`.\n * Added a new optional field `clusterInstall` to the `ClusterConfigStatus`\n object used by `GET /cluster/{id}/bootstrap`.\n * Added the `GET /cluster/{id}/install` endpoint to return the current\n status of Rubrik CDM install on a cluster.\n * Added the `POST /cluster/{id}/install` endpoint to allow Rubrik CDM \n install on cluster nodes which are not bootstrapped.\n * Added the `GET /cluster/{id}/packages` endpoint to return the list of\n Rubrik CDM packages available for installation.\n * Updated `request_id` parameter in the `GET /cluster/{id}/bootstrap` \n endpoint, as not required.\n * Updated `request_id` parameter in the `GET /cluster/{id}/install` \n endpoint, as not required.\n * Updated `BootstrappableNodeInfo` returned by `GET /cluster/{id}/discover`\n endpoint to include the `version` field, to indicate the\n Rubrik CDM software version.\n * Added a new optional field `isSetupNetworkOnly` to the `ClusterConfig`\n object used by `POST /cluster/{id}/setupnetwork`.\n * Added the `POST /cluster/{id}/setupnetwork` endpoint to enable Rubrik CDM\n to perform network setup on nodes that are not bootstrapped.\n * Added the `GET /cluster/{id}/setupnetwork` endpoint to return the current\n status of setup network command on node or nodes.\n * Added a new optional field `hostname` to the `NodeStatus` object used by\n `GET /cluster/{id}/node`, `GET /node`, `GET /node/stats`, `GET /node/{id}`,\n and `GET /node/{id}/stats`.\n * Added new optional fields `usedFastVhdx` and `fileSizeInBytes` to the\n `HypervVirtualMachineSnapshotSummary` returned by the API\n `GET /hyperv/vm/{id}/snapshot`.\n * Added the `GET /archive/location/request/{id}` endpoint to query the status\n of asynchronous archival location requests.\n\n ## Deprecation:\n * Deprecated the following Oracle endpoints\n * `GET /oracle/db`\n * `GET /oracle/db/{id}`\n * `PATCH /oracle/db/{id}`\n * Deprecated the following vcd hierarchy endpoints. \n * `GET /vcd/hierarchy/{id}`\n * `GET /vcd/hierarchy/{id}/children`\n * `GET /vcd/hierarchy/{id}/descendants`\n * Deprecated the following vcd cluster endpoints.\n * `GET /vcd/cluster`\n * `POST /vcd/cluster`\n * `GET /vcd/cluster/{id}/vimserver`\n * `POST /vcd/cluster/{id}/refresh`\n * `GET /vcd/cluster/{id}`\n * `PATCH /vcd/cluster/{id}`\n * `DELETE /vcd/cluster/{id}`\n * `GET /vcd/cluster/request/{id}`\n * Deprecated the following vcd vapp endpoints.\n * `GET /vcd/vapp`\n * `GET /vcd/vapp/{id}`\n * `PATCH /vcd/vapp/{id}`\n * `GET /vcd/vapp/{id}/snapshot`\n * `POST /vcd/vapp/{id}/snapshot`\n * `DELETE /vcd/vapp/{id}/snapshot`\n * `GET/vcd/vapp/snapshot/{id}`\n * `DELETE /vcd/vapp/snapshot/{id}`\n * `GET /vcd/vapp`\n * `GET /vcd/vapp/{id}/missed_snapshot`\n * `GET /vcd/vapp/snapshot/{snapshot_id}/export/options`\n * `POST /vcd/vapp/snapshot/{snapshot_id}/export`\n * `POST /vcd/vapp/snapshot/{snapshot_id}/instant_recover`\n * `GET /vcd/vapp/snapshot/{snapshot_id}/instant_recover/options`\n * `GET /vcd/vapp/request/{id}`\n * `GET /vcd/vapp/{id}/search`\n * `POST /vcd/vapp/snapshot/{id}/download`\n\n ### Changes to Internal API in Rubrik version 5.3.2\n ## Deprecation:\n * Deprecated `compliance24HourStatus` in `DataSourceTableRequest` for\n `POST /report/data_source/table`.\n Use `complianceStatus`, `awaitingFirstFull`, and `snapshotRange`\n as replacements.\n\n ### Changes to Internal API in Rubrik version 5.3.1\n ## Breaking changes:\n * Added new required field `isPwdEncryptionSupported` to\n the API response `PlatformInfo` for password-based encryption at rest\n in the API `GET /cluster/{id}/platforminfo`.\n\n ## Feature additions/improvements:\n * Added new field `hostsInfo` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/children`.\n * Added new field `hostsInfo` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/descendants`.\n * Added new field `hostsInfo` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}`.\n * Added `shouldKeepConvertedDisksOnFailure` as an optional field in\n CreateCloudInstanceRequest definition used in the on-demand API\n conversion API `/cloud_on/aws/instance` and `/cloud_on/azure/instance`.\n This will enable converted disks to be kept on failure for CloudOn\n conversion.\n * Added the `hostsInfo` field to the OracleDbDetail that the\n `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}` endpoints return.\n * Added new optional field `isOnNetAppSnapMirrorDestVolume` to\n HostShareParameters to support backup of NetApp SnapMirror\n destination volume.\n * Added new optional fields `encryptionPassword` and\n `newEncryptionPassword` to the KeyRotationOptions to support\n key rotation for password-based encryption at rest in\n internal API `POST /cluster/{id}/security/key_rotation`.\n * Added `Index` to `ReportableTaskType`.\n * Added new optional field `totpStatus` in `UserDetail` for\n showing the TOTP status of the user with the endpoint\n `GET /internal/user/{id}`\n * Added new optional field `isTotpEnforced` in `UserDefinition` for\n configuring the TOTP enforcement for the user with the endpoint\n `POST /internal/user`\n * Added new optional field `isTotpEnforced` in `UserUpdateInfo` for\n configuring the TOTP enforcement for the user with the endpoint\n `PATCH /internal/user/{id}`\n * Added a new field `HypervVirtualDiskInfo` to HypervVirtualMachineDetail \n used by `GET /hyperv/vm/{id}`.\n * Added a new field `virtualDiskIdsExcludedFromSnapshot` to \n HypervVirtualMachineUpdate used by `PATCH /hyperv/vm/{id}`.\n\n ### Changes to Internal API in Rubrik version 5.3.0\n ## Deprecation:\n * Deprecated `GET /authorization/role/admin`,\n `GET /authorization/role/compliance_officer`,\n `GET /authorization/role/end_user`,\n `GET /authorization/role/infra_admin`,\n `GET /authorization/role/managed_volume_admin`,\n `GET /authorization/role/managed_volume_user`,\n `GET /authorization/role/org_admin`,\n `GET /authorization/role/organization`,\n `GET /authorization/role/read_only_admin` endpoints. Use the new\n v1 endpoints for role management.\n * Deprecated `SnapshotCloudStorageTier` enum value Cold. It will be left,\n but will be mapped internally to the new value, AzureArchive, which is\n recommended as a replacement.\n * Deprecated the `GET /snapshot/{id}/storage/stats` endpoint. Use the v1\n version when possible.\n * Deprecated `POST /hierarchy/bulk_sla_conflicts`. It is migrated to\n v1 and using that is recommended.\n * Deprecated `GET /mssql/availability_group`,\n `GET /mssql/availability_group/{id}`,\n `PATCH /mssql/availability_group/{id}`, `PATCH /mssql/db/bulk`,\n `POST /mssql/db/bulk/snapshot`, `GET /mssql/db/bulk/snapshot/{id}`,\n `GET /mssql/db/count`, `DELETE /mssql/db/{id}/recoverable_range/download`,\n `GET /mssql/db/{id}/compatible_instance`, `GET /mssql/instance/count`,\n `GET /mssql/db/{id}/restore_estimate`, `GET /mssql/db/{id}/restore_files`,\n `GET /mssql/db/{id}/snappable_id`, `GET /mssql/db/defaults`,\n `PATCH /mssql/db/defaults` and `GET /mssql/db/recoverable_range/download/{id}`\n endpoints. Use the v1 version when possible.\n ## Breaking changes:\n * Added new Boolean field `isLinkLocalIpv4Mode` to `AddNodesConfig` and\n `ReplaceNodeConfig`.\n * Changed the type for ReplicationSnapshotLag, which is used by /report/{id} GET\n and PATCH endpoints from integer to string.\n * Added new required field `objectStore` to DataSourceDownloadConfig used by\n `POST /report/data_source/download`.\n * Removed the `storageClass` field from the DataSourceDownloadConfig object used\n by the `POST /report/data_source/download` endpoint. The value was not used.\n * Removed endpoint `GET /mfa/rsa/server` and moved it to v1.\n * Removed endpoint `POST /mfa/rsa/server` and moved it to v1.\n * Removed endpoint `GET /mfa/rsa/server/{id}` and moved it to v1.\n * Removed endpoint `PATCH /mfa/rsa/server/{id}` and moved it to v1.\n * Removed endpoint `DELETE /mfa/rsa/server/{id}` and moved it to v1.\n * Removed endpoint `PUT /cluster/{id}/security/web_signed_cert`\n and moved it to v1.\n * Removed endpoint `DELETE /cluster/{id}/security/web_signed_cert`\n and moved it to v1\n * Removed endpoint `PUT /cluster/{id}/security/kmip/client` and added it\n to v1.\n * Removed endpoint `GET /cluster/{id}/security/kmip/client` and added it\n to v1.\n * Removed endpoint `GET /cluster/{id}/security/kmip/server` and added it\n to v1.\n * Removed endpoint `PUT /cluster/{id}/security/kmip/server` and added it\n to v1.\n * Removed endpoint `DELETE /cluster/{id}/security/kmip/server` and added\n it to v1.\n * Removed endpoint `POST /replication/global_pause`. To toggle replication\n pause between enabled and disabled, use\n `POST /v1/replication/location_pause/disable` and\n `POST /v1/replication/location_pause/enable` instead.\n * Removed `GET /replication/global_pause`. To retrieve replication pause\n status, use `GET /internal/replication/source` and\n `GET /internal/replication/source/{id}` instead.\n * Removed `GET /node_management/{id}/fetch_package` since it was never used.\n * Removed `GET /node_management/{id}/upgrade` since it was never used.\n * Removed `POST /node_management/{id}/fetch_package` since it was never used.\n * Removed `POST /node_management/{id}/upgrade` since it was never used.\n\n ## Feature additions/improvements:\n * Added new optional field `pubKey` to the GlobalManagerConnectionUpdate\n object and the GlobalManagerConnectionInfo object used by\n `GET /cluster/{id}/global_manager` and `PUT /cluster/{id}/global_manager`.\n * Added a new optional field `storageClass` to the `ArchivalLocationSummary`\n type.\n * Added optional field `StartMethod` to the following components: \n ChartSummary, TableSummary, ReportTableRequest, FilterSummary and\n RequestFilters.\n * Added new enum field `StackedReplicationComplianceCountByStatus` to the\n measure property in ChartSummary.\n * Added new enum fields `ReplicationInComplianceCount`,\n `ReplicationNonComplianceCount` to the following properties:\n measure property in ChartSummary, column property in TableSummary,\n and sortBy property in ReportTableRequest.\n * Added the endpoint `GET /vmware/config/datastore_freespace_threshold` to\n query the VMware datastore freespace threshold config.\n * Added the endpoint `PATCH /vmware/config/set_datastore_freespace_threshold`\n to update the VMware datastore freespace threshold config.\n * Added two new optional query parameters `offset` and `limit` to\n `GET /organization`.\n * Added two new optional query parameters `offset` and `limit` to\n `GET /user/{id}/organization`.\n * Modified `SnapshotCloudStorageTier`, enum adding values AzureArchive, Glacier,\n and GlacierDeepArchive.\n * Added the `lastValidationResult` field to the OracleDbDetail that the\n `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}` endpoints return.\n * Added `isValid` field to the OracleDbSnapshotSummary of\n OracleRecoverableRange that the `GET /oracle/db/\n {id}/recoverable_range` endpoint returns.\n * Added the `isRemoteGlobalBlackoutActive` field to the\n ReplicationSourceSummary object that the\n `GET /organization/{id}/replication/source` endpoint returns.\n * Added the `isRemoteGlobalBlackoutActive` field to the\n ReplicationSourceSummary object that the\n `GET /replication/source/{id}` endpoint returns.\n * Added the `isRemoteGlobalBlackoutActive` field to the\n ReplicationSourceSummary object that the\n `GET /replication/source` endpoint returns.\n * Added the `isReplicationTargetPauseEnabled` field to the\n ReplicationSourceSummary object that the\n `GET /organization/{id}/replication/source` endpoint returns.\n * Added the `isReplicationTargetPauseEnabled` field to the\n ReplicationSourceSummary object that the\n `GET /replication/source/{id}` endpoint returns.\n * Added the `isReplicationTargetPauseEnabled` field to the\n ReplicationSourceSummary object that the\n `GET /replication/source` endpoint returns.\n * Added new optional field `cloudRehydrationSpeed` to the\n ObjectStoreLocationSummary, ObjectStoreUpdateDefinition,\n PolarisAwsArchivalLocationSpec, and PolarisAzureArchivalLocationSpec\n objects to specify the rehydration speed to use when performing cloud\n rehydration on objects tiered cold storage.\n * Added new optional field earliestTimestamp to the `POST\n /polaris/export_info` endpoint to enable incremental MDS synchronization.\n * Added new values `RetentionSlaDomainName` , `ObjectType`, `SnapshotCount`,\n `AutoSnapshotCount` and `ManualSnapshotCount` to\n `UnmanagedObjectSortAttribute` field of the `GET /unmanaged_object` endpont.\n * Added new optional field `endpoint` to the ObjectStorageDetail\n object used by several Polaris APIs.\n * Added new optional field `accessKey` to the ObjectStorageConfig\n object used by several Polaris APIs.\n * Added new optional field `endpoint` to DataSourceDownloadConfig used by\n `POST /report/data_source/download`.\n * Added new field `slaClientConfig` to the `ManagedVolumeUpdate`\n object used by the `PATCH /managed_volume/{id}` endpoint to enable\n edits to the configuration of SLA Managed Volumes.\n * Added new field `shouldSkipPrechecks` to DecommissionNodesConfig used by\n `POST /cluster/{id}/decommission_nodes`.\n * Added new query parameter `managed_volume_type` to allow filtering\n managed volumes based on their type using the `GET /managed_volume`\n endpoint.\n * Added new query parameter `managed_volume_type` to allow filtering\n managed volume exports based on their source managed volume type\n using the `GET /managed_volume/snapshot/export` endpoint.\n * Added the new fields `mvType` and `slaClientConfig` to the\n `ManagedVolumeConfig` object. These fields are used with the\n `POST /managed_volume` endpoint to manage SLA Managed Volumes.\n * Added the new fields `mvType` and `slaManagedVolumeDetails` to the\n `ManagedVolumeSummary` object returned by the `GET /managed_volume`,\n `POST /managed_volume`, `GET /managed_volume/{id}` and\n `POST /managed_volume/{id}` endpoints.\n * Added new field `mvType` to the `ManagedVolumeSnapshotExportSummary`\n object returned by the `GET /managed_volume/snapshot/export` and\n `GET /managed_volume/snapshot/export/{id}` endpoints.\n * Added optional field `hostMountPoint` in the `ManagedVolumeChannelConfig`.\n `ManagedVolumeChannelConfig` is returned as part of\n `ManagedVolumeSnapshotExportSummary`, which is returned\n by the `GET /managed_volume/snapshot/export` and\n `GET /managed_volume/snapshot/export/{id}` endpoints.\n * Added `POST /managed_volume/{id}/snapshot` method to take an on\n demand snapshot for SLA Managed Volumes.\n * Added new field `isPrimary` to OracleDbSummary returned by\n `GET /oracle/db`.\n * Added new field `isPrimary` to OracleDbDetail returned by\n `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}`.\n * Added new field `isOracleHost` to HostDetail\n returned by `GET /host/{id}`.\n * Added optional isShareAutoDiscoveryAndAdditionEnabled in the\n NasBaseConfig and NasConfig.\n NasBaseConfig is returned as part of HostSummary, which is returned by the\n `Get /host/envoy` and `Get /host` endpoints. NasConfig is used by\n HostRegister and HostUpdate. The HostRegister field is used by the\n `Post /host/bulk` endpoint and the HostUpdate is field used by the\n `PATCH /host/bulk` endpoint.\n * Added new endpoint `POST /managed_volume/{id}/resize` to resize managed\n volume to a larger size.\n * Added ReplicationComplianceStatus as an optional field to the TableSummary\n which is used by /report/{id} GET and PATCH endpoints and to RequestFilters\n which is used by /report/data_source/table.\n * Added `PATCH /cluster/{id}/trial_edge` endpoint to extend the trial period.\n * Added new optional fields `extensionsLeft` and `daysLeft` to\n EdgeTrialStatus returned by `GET /cluster/{id}/trial_edge` and\n `PATCH /cluster/{id}/trial_edge`.\n * Added new endpoint `POST /managed_volume/snapshot/{id}/restore` to export a\n managed volume snapshot and mount it on a host.\n * Added new endpoints `PATCH /config/{component}/reset` to allow configs to\n be reset to DEFAULT state.\n * Added a new field `logRetentionTimeInHours` to the `MssqlDbDefaults`\n object returned by the `GET /mssql/db/defaults` and\n `PATCH /mssql/db/defaults` endpoints.\n * Added new optional field `logRetentionTimeInHours` to `MssqlDbDefaultsUpdate`\n object which is used by `PATCH /mssql/db/defaults`.\n * Added new optional field `unreadable` to `BrowseResponse` and\n `SnapshotSearchResponse`, which are used by `GET /browse` and\n `GET /search/snapshot_search` respectively.\n * Added MissedReplicationSnapshots as an optional field to the TableSummary\n which is used by /report/{id} GET and PATCH endpoints.\n * Added new optional field `pitRecoveryInfo` to `ChildSnappableFailoverInfo`\n object which is used by `PUT /polaris/failover/target/{id}/start`\n * Added ReplicationDataLag as an optional field to the TableSummary\n which is used by /report/{id} GET and PATCH endpoints.\n * Added UnreplicatedSnapshots as an optional field to the TableSummary\n which is used by /report/{id} GET and PATCH endpoints.\n * Added the field `networkAdapterType` to `VappVmNetworkConnection`.\n `VappVmNetworkConnection` is returned by the\n `GET /vcd/vapp/snapshot/{snapshot_id}/instant_recover/options` and\n `GET /vcd/vapp/snapshot/{snapshot_id}/export/options` endpoints and is\n used by the `POST /vcd/vapp/snapshot/{snapshot_id}/export` and\n `POST /vcd/vapp/snapshot/{snapshot_id}/instant_recover` endpoints.\n Also added `VcdVmSnapshotDetail`, which is returned by the\n `GET /vcd/vapp/snapshot/{id}` endpoint.\n * Added new endpoint `GET /report/template` to return details\n of a report template.\n * Added new endpoint `POST /report/{id}/send_email` to send an email of the report.\n ## Breaking changes:\n * Made field `restoreScriptSmbPath` optional in `VolumeGroupMountSummary`.\n Endpoints `/volume_group/snapshot/mount` and\n `/volume_group/snapshot/mount/{id}` are affected by this change.\n * Moved endpoints `GET /volume_group`, `GET /volume_group/{id}`,\n `PATCH /volume_group/{id}`, `GET /volume_group/{id}/snapshot`,\n `POST /volume_group/{id}/snapshot`, `GET /volume_group/snapshot/{id}`,\n `GET /volume_group/snapshot/mount`, and\n `GET /volume_group/snapshot/mount/{id}` from internal to v1.\n * Moved endpoint `GET /host/{id}/volume` from internal to v1.\n\n ### Changes to Internal API in Rubrik version 5.2.2\n ## Feature Additions/improvements:\n * Added new field `exposeAllLogs` to ExportOracleTablespaceConfig\n used by `POST /oracle/db/{id}/export/tablespace`.\n\n ### Changes to Internal API in Rubrik version 5.2.1\n ## Feature Additions/improvements:\n * Added new field `shouldBlockOnNegativeFailureTolerance` to\n DecommissionNodesConfig used by `POST /cluster/{id}/decommission_nodes`.\n\n ### Changes to Internal API in Rubrik version 5.2.0\n ## Deprecation:\n * Deprecating `GET /replication/global_pause`. Use\n `GET /internal/replication/source` and\n `GET /internal/replication/source/{id}` to retrieve replication\n pause status in CDM v5.3.\n * Deprecating `POST /replication/global_pause`. Use\n `POST /v1/replication/location_pause/disable` and\n `POST /v1/replication/location_pause/enable` to toggle replication\n pause in CDM v5.3.\n * Deprecating `slaId` field returned by `GET /vcd/vapp/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /vcd/vapp/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /oracle/db/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /oracle/db/\n {id}/recoverable_range`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /oracle/db/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /hyperv/vm/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /hyperv/vm/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /volume_group/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /volume_group/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /storage/array_volume_group\n/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /vcd/vapp/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /host_fileset/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /host_fileset/share/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /app_blueprint/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /app_blueprint/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /managed_volume/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `POST /managed_volume/{id\n}/end_snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /managed_volume/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /aws/ec2_instance/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /aws/ec2_instance/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /nutanix/vm/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /nutanix/vm/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /fileset/bulk`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Added a new field `pendingSlaDomain` to `VirtualMachineDetail`\n object referred by `VappVmDetail` returned by\n `GET /vcd/vapp/{id}` and `PATCH /vcd/vapp/{id}`\n * Deprecated `POST /internal/vmware/vcenter/{id}/refresh_vm` endpoint. Use\n `POST /v1/vmware/vcenter/{id}/refresh_vm` instead to refresh a\n virtual machine by MOID.\n\n ## Breaking changes:\n* Rename the field configuredSlaDomainId in the OracleUpdate object to\n configuredSlaDomainIdDeprecated and modify the behavior so\n configuredSlaDomainIdDeprecated is only used to determine log backup\n frequency and not to set retention time.\n* Removed `GET /event/count_by_status` endpoint and it will be\n replaced by `GET /job_monitoring/summary_by_job_state`.\n* Removed `GET /event/count_by_job_type` endpoint and it will be\n replaced by `GET /job_monitoring/summary_by_job_type`.\n* Removed `GET /event_series` endpoint and it will be replaced by\n `GET /job_monitoring`.\n* Refactor `PUT /cluster/{id}/security/web_signed_cert` to accept\n certificate_id instead of X.509 certificate text. Also removed\n the `POST /cluster/{id}/security/web_csr` endpoint.\n * Refactor `GET /rsa-server`, `POST /rsa-server`, `GET /rsa-server/{id}`,\n and `PATCH /rsa-server/{id}` to take in a certificate ID instead of\n a certificate.\n * Changed definition of CloudInstanceUpdate by updating the enums ON/OFF\n to POWERSTATUS_ON/POWERSTATUS_OFF\n * Removed `GET /event_series/{status}/csv_link` endpoint to download CSV\n with job monitoring information. It has been replaced by the\n `GET /job_monitoring//csv_download_link` v1 endpoint.\n * Removed GET `/report/summary/physical_storage_time_series`. Use\n GET `/stats/total_physical_storage/time_series` instead.\n * Removed GET `/report/summary/average_local_growth_per_day`. Use\n GET `/stats/average_storage_growth_per_day` instead.\n * Removed POST `/job/instances/`. Use GET `/job/{job_id}/instances` instead.\n * Removed the POST `/cluster/{id}/reset` endpoint.\n * Removed GET `/user`. Use the internal POST `/principal_search`\n or the v1 GET `/principal` instead for querying any principals,\n including users.\n\n ## Feature additions/improvements:\n * Added the `GET /replication/global_pause` endpoint to return the current\n status of global replication pause. Added the `POST /replication/global_pause`.\n endpoint to toggle the replication target global pause jobs status. When\n global replication pause is enabled, all replication jobs on the local\n cluster are paused. When disabling global replication pause, optional\n parameter `shouldOnlyReplicateNewSnapshots` can be set to `true` to only\n replicate snapshots taken after disabling the pause. These endpoints must\n be used at the target cluster.\n * Added new field `parentSnapshotId` to AppBlueprintSnapshotSummary returned\n by `GET /app_blueprint/{id}/snapshot`.\n * Added new field `parentSnapshotId` to AppBlueprintSnapshotDetail returned\n by `GET /app_blueprint/snapshot/{id}`.\n * Added new field `parentSnapshotId` to AwsEc2InstanceSummary returned by\n `GET /aws/ec2_instance`.\n * Added new field `parentSnapshotId` to AwsEc2InstanceDetail returned by\n `GET /aws/ec2_instance/{id}`.\n * Added new field `parentSnapshotId` to AwsEc2InstanceDetail returned by\n `PATCH /aws/ec2_instance/{id}`.\n * Added new field `parentSnapshotId` to HypervVirtualMachineSnapshotSummary\n returned by `GET /hyperv/vm/{id}/snapshot`.\n * Added new field `parentSnapshotId` to HypervVirtualMachineSnapshotDetail\n returned by `GET /hyperv/vm/snapshot/{id}`.\n * Added new field `parentSnapshotId` to ManagedVolumeSnapshotSummary\n returned by `GET /managed_volume/{id}/snapshot`.\n * Added new field `parentSnapshotId` to ManagedVolumeSnapshotSummary\n returned by `POST /managed_volume/{id}/end_snapshot`.\n * Added new field `parentSnapshotId` to ManagedVolumeSnapshotDetail returned\n by `GET /managed_volume/snapshot/{id}`.\n * Added new field `parentSnapshotId` to NutanixVmSnapshotSummary returned by\n `GET /nutanix/vm/{id}/snapshot`.\n * Added new field `parentSnapshotId` to NutanixVmSnapshotDetail returned by\n `GET /nutanix/vm/snapshot/{id}`.\n * Added new field `parentSnapshotId` to OracleDbSnapshotSummary returned by\n `GET /oracle/db/{id}/snapshot`.\n * Added new field `parentSnapshotId` to OracleDbSnapshotDetail returned by\n `GET /oracle/db/snapshot/{id}`.\n * Added new field `parentSnapshotId` to StorageArrayVolumeGroupSnapshotSummary\n returned by `GET /storage/array_volume_group/{id}/snapshot`.\n * Added new field `parentSnapshotId` to StorageArrayVolumeGroupSnapshotDetail\n returned by `GET /storage/array_volume_group/snapshot/{id}`.\n * Added new field `parentSnapshotId` to VcdVappSnapshotSummary returned by\n `GET /vcd/vapp/{id}/snapshot`.\n * Added new field `parentSnapshotId` to VcdVappSnapshotDetail returned by\n `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `parentSnapshotId` to VolumeGroupSnapshotSummary returned by\n `GET /volume_group/{id}/snapshot`.\n * Added new field `parentSnapshotId` to VolumeGroupSnapshotDetail returned by\n `GET /volume_group/snapshot/{id}`.\n * Added new field `retentionSlaDomanId` to MssqlAvailabilityGroupSummary\n returned by `GET /mssql/availability_group`.\n * Added new field `retentionSlaDomanId` to MssqlAvailabilityGroupDetail\n returned by `GET /mssql/availability_group/{id}`.\n * Added new field `retentionSlaDomanId` to MssqlAvailabilityGroupDetail\n returned by `PATCH /mssql/availability_group/{id}`.\n * Added new field `retentionSlaDomainId` to UnmanagedObjectSummary\n returned by `GET /unmanaged_object`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `GET /managed_volume`.\n * Added new field `retentionSlaDomainId` to AppBlueprintDetail\n returned by `GET /app_blueprint/{id}`.\n * Added new field `retentionSlaDomainId` to AppBlueprintDetail\n returned by `PATCH /polaris/app_blueprint/{id}`.\n * Added new field `retentionSlaDomainId` to AppBlueprintDetail\n returned by `POST /polaris/app_blueprint`.\n * Added new field `retentionSlaDomainId` to AppBlueprintExportSnapshotJobConfig\n returned by `POST /polaris/app_blueprint/snapshot/{id}/export`.\n * Added new field `retentionSlaDomainId` to AppBlueprintInstantRecoveryJobConfig\n returned by `POST /polaris/app_blueprint/snapshot/{id}/instant_recover`.\n * Added new field `retentionSlaDomainId` to AppBlueprintMountSnapshotJobConfig\n returned by `POST /polaris/app_blueprint/snapshot/{id}/mount`.\n * Added new field `retentionSlaDomainId` to AppBlueprintSummary\n returned by `GET /app_blueprint`.\n * Added new field `retentionSlaDomainId` to AwsEc2InstanceDetail\n returned by `GET /aws/ec2_instance/{id}`.\n * Added new field `retentionSlaDomainId` to AwsEc2InstanceDetail\n returned by `PATCH /aws/ec2_instance/{id}`.\n * Added new field `retentionSlaDomainId` to AwsEc2InstanceSummary\n returned by `GET /aws/ec2_instance`.\n * Added new field `retentionSlaDomainId` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to HypervHierarchyObjectSummary\n returned by `GET /organization/{id}/hyperv`.\n * Added new field `retentionSlaDomainId` to HypervVirtualMachineDetail\n returned by `GET /hyperv/vm/{id}`.\n * Added new field `retentionSlaDomainId` to HypervVirtualMachineDetail\n returned by `PATCH /hyperv/vm/{id}`.\n * Added new field `retentionSlaDomainId` to HypervVirtualMachineSummary\n returned by `GET /hyperv/vm`.\n * Added new field `retentionSlaDomainId` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}/sla_conflicts`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `GET /managed_volume/{id}`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `GET /organization/{id}/managed_volume`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `PATCH /managed_volume/{id}`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `POST /managed_volume`.\n * Added new field `retentionSlaDomainId` to MountDetail\n returned by `GET /vmware/vm/snapshot/mount/{id}`.\n * Added new field `retentionSlaDomainId` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to NutanixHierarchyObjectSummary\n returned by `GET /organization/{id}/nutanix`.\n * Added new field `retentionSlaDomainId` to OracleDbDetail\n returned by `GET /oracle/db/{id}`.\n * Added new field `retentionSlaDomainId` to OracleDbDetail\n returned by `PATCH /oracle/db/{id}`.\n * Added new field `retentionSlaDomainId` to OracleDbSummary\n returned by `GET /oracle/db`.\n * Added new field `retentionSlaDomainId` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to OracleHierarchyObjectSummary\n returned by `GET /organization/{id}/oracle`.\n * Added new field `retentionSlaDomainId` to SlaConflictsSummary\n returned by `POST /hierarchy/bulk_sla_conflicts`.\n * Added new field `retentionSlaDomainId` to SnappableRecoverySpecDetails\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new field `retentionSlaDomainId` to SnappableRecoverySpec\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new field `retentionSlaDomainId` to Snappable\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new field `retentionSlaDomainId` to Snappable\n returned by `POST /stats/snappable_storage`.\n * Added new field `retentionSlaDomainId` to StorageArrayHierarchyObjectSummary\n returned by `GET /organization/{id}/storage/array`.\n * Added new field `retentionSlaDomainId` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to StorageArrayVolumeGroupDetail\n returned by `GET /storage/array_volume_group/{id}`.\n * Added new field `retentionSlaDomainId` to StorageArrayVolumeGroupDetail\n returned by `PATCH /storage/array_volume_group/{id}`.\n * Added new field `retentionSlaDomainId` to StorageArrayVolumeGroupDetail\n returned by `POST /storage/array_volume_group`.\n * Added new field `retentionSlaDomainId` to StorageArrayVolumeGroupSummary\n returned by `GET /storage/array_volume_group`.\n * Added new field `retentionSlaDomainId` to TriggerFailoverOnTargetDefinition\n returned by `PUT /polaris/failover/target/{id}/resume`.\n * Added new field `retentionSlaDomainId` to TriggerFailoverOnTargetDefinition\n returned by `PUT /polaris/failover/target/{id}/start`.\n * Added new field `retentionSlaDomainId` to UpsertSnappableRecoverySpecResponse\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new field `retentionSlaDomainId` to VcdHierarchyObjectSummary\n returned by `GET /organization/{id}/vcd`.\n * Added new field `retentionSlaDomainId` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to VcdVappDetail\n returned by `GET /vcd/vapp/{id}`.\n * Added new field `retentionSlaDomainId` to VcdVappDetail\n returned by `PATCH /vcd/vapp/{id}`.\n * Added new field `retentionSlaDomainId` to VcdVappSnapshotDetail\n returned by `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `retentionSlaDomainId` to VolumeGroupDetail\n returned by `GET /volume_group/{id}`.\n * Added new field `retentionSlaDomainId` to VolumeGroupDetail\n returned by `PATCH /volume_group/{id}`.\n * Added new field `retentionSlaDomainId` to VolumeGroupSummary\n returned by `GET /volume_group`.\n * Added new field `retentionSlaDomainId` to AwsHierarchyObjectSummary\n returned by `GET /organization/{id}/aws`.\n * Added new field `retentionSlaDomainId` to VmwareVmMountSummary\n returned by `GET /vmware/vm/snapshot/mount`.\n * Added new field `retentionSlaDomainId` to VcdVappSummary\n returned by `GET /vcd/vapp`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `GET /replication/target`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `POST /replication/target`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `GET /replication/target/{id}`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `GET /replication/target/{id}`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `PATCH /replication/target/{id}`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `GET /organization/{id}/replication/target`.\n * Added new field `hasSnapshotsWithPolicy` to UnmanagedObjectSummary returned\n by GET `/unmanaged_object`\n * Added new field `slaLastUpdateTime` to AppBlueprintDetail\n returned by POST `/polaris/app_blueprint`.\n * Added new field `slaLastUpdateTime` to AppBlueprintDetail\n returned by `GET /app_blueprint/{id}`.\n * Added new field `slaLastUpdateTime` to AppBlueprintDetail\n returned by `PATCH /polaris/app_blueprint/{id}`.\n * Added new field `slaLastUpdateTime` to AppBlueprintExportSnapshotJobConfig\n returned by POST `/polaris/app_blueprint/snapshot/{id}/export`.\n * Added new field `slaLastUpdateTime` to AppBlueprintInstantRecoveryJobConfig\n returned by POST `/polaris/app_blueprint/snapshot/{id}/instant_recover`.\n * Added new field `slaLastUpdateTime` to AppBlueprintMountSnapshotJobConfig\n returned by POST `/polaris/app_blueprint/snapshot/{id}/mount`.\n * Added new field `slaLastUpdateTime` to AppBlueprintSummary\n returned by `GET /app_blueprint`.\n * Added new field `slaLastUpdateTime` to AwsAccountDetail\n returned by `PATCH /aws/account/dca/{id}`.\n * Added new field `slaLastUpdateTime` to AwsAccountDetail\n returned by `GET /aws/account/{id}`.\n * Added new field `slaLastUpdateTime` to AwsAccountDetail\n returned by `PATCH /aws/account/{id}`.\n * Added new field `slaLastUpdateTime` to AwsEc2InstanceDetail\n returned by `GET /aws/ec2_instance/{id}`.\n * Added new field `slaLastUpdateTime` to AwsEc2InstanceDetail\n returned by `PATCH /aws/ec2_instance/{id}`.\n * Added new field `slaLastUpdateTime` to FilesetDetail\n returned by POST `/fileset/bulk`.\n * Added new field `slaLastUpdateTime` to AwsEc2InstanceSummary\n returned by `GET /aws/ec2_instance`.\n * Added new field `slaLastUpdateTime` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to AwsHierarchyObjectSummary\n returned by `GET /organization/{id}/aws`.\n * Added new field `slaLastUpdateTime` to DataCenterDetail\n returned by `GET /vmware/data_center/{id}`.\n * Added new field `slaLastUpdateTime` to DataCenterSummary\n returned by `GET /vmware/data_center`.\n * Added new field `slaLastUpdateTime` to DataStoreDetail\n returned by `GET /vmware/datastore/{id}`.\n * Added new field `slaLastUpdateTime` to FolderDetail\n returned by `GET /folder/host/{datacenter_id}`.\n * Added new field `slaLastUpdateTime` to FolderDetail\n returned by `GET /folder/vm/{datacenter_id}`.\n * Added new field `slaLastUpdateTime` to FolderDetail\n returned by `GET /folder/{id}`.\n * Added new field `slaLastUpdateTime` to HostFilesetDetail\n returned by `GET /host_fileset/{id}`.\n * Added new field `slaLastUpdateTime` to HostFilesetShareDetail\n returned by `GET /host_fileset/share/{id}`.\n * Added new field `slaLastUpdateTime` to HostFilesetShareSummary\n returned by `GET /host_fileset/share`.\n * Added new field `slaLastUpdateTime` to HostFilesetSummary\n returned by `GET /host_fileset`.\n * Added new field `slaLastUpdateTime` to HypervClusterDetail\n returned by `GET /hyperv/cluster/{id}`.\n * Added new field `slaLastUpdateTime` to HypervClusterDetail\n returned by `PATCH /hyperv/cluster/{id}`.\n * Added new field `slaLastUpdateTime` to HypervClusterSummary\n returned by `GET /hyperv/cluster`.\n * Added new field `slaLastUpdateTime` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to HypervHierarchyObjectSummary\n returned by `GET /organization/{id}/hyperv`.\n * Added new field `slaLastUpdateTime` to HypervHostDetail\n returned by `GET /hyperv/host/{id}`.\n * Added new field `slaLastUpdateTime` to HypervHostDetail\n returned by `PATCH /hyperv/host/{id}`.\n * Added new field `slaLastUpdateTime` to HypervHostSummary\n returned by `GET /hyperv/host`.\n * Added new field `slaLastUpdateTime` to HypervScvmmDetail\n returned by `GET /hyperv/scvmm/{id}`.\n * Added new field `slaLastUpdateTime` to HypervScvmmDetail\n returned by `PATCH /hyperv/scvmm/{id}`.\n * Added new field `slaLastUpdateTime` to HypervScvmmSummary\n returned by `GET /hyperv/scvmm`.\n * Added new field `slaLastUpdateTime` to HypervVirtualMachineDetail\n returned by `GET /hyperv/vm/{id}`.\n * Added new field `slaLastUpdateTime` to HypervVirtualMachineDetail\n returned by `PATCH /hyperv/vm/{id}`.\n * Added new field `slaLastUpdateTime` to HypervVirtualMachineSummary\n returned by `GET /hyperv/vm`.\n * Added new field `slaLastUpdateTime` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}/sla_conflicts`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by `GET /managed_volume`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by POST `/managed_volume`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by `GET /managed_volume/{id}`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by `PATCH /managed_volume/{id}`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by `GET /organization/{id}/managed_volume`.\n * Added new field `slaLastUpdateTime` to MountDetail\n returned by `GET /vmware/vm/snapshot/mount/{id}`.\n * Added new field `slaLastUpdateTime` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to NutanixHierarchyObjectSummary\n returned by `GET /organization/{id}/nutanix`.\n * Added new field `slaLastUpdateTime` to OracleDbDetail\n returned by `GET /oracle/db/{id}`.\n * Added new field `slaLastUpdateTime` to OracleDbDetail\n returned by `PATCH /oracle/db/{id}`.\n * Added new field `slaLastUpdateTime` to OracleDbSummary\n returned by `GET /oracle/db`.\n * Added new field `slaLastUpdateTime` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to OracleHierarchyObjectSummary\n returned by `GET /organization/{id}/oracle`.\n * Added new field `slaLastUpdateTime` to OracleHostDetail\n returned by `GET /oracle/host/{id}`.\n * Added new field `slaLastUpdateTime` to OracleHostDetail\n returned by `PATCH /oracle/host/{id}`.\n * Added new field `slaLastUpdateTime` to OracleHostSummary\n returned by `GET /oracle/host`.\n * Added new field `slaLastUpdateTime` to OracleRacDetail\n returned by `GET /oracle/rac/{id}`.\n * Added new field `slaLastUpdateTime` to OracleRacDetail\n returned by `PATCH /oracle/rac/{id}`.\n * Added new field `slaLastUpdateTime` to OracleRacSummary\n returned by `GET /oracle/rac`.\n * Added new field `slaLastUpdateTime` to Snappable\n returned by POST `/polaris/failover/recovery_spec/upsert`.\n * Added new field `slaLastUpdateTime` to SnappableRecoverySpec\n returned by POST `/polaris/failover/recovery_spec/upsert`.\n * Added new field `slaLastUpdateTime` to SnappableRecoverySpecDetails\n returned by POST `/polaris/failover/recovery_spec/upsert`.\n * Added new field `slaLastUpdateTime` to StorageArrayHierarchyObjectSummary\n returned by `GET /organization/{id}/storage/array`.\n * Added new field `slaLastUpdateTime` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to StorageArrayVolumeGroupDetail\n returned by POST `/storage/array_volume_group`.\n * Added new field `slaLastUpdateTime` to StorageArrayVolumeGroupDetail\n returned by `GET /storage/array_volume_group/{id}`.\n * Added new field `slaLastUpdateTime` to StorageArrayVolumeGroupDetail\n returned by `PATCH /storage/array_volume_group/{id}`.\n * Added new field `slaLastUpdateTime` to StorageArrayVolumeGroupSummary\n returned by `GET /storage/array_volume_group`.\n * Added new field `slaLastUpdateTime` to VcdClusterDetail\n returned by `GET /vcd/cluster/{id}`.\n * Added new field `slaLastUpdateTime` to VcdClusterDetail\n returned by `PATCH /vcd/cluster/{id}`.\n * Added new field `slaLastUpdateTime` to VcdClusterSummary\n returned by `GET /vcd/cluster`.\n * Added new field `slaLastUpdateTime` to VcdHierarchyObjectSummary\n returned by `GET /organization/{id}/vcd`.\n * Added new field `slaLastUpdateTime` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to VcdVappDetail\n returned by `GET /vcd/vapp/{id}`.\n * Added new field `slaLastUpdateTime` to VcdVappDetail\n returned by `PATCH /vcd/vapp/{id}`.\n * Added new field `slaLastUpdateTime` to VcdVappSnapshotDetail\n returned by `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `slaLastUpdateTime` to VcdVappSummary\n returned by `GET /vcd/vapp`.\n * Added new field `slaLastUpdateTime` to VmwareVmMountSummary\n returned by `GET /vmware/vm/snapshot/mount`.\n * Added new field `slaLastUpdateTime` to VolumeGroupDetail\n returned by `GET /volume_group/{id}`.\n * Added new field `slaLastUpdateTime` to VolumeGroupDetail\n returned by `PATCH /volume_group/{id}`.\n * Added new field `slaLastUpdateTime` to VolumeGroupSummary\n returned by `GET /volume_group`.\n * Added new field `slaLastUpdateTime` to VsphereCategory\n returned by `GET /vmware/vcenter/{id}/tag_category`.\n * Added new field `slaLastUpdateTime` to VsphereCategory\n returned by `GET /vmware/vcenter/tag_category/{tag_category_id}`.\n * Added new field `slaLastUpdateTime` to VsphereTag\n returned by `GET /vmware/vcenter/{id}/tag`.\n * Added new field `slaLastUpdateTime` to VsphereTag\n returned by `GET /vmware/vcenter/tag/{tag_id}`.\n * Added new Field `configuredSlaDomainType` to AppBlueprintDetail returned by\n `POST /polaris/app_blueprint`.\n * Added new Field `configuredSlaDomainType` to AppBlueprintDetail returned by\n `GET /app_blueprint/{id}`.\n * Added new Field `configuredSlaDomainType` to AppBlueprintDetail returned by\n `PATCH /polaris/app_blueprint/{id}`.\n * Added new Field `configuredSlaDomainType` to\n AppBlueprintExportSnapshotJobConfig returned by\n `POST /polaris/app_blueprint/snapshot/{id}/export`.\n * Added new Field `configuredSlaDomainType` to\n AppBlueprintInstantRecoveryJobConfig returned by\n `POST /polaris/app_blueprint/snapshot/{id}/instant_recover`.\n * Added new Field `configuredSlaDomainType` to\n AppBlueprintMountSnapshotJobConfig returned by\n `POST /polaris/app_blueprint/snapshot/{id}/mount`.\n * Added new Field `configuredSlaDomainType` to AppBlueprintSummary returned by\n `GET /app_blueprint`.\n * Added new Field `configuredSlaDomainType` to AwsAccountDetail returned by\n `PATCH /aws/account/dca/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsAccountDetail returned by\n `GET /aws/account/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsAccountDetail returned by\n `PATCH /aws/account/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsEc2InstanceDetail returned\n by `GET /aws/ec2_instance/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsEc2InstanceDetail returned\n by `PATCH /aws/ec2_instance/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsEc2InstanceSummary returned\n by `GET /aws/ec2_instance`.\n * Added new Field `configuredSlaDomainType` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to AwsHierarchyObjectSummary\n returned by `GET /organization/{id}/aws`.\n * Added new Field `configuredSlaDomainType` to DataCenterDetail returned by\n `GET /vmware/data_center/{id}`.\n * Added new Field `configuredSlaDomainType` to DataCenterSummary returned by\n `GET /vmware/data_center`.\n * Added new Field `configuredSlaDomainType` to DataStoreDetail returned by\n `GET /vmware/datastore/{id}`.\n * Added new Field `configuredSlaDomainType` to FilesetDetail returned by\n `POST /fileset/bulk`.\n * Added new Field `configuredSlaDomainType` to FolderDetail returned by\n `GET /folder/host/{datacenter_id}`.\n * Added new Field `configuredSlaDomainType` to FolderDetail returned by\n `GET /folder/vm/{datacenter_id}`.\n * Added new Field `configuredSlaDomainType` to FolderDetail returned by\n `GET /folder/{id}`.\n * Added new Field `configuredSlaDomainType` to HostFilesetDetail returned by\n `GET /host_fileset/{id}`.\n * Added new Field `configuredSlaDomainType` to HostFilesetShareDetail returned\n by `GET /host_fileset/share/{id}`.\n * Added new Field `configuredSlaDomainType` to HostFilesetShareSummary\n returned by `GET /host_fileset/share`.\n * Added new Field `configuredSlaDomainType` to HostFilesetSummary returned by\n `GET /host_fileset`.\n * Added new Field `configuredSlaDomainType` to HypervClusterDetail returned by\n `GET /hyperv/cluster/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervClusterDetail returned by\n `PATCH /hyperv/cluster/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervClusterSummary returned\n by `GET /hyperv/cluster`.\n * Added new Field `configuredSlaDomainType` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to HypervHierarchyObjectSummary\n returned by `GET /organization/{id}/hyperv`.\n * Added new Field `configuredSlaDomainType` to HypervHostDetail returned by\n `GET /hyperv/host/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervHostDetail returned by\n `PATCH /hyperv/host/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervHostSummary returned by\n `GET /hyperv/host`.\n * Added new Field `configuredSlaDomainType` to HypervScvmmDetail returned by\n `GET /hyperv/scvmm/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervScvmmDetail returned by\n `PATCH /hyperv/scvmm/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervScvmmSummary returned by\n `GET /hyperv/scvmm`.\n * Added new Field `configuredSlaDomainType` to HypervVirtualMachineDetail\n returned by `GET /hyperv/vm/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervVirtualMachineDetail\n returned by `PATCH /hyperv/vm/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervVirtualMachineSummary\n returned by `GET /hyperv/vm`.\n * Added new Field `configuredSlaDomainType` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}/sla_conflicts`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `GET /managed_volume`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `POST /managed_volume`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `GET /managed_volume/{id}`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `PATCH /managed_volume/{id}`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `GET /organization/{id}/managed_volume`.\n * Added new Field `configuredSlaDomainType` to MountDetail returned by\n `GET /vmware/vm/snapshot/mount/{id}`.\n * Added new Field `configuredSlaDomainType` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to NutanixHierarchyObjectSummary\n returned by `GET /organization/{id}/nutanix`.\n * Added new Field `configuredSlaDomainType` to OracleDbDetail returned by\n `GET /oracle/db/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleDbDetail returned by\n `PATCH /oracle/db/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleDbSummary returned by\n `GET /oracle/db`.\n * Added new Field `configuredSlaDomainType` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to OracleHierarchyObjectSummary\n returned by `GET /organization/{id}/oracle`.\n * Added new Field `configuredSlaDomainType` to OracleHostDetail returned by\n `GET /oracle/host/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleHostDetail returned by\n `PATCH /oracle/host/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleHostSummary returned by\n `GET /oracle/host`.\n * Added new Field `configuredSlaDomainType` to OracleRacDetail returned by\n `GET /oracle/rac/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleRacDetail returned by\n `PATCH /oracle/rac/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleRacSummary returned by\n `GET /oracle/rac`.\n * Added new Field `configuredSlaDomainType` to SlaConflictsSummary returned by\n `POST /hierarchy/bulk_sla_conflicts`.\n * Added new Field `configuredSlaDomainType` to Snappable returned by\n `POST /polaris/failover/recovery_spec/upsert`.\n * Added new Field `configuredSlaDomainType` to SnappableRecoverySpec returned\n by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new Field `configuredSlaDomainType` to SnappableRecoverySpecDetails\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new Field `configuredSlaDomainType` to\n StorageArrayHierarchyObjectSummary returned by\n `GET /organization/{id}/storage/array`.\n * Added new Field `configuredSlaDomainType` to\n StorageArrayHierarchyObjectSummary returned by\n `GET /storage/array/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to\n StorageArrayHierarchyObjectSummary returned by\n `GET /storage/array/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to\n StorageArrayHierarchyObjectSummary returned by\n `GET /storage/array/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to StorageArrayVolumeGroupDetail\n returned by `POST /storage/array_volume_group`.\n * Added new Field `configuredSlaDomainType` to StorageArrayVolumeGroupDetail\n returned by `GET /storage/array_volume_group/{id}`.\n * Added new Field `configuredSlaDomainType` to StorageArrayVolumeGroupDetail\n returned by `PATCH /storage/array_volume_group/{id}`.\n * Added new Field `configuredSlaDomainType` to StorageArrayVolumeGroupSummary\n returned by `GET /storage/array_volume_group`.\n * Added new Field `configuredSlaDomainType` to\n TriggerFailoverOnTargetDefinition returned by\n `PUT /polaris/failover/target/{id}/start`.\n * Added new Field `configuredSlaDomainType` to\n TriggerFailoverOnTargetDefinition returned by\n `PUT /polaris/failover/target/{id}/resume`.\n * Added new Field `configuredSlaDomainType` to\n UnmanagedObjectSummary returned by `GET /unmanaged_object`.\n * Added new Field `configuredSlaDomainType` to\n UpsertSnappableRecoverySpecResponse returned by\n `POST /polaris/failover/recovery_spec/upsert`.\n * Added new Field `configuredSlaDomainType` to VcdClusterDetail returned by\n `GET /vcd/cluster/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdClusterDetail returned by\n `PATCH /vcd/cluster/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdClusterSummary returned by\n `GET /vcd/cluster`.\n * Added new Field `configuredSlaDomainType` to VcdHierarchyObjectSummary\n returned by `GET /organization/{id}/vcd`.\n * Added new Field `configuredSlaDomainType` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to VcdVappDetail returned by\n `GET /vcd/vapp/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdVappDetail returned by\n `PATCH /vcd/vapp/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdVappSnapshotDetail returned\n by `GET /vcd/vapp/snapshot/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdVappSummary returned by\n `GET /vcd/vapp`.\n * Added new Field `configuredSlaDomainType` to VmwareVmMountSummary returned\n by `GET /vmware/vm/snapshot/mount`.\n * Added new Field `configuredSlaDomainType` to VolumeGroupDetail returned by\n `GET /volume_group/{id}`.\n * Added new Field `configuredSlaDomainType` to VolumeGroupDetail returned by\n `PATCH /volume_group/{id}`.\n * Added new Field `configuredSlaDomainType` to VolumeGroupSummary returned by\n `GET /volume_group`.\n * Added new Field `configuredSlaDomainType` to VsphereCategory returned by\n `GET /vmware/vcenter/{id}/tag_category`.\n * Added new Field `configuredSlaDomainType` to VsphereCategory returned by\n `GET /vmware/vcenter/tag_category/{tag_category_id}`.\n * Added new Field `configuredSlaDomainType` to VsphereTag returned by\n `GET /vmware/vcenter/{id}/tag`.\n * Added new Field `configuredSlaDomainType` to VsphereTag returned by\n `GET /vmware/vcenter/tag/{tag_id}`.\n * Added a new optional query parameter `name` to\n `GET /user/{id}/organization`.\n * Added new field `hostLogRetentionHours` to OracleDbSummary returned by\n `GET /oracle/db`.\n * Added new field `isCustomRetentionApplied` to AppBlueprintSnapshotSummary\n returned by `GET /app_blueprint/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to AppBlueprintSnapshotDetail\n returned by `GET /app_blueprint/snapshot/{id}` .\n * Added new field `isCustomRetentionApplied` to AwsEc2InstanceSummary returned\n by `GET /aws/ec2_instance`.\n * Added new field `isCustomRetentionApplied` to AwsEc2InstanceDetail returned\n by `GET /aws/ec2_instance/{id}`.\n * Added new field `isCustomRetentionApplied` to AwsEc2InstanceDetail returned\n by `PATCH /aws/ec2_instance/{id}`.\n * Added new field `isCustomRetentionApplied` to\n HypervVirtualMachineSnapshotSummary returned by\n `GET /hyperv/vm/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to\n HypervVirtualMachineSnapshotDetail returned by\n `GET /hyperv/vm/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to ManagedVolumeSnapshotSummary\n returned by `GET /managed_volume/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to ManagedVolumeSnapshotSummary\n returned by `POST /managed_volume/{id}/end_snapshot`.\n * Added new field `isCustomRetentionApplied` to ManagedVolumeSnapshotDetail\n returned by `GET /managed_volume/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to NutanixVmSnapshotSummary\n returned by `GET /nutanix/vm/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to NutanixVmSnapshotDetail\n returned by `GET /nutanix/vm/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to OracleDbSnapshotSummary\n returned by `GET /oracle/db/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to OracleDbSnapshotDetail returned\n by `GET /oracle/db/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to\n StorageArrayVolumeGroupSnapshotSummary returned by\n `GET /storage/array_volume_group/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to\n StorageArrayVolumeGroupSnapshotDetail returned by\n `GET /storage/array_volume_group/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to VcdVappSnapshotSummary returned\n by `GET /vcd/vapp/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to VcdVappSnapshotDetail returned\n by `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to VolumeGroupSnapshotSummary\n returned by `GET /volume_group/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to VolumeGroupSnapshotDetail\n returned by `GET /volume_group/snapshot/{id}`.\n * Added optional field `isQueuedSnapshot` to the response of\n GET `/managed_volume/{id}/snapshot`, GET `/managed_volume/snapshot/{id}`.\n and POST `/managed_volume/{id}/end_snapshot`.\n The field specifies if ManagedVolume snapshots are in queue to be stored\n as patch file.\n * Added new field `securityLevel` to `SnmpTrapReceiverConfig` object as\n optional input parameter for SNMPv3, which is used in\n `PATCH /cluster/{id}/snmp_configuration` and\n `GET /cluster/{id}/snmp_configuration`.\n * Added new field `advancedRecoveryConfigBase64` to `ExportOracleDbConfig`.\n and `MountOracleDbConfig` objects as optional input parameter\n during Oracle recovery.\n * Added new optional field `isRemote` to UnmanagedObjectSummary object, which\n is returned from a `GET /unmanaged_object` call.\n * Added new field `hostLogRetentionHours` to OracleRacDetail returned by\n `GET /oracle/rac/{id}` and `PATCH /oracle/rac/{id}`.\n * Added new field `hostLogRetentionHours` to OracleHostDetail returned by\n `GET /oracle/host/{id}` and `PATCH /oracle/host/{id}`.\n * Added new field `hostLogRetentionHours` to OracleDbDetail returned by\n `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AppBlueprintSnapshotSummary returned\n by `GET /app_blueprint/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AppBlueprintSnapshotDetail returned by\n `GET /app_blueprint/snapshot/{id}` .\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AwsEc2InstanceSummary returned by\n `GET /aws/ec2_instance`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AwsEc2InstanceDetail returned by\n `GET /aws/ec2_instance/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AwsEc2InstanceDetail returned by\n `PATCH /aws/ec2_instance/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of HypervVirtualMachineSnapshotSummary\n returned by `GET /hyperv/vm/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of HypervVirtualMachineSnapshotDetail\n returned by `GET /hyperv/vm/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of ManagedVolumeSnapshotSummary returned\n by `GET /managed_volume/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of ManagedVolumeSnapshotSummary returned by\n `POST /managed_volume/{id}/end_snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of ManagedVolumeSnapshotDetail returned\n by `GET /managed_volume/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of NutanixVmSnapshotSummary returned by\n `GET /nutanix/vm/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of NutanixVmSnapshotDetail returned by\n `GET /nutanix/vm/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of OracleDbSnapshotSummary returned by\n `GET /oracle/db/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of OracleDbSnapshotDetail returned by\n `GET /oracle/db/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of StorageArrayVolumeGroupSnapshotSummary\n returned by `GET /storage/array_volume_group/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of StorageArrayVolumeGroupSnapshotDetail\n returned by `GET /storage/array_volume_group/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of VcdVappSnapshotSummary returned by\n `GET /vcd/vapp/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of VcdVappSnapshotDetail returned by\n `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of VolumeGroupSnapshotSummary returned by\n `GET /volume_group/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of VolumeGroupSnapshotDetail returned by\n `GET /volume_group/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to AppBlueprintSnapshotSummary\n returned by `GET /app_blueprint/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to AppBlueprintSnapshotDetail\n returned by `GET /app_blueprint/snapshot/{id}` .\n * Added new field `SnapshotRetentionInfo` to AwsEc2InstanceSummary returned\n by `GET /aws/ec2_instance`.\n * Added new field `SnapshotRetentionInfo` to AwsEc2InstanceDetail returned\n by `GET /aws/ec2_instance/{id}`.\n * Added new field `SnapshotRetentionInfo` to AwsEc2InstanceDetail returned\n by `PATCH /aws/ec2_instance/{id}`.\n * Added new field `SnapshotRetentionInfo` to\n HypervVirtualMachineSnapshotSummary returned by\n `GET /hyperv/vm/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to\n HypervVirtualMachineSnapshotDetail returned by\n `GET /hyperv/vm/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to ManagedVolumeSnapshotSummary\n returned by `GET /managed_volume/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to ManagedVolumeSnapshotSummary\n returned by `POST /managed_volume/{id}/end_snapshot`.\n * Added new field `SnapshotRetentionInfo` to ManagedVolumeSnapshotDetail\n returned by `GET /managed_volume/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to NutanixVmSnapshotSummary\n returned by `GET /nutanix/vm/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to NutanixVmSnapshotDetail\n returned by `GET /nutanix/vm/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to OracleDbSnapshotSummary\n returned by `GET /oracle/db/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to OracleDbSnapshotDetail returned\n by `GET /oracle/db/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to\n StorageArrayVolumeGroupSnapshotSummary returned by\n `GET /storage/array_volume_group/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to\n StorageArrayVolumeGroupSnapshotDetail returned by\n `GET /storage/array_volume_group/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to VcdVappSnapshotSummary returned\n by `GET /vcd/vapp/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to VcdVappSnapshotDetail returned\n by `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to VolumeGroupSnapshotSummary\n returned by `GET /volume_group/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to VolumeGroupSnapshotDetail\n returned by `GET /volume_group/snapshot/{id}`.\n * Added optional field `networkInterface` to `NetworkThrottleUpdate`. The\n field allows users to specify non standard network interfaces. This applies\n to the `PATCH /network_throttle/{id}` endpoint.\n * Added mandatory field `networkInterface` to `NetworkThrottleSummary`.\n This applies to the endpoints `GET /network_throttle` and\n `GET /network_throttle/{id}`.\n * Added endpoint `POST /cluster/{id}/manual_discover`, which allows\n the customer to manually input data that would be learned using\n mDNS discovery. Returns same output as discover.\n * `PATCH /cluster/{id}/snmp_configuration` will now use\n `SnmpConfigurationPatch` as a parameter.\n * Added optional field `user` to `SnmpTrapReceiverConfig`. The field\n specifies which user to use for SNMPv3 traps.\n * Added optional field `users` to `SnmpConfiguration`. The field contains\n usernames of users configured for SNMPv3.\n * Added two new models `SnmpUserConfig` to store user credentials and\n `SnmpConfigurationPatch`.\n * Added new endpoint `POST /role/authorization_query` to get authorizations\n granted to roles.\n * Added new endpoint `GET /role/{id}/authorization` to get authorizations\n granted to a role.\n * Added new endpoint `POST /role/{id}/authorization` to grant authorizations\n to a role.\n * Added new endpoint `POST /role/{id}/authorization/bulk_revoke` to revoke\n authorizations from a role.\n * Added optional field `recoveryInfo` to UnmanagedObjectSummary.\n * Added optional field `isRetentionLocked` to SlaInfo.\n The parameter indicates that the SLA Domain associated with the job is a\n Retention Lock SLA Domain.\n * Added optional field `legalHoldDownloadConfig` to\n `FilesetDownloadFilesJobConfig`,`HypervDownloadFileJobConfig`,\n `DownloadFilesJobConfig`,`ManagedVolumeDownloadFileJobConfig`,\n `NutanixDownloadFilesJobConfig`,`StorageArrayDownloadFilesJobConfig`,\n `VolumeGroupDownloadFilesJobConfig`.This is an optional argument\n containing a Boolean parameter to depict if the download is being\n triggered for Legal Hold use case. This change applies to\n /fileset/snapshot/{id}/download_files,\n /hyperv/vm/snapshot/{id}/download_file,\n /vmware/vm/snapshot/{id}/download_files,\n /managed_volume/snapshot/{id}/download_file,\n /nutanix/vm/snapshot/{id}/download_files,\n /storage/array_volume_group/snapshot/{id}/download_files and\n /volume_group/snapshot/{id}/download_files endpoints.\n * Added optional field isPlacedOnLegalHold to BaseSnapshotSummary.\n The Boolean parameter specifies whether the snapshot is placed under a\n Legal Hold.\n * Added new endpoint `GET /ods_configuration`.\n Returns the current configuration of on-demand snapshot handling.\n * Added new endpoint `PUT /ods_configuration`.\n Update the configuration of on-demand snapshot handling.\n * Added two new models `OdsConfigurationSummary`, `OdsPolicyOnPause` and a new\n enum `SchedulingType`.\n * Added `odsPolicyOnPause` field in `OdsConfigurationSummary` to include the\n policy followed by the on-demand snapshots, during an effective pause.\n * Added new enum field `schedulingType` in `OdsPolicyOnPause` to support\n deferring the on-demand snapshots during an effective pause.\n * Added optional query parameter `show_snapshots_legal_hold_status` to\n `GET /archive/location` endpoint, indicating if `isLegalHoldSnapshotPresent`.\n field should be populated in response.\n * Added storage array volume group asynchronous request status endpoint\n `GET /storage/array_volume_group/request/{id}`. Request statuses for\n storage array volume groups which previously used\n `/storage/array/request/{id}` must now use this new endpoint.\n * Added forceFull parameter to the properties of patch volume group object\n to permit forcing a full snapshot for a specified volume group.\n * Added `isDcaAccountInstance` field to `AwsEc2InstanceSummary` to indicate\n whether the EC2 instance belongs to a DCA account. This impacts the endpoints\n `GET /aws/ec2_instance` and `GET /aws/ec2_instance/{id}`.\n * Added `encryptionKeyId` as an optional field in CreateCloudInstanceRequest\n definition used in the on-demand API conversion API `/cloud_on/aws/instance`.\n to support KMS encryption for CloudOn conversion in AWS.\n * Added new endpoint `GET /job/{id}/child_job_instance`.\n Returns the child job instances (if any) spawned by the given parent job\n instance. This endpoint requires a support token to access.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include the `isConsolidationEnabled` field, to indicate\n if consolidation is enabled for the given archival location.\n * Changed `encryptionPassword` parameter to optional in\n `NfsLocationCreationDefinition` to support creating NFS archival location\n without encryption via `POST /archive/nfs`.\n * Added an optional parameter `disabledEncryption` to\n `NfsLocationCreationDefinition` with a default value of false, to enable or\n disable encryption via `POST /archive/nfs`.\n * Added a new model `ValidationResponse` and REST API endpoints\n `/cloud_on/validate/instantiate_on_cloud` and\n `/cloud_on/validate/cloud_image_conversion` for validation of cloud\n conversion.\n * Added `sortBy` and `sortOrder` parameters to `GET /hyperv/vm/snapshot/mount`.\n to allow sorting of Hyper-V mounts.\n Added the enum `HypervVirtualMachineMountListSortAttribute`, defining which\n properties of Hyper-V mounts are sortable.\n * Added an optional field `shouldApplyToExistingSnapshots` in\n `SlaDomainAssignmentInfo` to apply the new SLA configuration to existing\n snapshots of protected objects.\n * Added a new optional field `isOracleHost` to `HostRegister` in\n `POST /host/bulk` and `HostUpdate` in `PATCH /host/bulk` to indicate if we\n should discover Oracle information during registration and host refresh.\n * Added a new model `NutanixVirtualDiskSummary` that is returned by\n `GET /nutanix/vm/{id}` to include the disks information for a Nutanix\n virtual machine.\n * Added mandatory field `pendingSnapshot` to `SystemStorageStats`, which is\n returned by `GET /stats/system_storage`.\n * Added optional isIsilonChangelistEnabled in the NasBaseConfig and NasConfig.\n NasBaseConfig is returned as part of HostSummary, which is returned by the\n `Get /host/envoy` and `Get /host` endpoints. NasConfig is used by\n HostRegister and HostUpdate. The HostRegister is used by the\n `Post /host/bulk` endpoint and the HostUpdate is used by the\n `PATCH /host/bulk` endpoint.\n * Added a new model `HostShareParameters`. This model has two fields,\n isNetAppSnapDiffEnabled and isIsilonChangelistEnabled. The\n isNetAppSnapDiffEnabled is a Boolean value that specifies whether the\n SnapDiff feature of NetApp NAS is used to back up the NAS share. The\n isIsilonChangelistEnabled is a Boolean value that specifies whether\n the Changelist feature of Isilon NAS is used to back up the NAS share.\n * Added optional field `HostShareParameters` in `HostFilesetShareSummary`,\n `HostFilesetShareDetail` and `HostShareDetail`. The HostShareDetail impacts\n the endpoints `Get /host/share` and `Post /host/share`. The\n `HostFilesetShareDetail` impacts the endpoint `Get /host_fileset/share/{id}`.\n . The HostFilesetShareSummary impacts the endpoint\n `Get /host_fileset/share`.\n * Added `isInVmc` in `GET /vcd/vapp/{id}`, and `PATCH /vcd/vapp/{id}`.\n to return whether the virtual machine is in a VMC setup.\n * Added new endpoint `GET /vmware/hierarchy/{id}/export`. Returns the\n VmwareHierarchyInfo object with the given ID.\n * Added optional field `platformDetails` to `PlatformInfo`, which is returned\n by `GET /cluster/{id}/platforminfo`.\n * Added optional field `cpuCount` to `PlatformInfo`, which is returned by\n `GET /cluster/{id}/platforminfo`.\n * Added optional field `ramSize` to `PlatformInfo`, which is returned by\n `GET /cluster/{id}/platforminfo`.\n * Added new value `RangeInTime` to `RecoveryPointType` enum, which is used in\n the `ReportTableRequest` object for the POST `/report/{id}/table` and POST\n `/report/data_source/table` endpoints.\n * Added the optional field `shouldForceFull` to `MssqlDbUpdate` object,\n which is referred by `MssqlDbUpdateId`, which is referred as the\n body parameter of `PATCH /mssql/db/bulk`.\n\n ### Changes to Internal API in Rubrik version 5.1.1\n ## Breaking changes:\n * Changed response code of a successful\n `POST /managed_volume/{id}/begin_snapshot` API from 201 to 200.\n\n ### Changes to Internal API in Rubrik version 5.1.0\n ## Breaking changes:\n * Changed response type of percentInCompliance and percentOutOfCompliance\n in ComplianceSummary to double.\n * Renamed new enum field `MissedSnapshots` to `MissedLocalSnapshots`.\n and `LastSnapshot` to `LatestLocalSnapshot`, in the\n following properties:\n measure property in ChartSummary, column property in TableSummary,\n and sortBy property in ReportTableRequest.\n * Renamed effectiveThroughput to throughput in EventSeriesMonitoredJobSummary.\n * Renamed realThroughput to throughput in EventSeriesSummary.\n * Updated response of GET /event_series/{id} to remove effectiveThroughput.\n * Renamed paths `/storage/array/volume/group` to `/storage/array_volume_group`.\n * Renamed the field cassandraSetup in ReplaceNodeStatus to metadataSetup\n * Renamed the field cassandraSetup in RecommisionNodeStatus to metadataSetup\n * Renamed the field cassandraSetup in AddNodesStatus to metadataSetup\n * Renamed the field cassandraSetup in ClusterConfigStatus to metadataSetup\n * Renamed the field removeCassandra in RemoveNodeStatus to removeMetadatastore\n for the GET /cluster/{id}/remove_node endpoint.\n * Moved the `GET /blackout_window` endpoint from internal to V1.\n * Moved the `PATCH /blackout_window` endpoint from internal to V1.\n * Removed endpoint POST /report/global_object endpoint.\n /report/data_source/table can be used to get the same information.\n * Made accessKey optional in ObjectStoreLocationDetail as accessKey is not\n defined in Cross Account Role Based locations. Also made accessKey required\n again in ObjectStoreLocationDefinition.\n * Removed `progressPercentage` from `EventSeriesMonitoredJobSummary` object.\n * Removed endpoint `POST cluster-id-security-password-strength` since it is\n no longer used at bootstrap.\n * Moved the GET `/mssql/hierarchy/{id}/descendants` and\n GET `/mssql/hierarchy/{id}/children` endpoints from internal to v1.\n\n ## Feature Additions/improvements:\n * GET POST /cluster/{id}/node now accepts an optional encryption\n password in the encryptionPassword field.\n * GET /node_management/replace_node now accepts an optional encryption\n password in the encryptionPassword field.\n * Added optional field `shouldSkipScheduleRecoverArchivedMetadataJob` to\n the body parameter of `POST /archive/object_store/reader/connect`, to\n determine whether to schedule the archival recovery job.\n When the value is 'false,' the recovery job is scheduled normally.\n When the value is 'true,' the recovery job is not scheduled.\n The default behavior is to schedule the recovery job.\n * Added mandatory field `cdp` to SystemStorageStats.\n * Added optional field `agentStatus` to NutanixHierarchyObjectSummary.\n The field indicates whether a Rubrik backup agent is registered to the\n Nutanix object.\n * Added optional field `shouldUseAgent` to `RestoreFilesJobConfig`.\n in `POST /vmware/vm/snapshot/{id}/restore_files` to specify\n whether to use Rubrik Backup Service to restore files. Default value is true.\n * GET /managed_object/bulk/summary and GET\n /managed_object/{managed_id}/summary no longer include archived objects\n with no unexpired snapshots in their results.\n * Added new required Boolean field `isDbLocalToTheCluster` to\n `OracleDbSummary` and `OracleDbDetail`.\n * Added optional field `awsAccountId` to ObjectStoreLocationSummary.\n * Added optional field `shouldRecoverSnappableMetadataOnly` to all the\n reader location connect definitions.\n * Added new enum value `ArchivalComplianceStatus` to the following properties:\n attribute property in ChartSummary and column property in TableSummary\n * Added new enum fields `ArchivalInComplianceCount`,\n `ArchivalNonComplianceCount` and `MissedArchivalSnapshots` to the\n following properties:\n measure property in ChartSummary, column property in TableSummary,\n and sortBy property in ReportTableRequest.\n * GET /managed_object/bulk/summary and GET\n /managed_object/{managed_id}/summary will always include the correct relic\n status for hosts and their descendants.\n * Added field `isLocked` to PrincipalSummary.\n * Added optional query parameter `snappableStatus` to /vmware/data_center and\n /vmware/host. This parameter enables a user to fetch the set of protectable\n objects from the list of objects visible to that user.\n * Added optional field `archivalComplianceStatus` to RequestFilters\n * Added optional field `archivalComplianceStatus` to FilterSummary\n * Added optional field `alias` to HostSummary, HostRegister, and HostUpdate\n schemas. This field will allow the user to specify an alias for each host\n which can be used for search.\n * Added optional field `subnet` to ManagedVolumeExportConfig\n * Added optional field `status` to oracle/hierarchy/{id}/children\n * Added optional field `status` to oracle/hierarchy/{id}/descendants\n * Added optional field `status` to hyperv/hierarchy/{id}/children\n * Added optional field `status` to hyperv/hierarchy/{id}/descendants\n * Added optional field `numNoSla` to ProtectedObjectsCount\n * Added optional field `numDoNotProtect` to ProtectedObjectsCount\n * Added optional field `limit`, `offset`, `sort_by`, `sort_order` to\n /node/stats\n * Added optional field encryptionAtRestPassword to configure password-based\n encryption for an edge instance.\n * Added new endpoint GET /report/data_source/{data_source_name}/csv.\n * Added new endpoint POST /report-24_hour_complianace_summary.\n * Added new endpoint POST /report/data-source/{data_source_name} to get\n columns directly from report data source.\n * Added optional field compliance24HourStatus to RequestFilters object.\n * Added the `port` optional field to QstarLocationDefinition. The `port` field\n enables a user to specify the server port when adding a new location or\n editing an existing location.\n * Added optional field archivalTieringSpec to ArchivalSpec and ArchivalSpecV2\n to support archival tiering. This enables the user to configure either\n Instant Tiering or Smart Tiering (with a corresponding minimum accessible\n duration) on an SLA domain with archival configured to an Azure archival\n location.\n * Updated endpoints /vcd/vapp, /oracle/db and /aws/ec2_instance\n to have a new optional query paramter, indicating if backup task information\n should be included.\n * Added optional field logConfig to SlaDomainSummaryV2, SlaDomainDefinitionV2\n and SlaDomainPatchDefintionV2 to support CDP (ctrlc). The parameters\n distinguish SLAs with CDP enabled from SLAs with CDP disabled, and enable\n users to specify log retention time. The field also provides an optional\n frequency parameter whhich can be used by Oracle and SQL Server log backups.\n * Added optional field logRetentionLimit to ReplicationSpec to support\n CDP replication. The field gives the retention limit for logs at the\n specified location.\n * Moved the `GET /vmware/compute_cluster` endpoint from internal to V1.\n * Moved the `GET /vmware/compute_cluster/{id}` endpoint from internal to V1.\n * Changed the existing `PATCH mssql/db/bulk` endpoint to return an\n unprotectable reason as a string in the `unprotectableReason` field instead\n of a JSON struct.\n * Added optional field `kmsMasterKeyId` and changed the existing field\n `pemFileContent` to optional field in `DcaLocationDefinition`.\n * Added new optional field `enableHardlinkSupport` to FilesetSummary and\n FilesetCreate in `POST /fileset`, \"GET /fileset\" and \"PATCH /fileset/{id}\"\n endpoints to enable recognition and deduplication of hardlinks in\n fileset backup.\n * Added optional query parameter to `GET /archive/location` endpoint,\n indicating if `isRetentionLockedSnapshotProtectedPresent` field should\n be populated in response.\n * Added continuous data protection state for each VMware virtual machine\n * Added new endpoint `PUT /polaris/archive/proxy_setting`.\n * Added new endpoint `GET /polaris/archive/proxy_setting/{id}`.\n * Added new endpoint `DELETE /polaris/archive/proxy_setting/{id}`.\n * Added new endpoint `PUT /polaris/archive/aws_compute_setting`.\n * Added new endpoint `GET /polaris/archive/aws_compute_setting/{id}`.\n * Added new endpoint `DELETE /polaris/archive/aws_compute_setting/{id}`.\n * Added new endpoint `PUT /polaris/archive/azure_compute_setting`.\n * Added new endpoint `GET /polaris/archive/azure_compute_setting/{id}`.\n * Added new endpoint `DELETE /polaris/archive/azure_compute_setting/{id}`.\n * Added new endpoint `PUT /polaris/archive/aws_iam_location`.\n * Added new endpoint `GET /polaris/archive/aws_iam_location/{id}`.\n * Added new endpoint `PUT /polaris/archive/azure_oauth_location`.\n * Added new endpoint `GET /polaris/archive/azure_oauth_location/{id}`.\n * Added new endpoint `PUT /polaris/archive/aws_iam_customer_account`.\n * Added new endpoint `GET /polaris/archive/aws_iam_customer/{id}`.\n * Added new endpoint `DELETE /polaris/archive/aws_iam_customer/{id}`.\n * Added new endpoint `PUT /polaris/archive/azure_oauth_customer`.\n * Added new endpoint `GET /polaris/archive/azure_oauth_customer/{id}`.\n * Added new endpoint `DELETE /polaris/archive/azure_oauth_customer/{id}`.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include `currentState` field, to indicate whether the archival\n location is connected or temporarily disconnected.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include `isComputeEnabled` field, to indicate whether the\n archival location has cloud compute enabled.\n * Added optional field `cloudStorageTier` to `BaseSnapshotSummary`, to indicate\n the current storage tier of the archived copy of a snapshot.\n * Added endpoint `PUT /polaris/archive/aws_iam_location/reader_connect`.\n to connect as a reader to an IAM based AWS archival location.\n * Added endpoint `PUT /polaris/archive/azure_oauth_location/reader_connect`.\n to connect as a reader to an OAuth based Azure archival location.\n * Added endpoint `POST polaris/archive/location/{id}/reader/promote`.\n to promote the current cluster to be the owner of a specified IAM based AWS\n archival location that is currently connected as a reader location.\n * Added endpoint `POST polaris/archive/location/{id}/reader/refresh`.\n to sync the current reader cluster with the contents on the IAM based AWS\n archival location.\n * Added effectiveSlaDomainName and effectiveSlaDomainSourceId fields\n to `GET /vmware/vcenter/{id}/tag_category` response object.\n * Added effectiveSlaDomainName and effectiveSlaDomainSourceId fields\n to `GET /vmware/vcenter/{id}/tag` response object.\n * Added continuous data protection status for reporting.\n * Added optional field `localCdpStatus` to the following components:\n ChartSummary, TableSummary, ReportTableRequest, RequestFilters and\n FilterSummary.\n * Added `ReportSnapshotIndexState` and `ReportObjectIndexType` to\n `/internal_report_models/internal/definitions/enums/internal_report.yml`.\n * Added optional field `latestSnapshotIndexState` and `objectIndexType` to\n the following components:\n TableSummary, ReportTableRequest, RequestFilters and FilterSummary.\n * Added 24 hour continuous data protection healthy percentage for reporting.\n * Added optional field `PercentLocal24HourCdpHealthy` to the following\n components: TableSummary, ReportTableRequest.\n * Added optional field `replicas` to MssqlHierarchyObjectSummary.\n * Added optional field `hosts` to MssqlHierarchyObjectSummary.\n * Added continuous data protection local log storage size and local throughput\n consumption for reporting.\n * Added optional fields `localCdpThroughput` and `localCdpLogStorage` to the\n following components: ChartSummary, TableSummary and ReportTableRequest.\n * Added optional field requestExclusionFilters to ReportTableRequest.\n * Added an optional field to ManagedVolumeSummary to retrieve the associated\n subnet.\n * Added optional field isEffectiveSlaDomainRetentionLocked to Snappable.\n The parameter depicts if the effective SLA domain for the snappable is\n a Retention Lock SLA Domain.\n * Updated the set of possible continuous data protection statuses for each\n VmwareVirtualMachine.\n * Added the optional field isEffectiveSlaDomainRetentionLocked to\n FilesetSummary. The field is a Boolean that specifies whether the effective\n SLA Domain of a fileset is retention locked.\n * Added optional field iConfiguredSlaDomainRetentionLocked to SlaAssignable.\n The parameter depicts if the configured SLA domain for the object is a\n Retention Lock SLA Domain.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include the `isTieringSupported` field, to indicate\n whether a given archival location supports tiering.\n * Added continuous data protection replication status for reporting.\n * Added CdpReplicationStatus as an optional field to the TableSummary and\n ReportTableRequest components.\n * Added optional CdpReplicationStatus field to RequestFilters and\n FiltersSummary.\n * Added optional field isEffectiveSlaDomainRetentionLocked to\n SearchItemSummary. The Boolean parameter specifies whether the effective\n SLA Domain for the search item is a Retention Lock SLA Domain.\n * Updated `OracleMountSummary` returned by GET /oracle/db/mount\n endpoint to include the isInstantRecovered field, to indicate\n whether the mount was created during an Instant Recovery or Live Mount.\n * Added optional field isEffectiveSlaDomainRetentionLocked to\n ManagedObjectSummary. The Boolean parameter specifies whether the effective\n SLA Domain for the search item is a Retention Lock SLA Domain.\n * Added optional field `isRetentionSlaDomainRetentionLocked` to\n UnmanagedSnapshotSummary. The parameter indicates that the retention SLA\n Domain associated with the snapshot is a Retention Lock SLA Domain.\n * Added optional field `isSlaRetentionLocked` to EventSeriesSummary.\n The parameter indicates that the SLA Domain associated with the event\n series is a Retention Lock SLA Domain.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include the `isConsolidationEnabled` field, to indicate\n if consolidation is enabled for the given archival location.\n * Added the `hasUnavailableDisks` field to `NodeStatus` to indicate whether a\n node has unavailable (failed or missing) disks. This change affects the\n endpoints `GET /cluster/{id}/node`, `GET /node`, `GET /node/{id}`, `GET\n /node/stats`, and `GET /node/{id}/stats`.\n * Added optional NAS vendor type to the HostShareDetail\n This change affectes the endpoints `Get /host/share`, `Post /host/share` and\n `Get /host/share/{id}`.\n * Added optional isSnapdiffEnabled in the NasBaseConfig and NasConfig\n NasBaseConfig is returned as part of HostSummary, which is returned by the\n `Get /host/envoy` and `Get /host` endpoints. NasConfig is used by\n HostRegister and HostUpdate. The HostRegister field is used by the\n `Post /host/bulk` endpoint and the HostUpdate is field used by the\n `PATCH /host/bulk` endpoint.\n * Added optional snapdiffUsed in the FilesetSnapshotSummary\n The FilesetSnapshotSummary is used by FilesetDetail and\n FilesetSnapshotDetail. This change affects the endpoints `Post\n /fileset/bulk`, `Get /host_fileset/share/{id}` and\n `Get /fileset/snapshot/{id}`.\n\n ### Changes to Internal API in Rubrik version 5.0.4\n ## Feature Additions/improvements:\n * Added objectState to FilterSummary which is part of body parameter of\n PATCH/report/{id}\n * Added objectState to RequestFilters which is part of body parameter of\n POST /report/data_source/table\n\n ### Changes to Internal API in Rubrik version 5.0.3\n ## Breaking changes:\n * Removed fields 'virtualMedia' and 'ssh' from IpmiAccess and\n IpmiAccessUpdate.\n\n ## Feature Additions/improvements:\n * Added a new optional field 'oracleQueryUser' to HostRegister, HostUpdate\n and HostDetail objects, for setting the Oracle username for account with\n query privileges on the host. This applies to the following endpoints:\n `POST /host/bulk`, `PATCH /host/{id}`, and `GET /host/{id}`.\n * Added a field `affectedNodeIds` to the `SystemStatus` object. This object is\n returned by `GET /cluster/{id}/system_status`.\n * Made `nodeId` a required field of the `DiskStatus` object. This object, or\n an object containing this object, is returned by the following endpoints:\n `GET /cluster/{id}/disk`, `PATCH /cluster/{id}/disk/{disk_id}`, and\n `GET /node/{id}`.\n\n ### Changes to Internal API in Rubrik version 5.0.2\n ## Feature Additions/improvements:\n * Added an optional fields `subnet` to `ManagedVolumeSummary` to retrieve the associated\n subnet.\n * Added `tablespaces` field in `OracleDbSnapshotSummary` to include the list\n of tablespaces in the Oracle database snapshot.\n * Added new endpoint `POST /hierarchy/bulk_sla_conflicts`.\n * Added optional field `limit`, `offset`, `sort_by`, `sort_order` to\n `GET /node/stats`.\n * Added optional field `numNoSla` to `ProtectedObjectsCount`.\n * Added optional field `numDoNotProtect` to `ProtectedObjectsCount`.\n * Introduced optional field `logicalSize` to `VirtualMachineDetail`. This\n field gives the sum of logical sizes of all the disks in the virtual\n machine.\n * Added optional fields `nodeIds`, `slaId`, `numberOfRetries`, and\n `isFirstFullSnapshot` to the response of `GET /event_series/{id}`.\n * Added `SapHanaLog` tag in `applicationTag` field of `ManagedVolumeConfig`.\n for SAP HANA log managed volumes.\n * Added required field `dbSnapshotSummaries` in `OracleRecoverableRange` to include\n the list of database snapshots in each Oracle recoverable range.\n * Added field `isOnline` to MssqlDbSummary and changed `hasPermissions` to\n required field.\n * Added `DbTransactionLog` tag, in applicationTag field of\n `ManagedVolumeConfig`, for generic log managed volumes. ApplicationTag has\n to be specified in the request field of POST /managed_volume.\n\n ### Changes to Internal API in Rubrik version 5.0.1\n ## Breaking changes:\n * Removed `GET/POST /smb/enable_security` endpoints.\n * Changed the `objectId` type in `EventSeriesMonitoredJobSummary` and\n `EventSeriesSummary` to a user-visible ID instead of a simple ID.\n * Updated endpoint `POST /smb/domain` to accept a list of domain controllers.\n * Removed endpoint `POST /report/global_object`.\n * Added optional field `kmsMasterKeyId` and changed the existing field\n `pemFileContent` to optional field in `DcaLocationDefinition`.\n * Removed `progressPercentage` from `EventSeriesMonitoredJobSummary` object.\n\n ## Feature Additions/improvements:\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include `currentState` field, to indicate whether the archival\n location is connected or temporarily disconnected.\n * Added optional field `subnet` to ManagedVolumeExportConfig.\n * Added the`PUT /smb/config` endpoint to manage SMB configuration.\n * Added the following two endpoints.\n - `GET /stats/per_vm_storage`.\n - `GET /stats/per_vm_storage/{vm_id}`.\n * Added optional field `isStickySmbService` to the response of\n `GET /smb/domain` and `POST /smb/domain`.\n * Added new endpoint `GET /report/data_source/{data_source_name}/csv`.\n * Added new endpoint `POST /report_24_hour_complianace_summary`.\n * Added new endpoint `POST /report/data-source/{data_source_name}` to get\n columns directly from report data source.\n * Added new report API endpoints:\n - `GET /report/summary/physical_storage_time_series`.\n - `GET /report/summary/average_local_growth_per_day`.\n * Added `GET /node/stats` which returns stats for all nodes.\n * Added `GET /cluster/{id}/security/password/zxcvbn` to return\n the enabled or disabled status of ZXCVBN validation for new passwords.\n * Added `POST /cluster/{id}/security/password/zxcvbn` to toggle\n ZXCVBN validation for new passwords.\n\n ### Changes to Internal API in Rubrik version 5.0.0\n ## Breaking changes:\n * Removed `/user_notification` endpoints.\n * Added `rawName` field in `ArchivalLocationSummary`, which contains the\n raw name of the archival location.\n * Removed `shareType` from config field in PATCH /managedvolume request.\n * Changed `/cluster/me/ntp_server` endpoint to accept symmetric keys\n and the corresponding hashing algorithm.\n * Removed `/job/type/prune_job_instances` endpoint.\n * Removed `/kmip/configuration` endpoint.\n * Removed `/session/api_token` endpoint.\n * Added `subnet` field in `ManagedVolumeConfig`, which specifies an outgoing\n VLAN interface for a Rubrik node. This is a required value when creating a\n managed volume on a Rubrik node that has multiple VLAN interfaces.\n * Removed the `VolumeGroupVolumeSummary`, and replaced it with\n `HostVolumeSummary`.\n * Removed `volumeIdsIncludedInSnapshots` from `VolumeGroupDetail`.\n * Added new optional fields `mssqlCbtEnabled`, `mssqlCbtEffectiveStatus`,\n `mssqlCbtDriverInstalled`, `hostVfdEnabled` and `hostVfdDriverState` to\n GET /host/{id} response.\n * Responses for `/cluster/{id}/dns_nameserver` and\n `/cluster/{id}/dns_search_domain` changed to be array of strings.\n * Added new required field `language` in `UserPreferencesInfo` for\n GET /user/{id}/preferences and PATCH /user/{id}/preferences\n * Added new field `missedSnapshotTimeUnits` in `MissedSnapshot`.\n * Removed `localStorage` and `archiveStorage` from `UnmanagedSnapshotSummary`.\n * Moved the `Turn on or off a given AWS cloud instance` endpoint from PATCH of\n `/cloud_on/aws/instance` to PATCH of `/cloud_on/aws/instance/{id}/cloud_vm`.\n Also removed the `id` field from the definition of `CloudInstanceUpdate`.\n * Moved the `Turn on or off a given Azure cloud instance` endpoint from PATCH\n of `/cloud_on/azure/instance` to PATCH of\n `/cloud_on/azure/instance/{id}/cloud_vm`. Also removed the `id` field from\n the definition of `CloudInstanceUpdate`.\n * Moved the `Delete a given AWS cloud instance` endpoint from DELETE of\n `/cloud_on/aws/instance/{id}` to DELETE of\n `/cloud_on/aws/instance/{id}/cloud_vm`.\n * Moved the `Delete a given Azure cloud instance` endpoint from DELETE of\n `/cloud_on/azure/instance/{id}` to DELETE of\n `/cloud_on/azure/instance/{id}/cloud_vm`.\n * Modified the existing endpoint DELETE `/cloud_on/aws/instance/{id}` to\n remove entry of a given AWS cloud instance instead of terminating the\n instance.\n * Modified the existing endpoint DELETE `/cloud_on/azure/instance/{id}` to\n remove entry of a given Azure cloud instance instead of terminating the\n instance.\n * Removed `/job/type/job-schedule_gc_job_start_time_now` endpoint. Use\n endpoint POST `/job/type/garbageCollection` to schedule a GC job to\n run now.\n * Removed `config` parameter from `/job/type/garbageCollection`.\n * Added optional parameter `jobInstanceId` to `EventSummary`.\n * Added `jobInstanceId` as a new optional query parameter for\n GET /event_series/{id}/status endpoint.\n * Modified the endpoint GET /event_series/status to a POST and changed the\n input parameter to a request body of type `EventSeriesDetail`.\n * Modified the endpoint PATCH /replication/target/{id} to take a request body\n of type ReplicationTargetUpdate instead of ReplicationTargetDefinition.\n * Added Discovery EventType.\n * Added `name` and deprecated `hostname` in `HostSummary` and `HostDetail`.\n response.\n * Added `isDeleted` and deprecated `isArchived` in MssqlDbReplica response.\n * Removed `GET /stats/cloud_storage` endpoint.\n * Removed DELETE /oracle/db/{id} endpoint to delete an Oracle database.\n * By default, a volume group is not associated with any volumes at creation\n time. This default is a change from the 4.2 implementation, where newly\n created volume groups contain all of the host volumes. On 5.0 clusters,\n use the `GET /host/{id}/volume` endpoint to query all host volumes.\n\n ## Feature Additions/improvements:\n * Added new endpoint POST/report/data-source/{data_source_name} to get columns\n directly from report data source.\n * Added optional field compliance24HourStatus to RequestFilters object.\n * Added GET /event/event_count-by-status to get job counts based on job status.\n * Added GET /event/event_count-by-job-type to get job counts based on job type.\n * Added GET /event_series endpoint to get all event series information in the\n past 24 hours.\n * Added `oracleDatabase` to ManagedObjectDescendantCounts.\n * Introduced `POST /session/realm/{name}` endpoint to generate session\n tokens in the LDAP display name of {name}.\n * Added optional `storageClass` field to `ObjectStoreReaderConnectDefinition`.\n to store `storageClass` field for the newly connected reader location.\n * Added optional `encryptionType` field to `ObjectStoreLocationSummary` to\n return encryption type used for an object store archival location.\n * Added a new endpoint POST /oracle/db/download/{snapshot_id} to download\n a particular snapshot (and corresponding logs) for Oracle.\n * Added optional `ownerId` and `reference` fields to\n `/managed_volume/{id}/begin_snapshot`.\n * Added new endpoints regarding references to Managed Volumes, which track\n the processes writing to the Managed Volume.\n - GET `/managed_volume/{id}/snapshot/{snapshot_id}/reference/{reference_id}`.\n PUT `/managed_volume/{id}/snapshot/{snapshot_id}/reference/{reference_id}`.\n PATCH\n `/managed_volume/{id}/snapshot/{snapshot_id}/reference/{reference_id}`.\n DELETE\n `/managed_volume/{id}/snapshot/{snapshot_id}/reference/{reference_id}`.\n are the endpoints for viewing, adding, editing and deleting a Managed\n Volume snapshot reference respectively.\n * Added optional `apiToken` and `apiEndpoint` fields to NasConfig to support\n Pure FlashBlade devices.\n * Added optional `smbValidIps`, `smbDomainName` and `smbValidUsers` fields\n to `VolumeGroupMountSnapshotJobConfig` to support secure SMB.\n * Added optional `smbDomainName`, `smbValidIps`, `smbValidUsers` fields to\n ManagedVolumeExportConfig to support secure SMB.\n * Added a new optional field `oracleSysDbaUser` to /host/{id} POST endpoint\n during register host for setting the Oracle username for account with sysdba\n privileges on this host.\n * Added a new endpoint DELETE /smb/domain/{domain_name} to delete the\n SMB Domain.\n * Added a new endpoint POST /smb/domain/{domain_name}/join to configure\n SMB Domain.\n * Added a new optional filed `oracleSysDbaUser` to /host/{id} endpoint for\n changing the Oracle username for account with sysdba privileges on this\n host.\n * Added a new endpoint POST /smb/enable_security to enable Live Mount\n security\n * Made the `numChannels` field in ManagedVolumeConfig optional.\n * Added `applicationTag` field to ManagedVolumeConfig to specify workload\n type for a managed volume.\n * Added Maintenance EventType\n * Added POST `/report/global_object` endpoint to directly query table data\n from GlobalObject based on ReportTableRequest\n * Added new API endpoint GET `/diagnostic/snappable/{id}` returns\n diagnostic information of all backup tasks of a data source.\n * Added new API endpoint GET `/diagnostic/snappable/{id}/latest` returns\n diagnostic information of the most recent backup task of a data source.\n * Added `shareType` field to ManagedVolumeSummary and ManagedVolumeDetail.\n * Added oracle instant recovery API to trigger instant recovery of a\n database.\n * Added RAC, Oracle host and Oracle database fields to the the oracle\n hierarchy API\n * Added a new endpoint GET /smb/domain to get a list of discovered\n SMB domains in the environment.\n * Added a new endpoint GET /notification_setting to get all Notification\n Settings.\n * Added a new endpoint POST /notification_setting to create a new\n Notification Setting.\n * Added a new endpoint GET /notification_setting/{id} to get a Notification\n Setting specified by the input id.\n * Added a new endpoint PATCH /notification_setting/{id} to update the values\n for a specified Notification Setting.\n * Added a new endpoint DELTE /notification_setting/{id} to delete a\n specified Notification Setting.\n * Introduced `POST /oracle/db/snapshot/{id}/export/tablespace` endpoint to\n trigger the export of a single tablespace in an Oracle database.\n * Added a new optional field `shouldRestoreFilesOnly` to POST\n /oracle/db/snapshot/{id}/export endpoint, used when exporting an Oracle\n database, to specify whether the user requires a full recovery of the\n database or a restoration of the database files.\n * Added /oracle/hierarchy/{id}/children endpoint to get children of\n object in Oracle hierarchy\n * Added /oracle/hierarchy/{id}/descendants endpoint to get descendants of\n object in Oracle hierarchy\n * Added a new endpoint POST /fileset/{id}/unprotect, which can be used to\n unprotect a fileset and specify a retention policy to apply to existing\n snapshots.\n * Added a new optional field `existingSnapshotRetention` to POST\n /sla_domain/{id}/assign, used when unprotecting an object, to specify whether\n to retain existing snapshots according to the current SLA domain, keep\n existing snapshots forever, or expire all snapshots immediately. If not\n specified, this field will default to the existing behavior of keeping\n snapshots forever.\n * Introduced `GET /kmip/client` endpoint to get the stored KMIP client\n configuration.\n * Introduced `PUT /kmip/client` endpoint to set the KMIP client configuration.\n * Introduced `GET /kmip/server` endpoint to get stored KMIP server\n information.\n * Introduced `PUT /kmip/server` endpoint to add a a KMIP server.\n * Introduced `DELETE /kmip/server` endpoint to remove a a KMIP server.\n * Introduced `POST /session` endpoint to generate session tokens.\n * Added a new optional field `mfaServerId` to /user endpoint for\n associating a configured MFA server.\n * Added REST support for Oracle RAC, Oracle Host.\n Updated the detail and summary for Oracle Database.\n * Added support to run on-demand backup jobs, export snapshots, live\n mount for Oracle Database.\n * Introduced `POST /mfa/rsa/server` endpoint to\n create a new RSA server configuration for MFA integration.\n * Introduced `GET /mfa/rsa/server` endpoint to\n get a list of RSA server configured for MFA integration.\n * Introduced `PATCH /mfa/rsa/server/{id}` endpoint to\n modify RSA server configuration.\n * Introduced `GET /mfa/rsa/server/{id}` endpoint to\n get RSA server configuration.\n * Introduced `POST /mfa/initialize` to initialize an attempt\n to perform Multifactor authentication for a user.\n * Introduced `POST /mfa/session` to perform Multifactor\n authentication for a user.\n * Introduced `POST /session/api_token` to create an API Token.\n * Added a new optional field `isArrayEnabled` to `FilesetTemplateCreate`.\n for creation of storage array-enabled fileset templates. We also include\n this new field in `FilesetTemplateDetail`.\n * Added a new optional field `arraySpec` to `FilesetCreate` for\n creation of storage array-enabled filesets. We also include\n this new field in `FilesetSummary` and `FilesetDetail`.\n * Introduced `GET /cluster/{id}/is_azure_cloud_only` to query if the cluster\n supports only Azure public cloud.\n * Introduced `POST /unmanaged_object/assign_retention_sla` to set Retention\n SLA of unmanaged objects.\n * Introduced `POST /unmanaged_object/snapshot/assign_sla` to set Retention\n SLA of unmanaged snapshots.\n * Introduced `POST /mssql/db/bulk/snapshot/{id}` to take an on-demand snapshot\n of multiple SQL Server databases. The result of this asynchronous request\n can be obtained from `GET /mssql/db/bulk/snapshot/{id}`.\n * Added a new field unprotectable_reasons to GET /mssql/db/{id} and\n GET /mssql/instance/{id}. This field keeps track of the reasons that a\n SQL Server database or instance cannot be protected by Rubrik.\n * Introduced a new `GET /cluster/me/login_banner` and\n `PUT /cluster/me/login_banner` endpoints to get and set the banner\n that displays after each successful login.\n * Introduced a new `GET /cluster/me/security_classification` and\n `PUT /cluster/me/security_classification` endpoints to get and set\n the security classification banner for the cluster. The cluster UI\n displays the banner in the specified color.\n * Introduced `GET /cluster/{id}/security/rksupport_cred` to provide\n the status of the rksupport credentials.\n * Introduced `POST /cluster/{id}/security/rksupport_cred` to update\n the cluster-wide credentials for the specified cluster.\n * Introduced `POST /vmware/vm/snapshot/{id}/mount_disks` to attach VMDKs\n from a mount snapshot to an existing virtual machine\n * Introduced new `GET /host/{id}/volume` endpoint to query the HostVolume\n from the host.\n * Added the `HostVolumeSummary`, which is the response of the endpoint\n `GET /host/{id}/volume` and a part of `VolumeGroupDetail`.\n * Introduced a new `GET /volume_group/host_layout/{snapshot_id}` and\n `GET /volume_group/{host_id}/host_layout` to get the Windows host layout\n of all disks and volumes.\n * Added `WindowsHostLayout` which is the response of\n `GET /volume_group/host_layout/{snapshot_id}` and\n `GET /volume_group/{host_id}/host_layout`.\n * Added support for Blueprint.\n * Added new fields `retentionSlaDomainId` and `retentionSlaDomainName` to\n UnmanagedObjectSummary object, which is returned from a\n `GET /unmanaged_object` call.\n * Removed `unmanagedSnapshotCount` and added new fields `autoSnapshotCount`.\n and `manualSnapshotCount` to UnmanagedObjectSummary object, which is\n returned from a `GET /unmanaged_object` call.\n * Added new fields `retentionSlaDomainId` and `retentionSlaDomainName` to\n UnmanagedSnapshotSummary object, which is returned from a\n `GET /unmanaged_object/{id}/snapshot` call.\n * Added a new field `hasAttachingDisk` to `GET /vmware/vm/snapshot/mount` and\n `GET /vmware/vm/snapshot/mount/{id}` that indicates to the user whether\n this is an attaching disk mount job.\n * Added a new field `attachingDiskCount` to `GET /vmware/vm/snapshot/mount`.\n and `GET /vmware/vm/snapshot/mount/{id}` that indicate to the user how many\n disks are attached.\n * Added field `RetentionSlaDomainName` to sort_by of a\n `GET * /unmanaged_object/{id}/snapshot` call.\n * Added field `excludedDiskIds` to NutanixVmDetail which is returned from a\n `GET /nutanix/vm/{id}` to exclude certain disks from backup. Also added\n field to NutanixVmPatch via `PATCH /nutanix/vm/{id}` to allow the field\n to be updated.\n * Introduced the `PATCH /aws/ec2_instance/indexing_state` endpoint for\n enabling/disabling indexing per EC2 instance.\n * Added new optional fields `organizationId` and `organizationName` to\n `/host/{id}` and `/host` endpoints to get the organization a host is\n assigned to due to Envoy.\n * Introduced a new `GET /host/envoy` endpoint. Acts similar to queryHost but\n also includes Envoy organization info if Envoy is enabled.\n * Added a new endpoint `GET /vmware/vcenter/{id}/tag_category` to get a list of\n Tag Categories associated with a vCenter.\n * Added a new endpoint `Get /vmware/vcenter/tag_category/{tag_category_id}` to\n get a specific Tag Category associated with a vCenter.\n * Added a new endpoint `GET /vmware/vcenter/{id}/tag` to get a list of Tags\n associated with a vCenter. The optional category_id parameter allow the\n response to be filtered by Tag Category.\n * Added a new endpoint `GET /vmware/vcenter/tag/{tag_id}` to get a\n specific Tag associated with a vCenter.\n * Introduced `GET /cluster/{id}/global_manager_connectivity` to\n retrieve a set of URLs that are pingable from the CDM cluster.\n * Added optional field `instanceName` in `ManagedObjectProperties`.\n * Added new endpoint GET `/cloud_on/aws/app_image/{id}` to retrieve a specified\n AWS AppBlueprint image.\n * Added new endpoint DELETE `/cloud_on/aws/app_image/{id}` to delete the\n given AWS AppBlueprint image.\n * Added new endpoint GET `/cloud_on/azure/app_image/{id}` to retrieve a\n specified Azure AppBlueprint image.\n * Added new endpoint DELETE `/cloud_on/azure/app_image/{id}` to delete the\n given Azure AppBlueprint image.\n * Added organization endpoint for Oracle.\n * Added new endpoint GET `/cloud_on/aws/app_image` to retrieve all\n AWS AppBlueprint images.\n * Added new endpoints `GET /stats/cloud_storage/physical`, `GET\n /stats/cloud_storage/ingested` and `GET /stats/cloud_storage/logical` which\n return respective stats aggregated across all archival locations\n * Added a new endpoint `POST /vmware/standalone_host/datastore` to get a list\n of datastore names for a given ESXi host.\n * Added a new optional field `apiEndpoint` to `NasBaseConfig`.\n\n ### Changes to Internal API in Rubrik version 4.2\n ## Breaking changes:\n * Introduced a new `GET /cluster/{id}/ipv6` endpoint for getting all IPv6\n addresses configured on a specific or all network interfaces.\n * Introduced a new `PATCH /cluster/{id}/ipv6` endpoint for configuring IPv6\n addresses on a specific network interface for each nodes in cluster.\n * Introduced a new `GET /cluster/{id}/trial_edge` for getting whether the\n cluster is a trial edge.\n * Moved the /auth_domain/ endpoint from internal APIs to the v1 APIs.\n * Deprecated `POST /archive/nfs/reconnect` endpoint. Use\n `POST /archive/nfs/reader/connect` instead to connect as a reader to an\n existing NFS archival location.\n * Deprecated `POST /archive/object_store/reconnect` endpoint. Use\n `POST /archive/object_store/reader/connect` instead to connect as a reader to\n an existing object store location.\n * Deprecated `POST /archive/qstar/reconnect` endpoint. Use\n `POST /archive/qstar/reader/connect` instead to connect as a reader to an\n existing QStar archival location.\n * Deprecated `POST /archive/dca/reconnect` endpoint. Use\n `POST /archive/dca/reader/connect` instead to connect as a reader to an\n existing DCA archival location.\n * Removed `POST /hyperv/vm/snapshot/{id}/restore_file` endpoint. Use\n `POST /hyperv/vm/snapshot/{id}/restore_files` instead to support\n multi-files restore for Hyper-V vm.\n * Removed `POST /nutanix/vm/snapshot/{id}/restore_file` endpoint. Use\n `POST /nutainx/vm/snapshot/{id}/restore_files` instead to support\n multi-files restore for Nutanix vm.\n * Removed `search_timezone_offset` parameter from\n `GET /unmanaged_object/{id}/snapshot` endpoint. The endpoint will now\n use configured timezone on the cluster.\n * Renamed the field `id` in `UserDefinition` to `username` for `POST /user`.\n endpoint.\n * Removed the `/mssql/db/sla/{id}/availability_group_conflicts` endpoint.\n * Removed the `/mssql/db/sla/{id}/assign` endpoint.\n * Added support for Envoy VMs for Organization.\n * Modified the `DELETE /storage/array/{id}` endpoint so that it now triggers\n an asynchronous deletion job, responds with an async request object, and\n archives the storage array's hierarchy.\n * Added `numStorageArrayVolumeGroupsArchived` to `DataLocationUsage` which\n is the response of the `GET /stats/data_location/usage` endpoint.\n * Modified `POST /storage/array` endpoint so that it now triggers an\n asynchronous refresh job, and responds with an async request object.\n * Modified the `GET /storage/array/{id}` and `DELETE /storage/array/{id}`.\n endpoints so that the `id` field now corresponds to the managed ID\n instead of the simple ID. The `managed ID` is the ID assigned to the\n storage array object by the Rubrik REST API server.\n * Moved /throttle endpoint to /backup_throttle.\n * Introduced a new `EmailSubscriptionUpdate` object for the request of the\n `PATCH /report/email_subscription/{subscription_id}` endpoint.\n * Introduced a new `ReportSubscriptionOwner` object for the response of\n `GET /report/email_subscription/{subscription_id}` and\n `GET /report/{id}/email_subscription` endpoints.\n * Added the envoyStatus field to the response of the GET /organization\n endpoint.\n * Added new `attachments` field to the `POST /report/{id}/email_subscription`.\n and `PATCH /report/email_subscription/{subscription_id}` endpoints.\n * Removed fields `length` and `isLog` in response of\n `/mssql/db/{id}/restore_files`.\n * Moved the `/cluster/decommissionNode` endpoint to\n `/cluster/decommissionNodes`. The `DecommissionNodeConfig` object is renamed\n as `DecommissionNodesConfig` and now takes in a list of strings which\n correspond to the IDs of the nodes that are to be decommissioned.\n * Moved the `POST /vmware/vm/{id}/register_agent` endpoint from internal\n APIs to the v1 APIs.\n * Added a required field for environment in AzureComputeSummary to support\n Azure Gov Cloud.\n * Remove `POST internal/vmware/vm/snapshot/{id}/mount` endpoint. Use public\n API of `POST v1/vmware/vm/snapshot/{id}/mount`.\n * The input field OperatingSystemType value `Linux` is replaced by `UnixLike`.\n in FilesetTemplateCreateDefinition, used by POST /fileset-template, and\n in FilesetTemplatePatchDefinition, used by PATCH /fileset_template/{id}.\n * The input field operating_system_type value `Linux` is replaced by `UnixLike`.\n in GET /host-fileset and GET /host-count.\n * Added `snmpAgentPort` field to SnmpConfig object.\n\n ## Feature Additions/improvements:\n * Introduced the `GET /node_management/default_gateway` and `POST\n /node_management/default_gateway` endpoint to get and set default gateway.\n * Introduced the `GET cloud_on/aws/instance_type_list` and `GET\n cloud_on/azure/instance_type_list` endpoint to fetch list of instance types\n for aws and azures.\n * Introduced the `GET /aws/account/{id}/subnet` endpoint to fetch an\n information summary for each of the subnets available in an AWS account.\n * Introduced the `GET /aws/account/{id}/security_group` endpoint to fetch an\n information summary for each of the security groups belonging to a particular\n virtual network in an AWS account.\n * Moved definitions `Subnet` and `SecurityGroup` of `definitions/cloud_on.yml`.\n to `definitions/cloud_common.yml` so that both the CloudOn and CloudNative\n features can use them.\n * Introduced the `GET /host/{id}/diagnose` endpoint to support target host\n diagnosis features. Network connectivity (machine/agent ping) implemented\n in the current version.\n * Added vCD endpoints to support vCloud Director. The following endpoints\n have been added to the vcdCluster object:\n - `POST /vcd/cluster` to add a new vCD cluster object.\n * Added support for CRUD operations on vCloud Director cluster objects.\n - POST /vcd/cluster, PATCH /vcd/cluster/{id}, DELETE /vcd/cluster/{id},\n POST /vcd/cluster/{id}/refresh are the endpoints for adding, editing,\n deleting and refreshing a vCD cluster object respectively.\n * Introduced endpoint `GET /search/snapshot_search` to search files in a\n given snapshot. The search supports prefix search only.\n * Introduced the new `POST /storage/array/{id}/refresh` endpoint to\n create a new refresh job to update the Storage Array metadata.\n * Introduced the new `GET /storage/array/request/{id}` endpoint to\n get status of a storage array-related asynchronous request.\n * Introduced the new `POST /storage/array/volume/group` endpoint\n to add a new storage array volume group.\n * Introduced the new `GET /storage/array/volume/group/{id}` endpoint\n to get details of a storage array volume group.\n * Introduced the new `DELETE /storage/array/volume/group/{id}` endpoint\n to remove a storage array volume group.\n * Introduced the new `GET /storage/array/hierarchy/{id}` endpoint\n to get a summary of an object in the storage array hierarchy.\n * Introduced the new `GET /storage/array/hierarchy/{id}/children` endpoint\n to get the children of an object in the storage array hierarchy.\n * Introduced the new `GET /storage/array/hierarchy/{id}/descendants` endpoint\n to get the descendants of an object in the storage array hierarchy.\n * Introduced the new `GET /storage/array/volume` endpoint to get\n summary information of all storage array volumes.\n * Introduced the new `GET /storage/array/volume/{id}` endpoint to get\n details of a storage array volume.\n * Introduced the new `POST /storage/array/volume/group/{id}/snapshot`.\n endpoint to create a new on-demand backup job for a storage array\n volume group.\n * Introduced the new `PATCH /storage/array/volume/group/{id}` endpoint to\n update the properties of a storage array volume group object.\n * Introduced the new `GET /storage/array/volume/group` endpoint to\n get all storage array volume groups subject to specified filters.\n * Introduced endpoint `POST /archive/location/{id}/owner/pause` to pause\n archiving to a given archival location that is owned by the current cluster.\n * Introduced endpoint `POST /archive/location/{id}/owner/resume` to resume\n archiving to a given archival location that is owned by the current cluster.\n * Introduced endpoint `POST /archive/location/{id}/reader/promote` to promote\n the current cluster to be the owner of a specified archival location that is\n currently connected as a reader location.\n * Introduced endpoint `POST /archive/location/{id}/reader/refresh` to sync the\n current reader cluster with the contents on the archival location. This pulls\n in any changes made by the owner cluster to the archival location since the\n last time the current cluster was synced.\n * Introduced endpoint `POST /archive/dca/reader/connect` to connect as a reader\n to a DCA archival location.\n * Introduced endpoint `POST /archive/nfs/reader/connect` to connect as a reader\n to an NFS archival location.\n * Introduced endpoint `POST /archive/object_store/reader/connect` to connect as\n a reader to an object store location.\n * Introduced endpoint `POST /archive/dca/qstar/connect` to connect as a reader\n to a QStar archival location.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include `ownershipStatus` field, to indicate whether the current\n cluster is connected to the archival location as an owner (active or paused),\n as a reader, or if the archival location is deleted.\n * Added the `ca_certs` field to `StorageArrayDefinition` to allow admins\n to specify certificates used for validation when making network\n requests to the storage array API service. This effects endpoints\n `POST /storage/array`, `GET /storage/array/{id}`, and\n `PUT /storage/array/{id}`.\n * Introduced the `POST /vmware/vm/snapshot/{id}/download_files` endpoint to\n download multiple files/folders from a given vm snapshot. The URL to\n download the zip file including the files will be presented to the users.\n * Introduced the `POST /fileset/snapshot/{id}/download_files` endpoint to\n download multiple files/folders from a given fileset snapshot. The URL to\n download the zip file including the specific files/folders will be presented\n to the users.\n * Introduced the `POST /nutanix/vm/snapshot/{id}/download_files` endpoint to\n download multiple files/folders from a given nutanix snapshot. The URL to\n download the zip file including the specific files/folders will be presented\n to the users.\n * Removed the `POST /nutanix/vm/snapshot/{id}/download_file` endpoint as\n downloading a single file/folder from the nutanix backup is just a special\n case of downloading multiple files/folders.\n * Introduced the `POST /hyperv/snapshot/{id}/download_files` endpoint to\n download multiple files/folders from a given Hyper-V snapshot. The URL to\n download the zip file including the specific files/folders will be presented\n to the users.\n * Introduced the POST /managed_volume/snapshot/{id}/download_files endpoint\n to download multiple files and/or folders from a given managed volume\n snapshot. This endpoint returns the URL to download the ZIP file that\n contains the specified files and/or folders.\n * Introduced the new `GET /storage/array/volume/group/{id}/search` endpoint to\n search storage array volume group for a file.\n * Introduced the new `GET /storage/array/volume/group/snapshot/{id}`.\n endpoint to retrieve details of a storage array volume group snapshot.\n * Introduced the new `DELETE /storage/array/volume/group/snapshot/{id}`.\n endpoint to remove a storage array volume group snapshot.\n * Introduced the new `DELETE /storage/array/volume/group/{id}` endpoint\n to delete all snapshots of a storage array volume group.\n * Introduced the new `POST /storage/array/volume/group/{id}/download`.\n endpoint to download a storage array volume group snapshot from archival.\n * Introduced new `GET/storage/array/volume/group/snapshot/{id}/restore_files`.\n endpoint to restore files from snapshot of a storage array volume group.\n * Added storage volume endpoints for AWS cloud native workload protection.\n Endpoints added:\n - GET /aws/ec2_instance/{id}/storage_volume/ to retrieve\n all storage volumes details attached to an ec2 instance object.\n - GET /aws/ec2_instance/{ec2_instance_id}/storage_volume/{id} to retrieve\n details of a storage volume attached to an ec2 instance object.\n - POST /aws/ec2_intance/snapshot/{id}/export to export the snapshot of\n an ec2 instance object to a new ec2 instance object.\n * Introduced the new `POST /storage/array/volume/group/{id}/download_file`.\n endpoint to download a file from an archived storage array volume group\n snapshot.\n * Introduced the new `GET /storage/array/volume/group/{id}/missed_snapshot`.\n endpoint to get details about all missed snapshots of a storage array volume\n group.\n * Introduced the `GET /network_throttle` endpoint for retrieving the list of\n network throttles.\n * Introduced the `PATCH /network_throttle/{id}` endpoint for updating\n network throttles.\n * Introduced the new `GET /storage/array/host/{id}` endpoint to get details\n about all storage array volumes connected to a host.\n * Introduced the `GET /organization/{id}/storage/array` endpoint for getting\n information for authorized storage array resources in an organization.\n * Introduced the `GET /organization/{id}/storage/array/volume_group/metric`.\n endpoint for getting storage array volume groups metrics in an\n organization.\n * Introduced the new POST /vmware/vm/snapshot/mount/{id}/rollback endpoint to\n rollback the datastore used by a virtual machine, after an Instant Recovery\n that used the preserve MOID setting. This endpoint `rolls back` the\n recovered virtual machine's datastore from the Rubrik cluster to the\n original datastore.\n * Added `owner` and `status` fields to the `EmailSubscriptionSummary`.\n object used in responses for many `/report/{id}/email_subscription`.\n and `/report/email_subscription/{subscription_id}` endpoints.\n * Added `availableSpace` and `readerLocationSummary` fields to the\n `NfsLocationDetail` object used in responses for `/archive/nfs` and\n `/archive/nfs/{id}` endpoints.\n * Added `availableSpace` and `readerLocationSummary` fields to the\n `QstarLocationSummary` object used in responses for the `/archive/qstar`.\n endpoint.\n * Added `availableSpace` and `readerLocationSummary` fields to the\n `QstarLocationDetail` object used in responses for the `/archive/qstar/{id}`.\n endpoint.\n * Added `readerLocationSummary` field to the `ObjectStoreLocationDetail`.\n object used in responses for the `/archive/object_store` and\n `/archive/object_store/{id}` endpoints.\n * Added `readerLocationSummary` field to the `DcaLocationDetail` object\n used in responses for the `/archive/dca` and `/archive/dca/{id}` endpoints.\n * Added a new field `guestOsType` to `HypervVirtualMachineDetail`.\n object used in response of `GET /hyperv/vm/{id}`.\n * Added a new field `guestOsType` to `VirtualMachineDetail`.\n object referred by `VappVmDetail`.\n * Added new field `fileType` in response of `/mssql/db/{id}/restore_files`.\n * Added an optional field `agentStatus` to `VirtualMachineSummary` object used\n in response of `GET /vmware/vm` endpoint. This allows user to check the\n Rubrik Backup Service connection status of the corresponding VMware VM.\n * Introduced the new `POST /fileset/snapshot/{id}/export_files` endpoint to\n export multiple files or directories to destination host.\n * Introduced the new `GET /vmware/config/esx_subnets` endpoint to get the\n the preferred subnets to reach ESX hosts.\n * Introduced the new `PATCH /vmware/config/reset_esx_subnets` endpoint to\n reset the preferred subnets to reach ESX hosts.\n * Changed the `PATCH /vmware/config/reset_esx_subnets` endpoint to\n `PATCH /vmware/config/set_esx_subnets`.\n * Removed the `needsInspection` field from the NodeStatus object returned in\n the `/cluster/{id}/node` and `/node` endpoints.\n * Introduced the new `PATCH /auth_domain/{id}` endpoint to update the Active\n Directory configuration parameters.\n * Introduced the new `GET /cluster/{id}/auto_removed_node` endpoint to\n query for unacknowledged automatic node removals by the Rubrik cluster.\n * Introduced the new\n `DELETE /cluster/{id}/auto_removed_node/{node_id}/acknowledge` endpoint to\n acknowledge an automatic node removal.\n * Introduced the new `GET /cluster/{id}/system_status` endpoint to retrieve\n information about the status of the Rubrik cluster.\n * Changed the `POST /cloud_on/azure/subscription` endpoint to to take\n the parameter `AzureSubscriptionRequest` instead of\n `AzureSubscriptionCredential` in body.\n * Changed the `POST /cloud_on/azure/storage_account` endpoint to to take\n the parameter `AzureStorageAccountRequest` instead of\n `AzureStorageAccountCredential` in body.\n * Changed the `POST /cloud_on/azure/resource_group` endpoint to take\n the parameter `AzureResourceGroupRequest` instead of\n `AzureResourceGroupCredential` in body.\n * Added a `reportTemplate` field to the response of both the\n `GET /report/{id}/table` and `GET /report/{id}/chart` endpoints.\n\n ### Changes to Internal API in Rubrik version 4.1\n ## Changes to support instance from image\n * POST /aws/instance and /azure/instance was supported only from a Rubrik\n snapshot. Now it is changed to support instantiation from Rubrik snapshot as\n well as pre-existing image. Rest end point is same, we just changed the\n CreateCloudInstanceRequest object type.\n * Add a new field `ignoreErrors` to POST /vmware/vm/snapshot/{id}/restore_files\n that will let job restore ignore file errors during restore job.\n ## Breaking changes:\n * None is removed as a Nutanix snapshot consistency mandate so it is no\n longer valid in GET /nutanix/vm, GET /nutanix/vm/{id}, and\n PATCH /nutanix/vm/{id}.\n * computeSecurityGroupId is replaced by the object defaultComputeNetworkConfig\n in ObjectStoreLocationSummary ,ObjectStoreUpdateDefinition and\n ObjectStoreReconnectDefinition which are used by\n GET /archive/object_store/{id}, PATCH /archive/object_store/{id} and\n POST /archive/object_store/reconnect respectively.\n * The PUT /throttle endpoint was changed to provide configuration for\n Hyper-V adaptive throttling. Three parameters were added:\n hypervHostIoLatencyThreshold, hypervHostCpuUtilizationThreshold, and\n hypervVmCpuUtilizationThreshold. To differentiate between the multiple\n hypervisors, the existing configuration parameters for VMware were renamed\n VmwareVmIoLatencyThreshold, VmwareDatastoreIoLatencyThreshold and\n VmwareCpuUtilizationThreshold. These changes also required modifications\n and additions to the GET /throttle endpoint.\n * For `POST /cluster/{id}/node` endpoint, it gets now `AddNodesConfig` in body\n instead of `Map_NodeConfig` directly.\n * For `POST /node_management/replace_node` endpoint, added the `ipmiPassword`.\n field to the `ReplaceNodeConfig` object.\n * For `POST /stats/system_storage` endpoint, added the miscellaneous, liveMount\n and snapshot field to `SystemStorageStats` object.\n * For `POST /principal_search`, removed `managedId` field from the\n `PrincipalSummary` object and changed the `id` field of the\n `PrincipalSummary` object to correspond to the managed id instead of the\n simple id.\n * For `GET /cluster/{id}/timezone` and `PATCH /cluster/{id}/timezone`, the\n functionality has merged into `GET /cluster/{id}` and `PATCH /cluster/{id}`.\n in v1.\n * Removed the `GET /cluster/{id}/decommissionNodeStatus` endpoint.\n Decommission status is now available through queries of the `jobId` that is\n returned by a decommission request. Queries can be performed at the\n `GET /job/{id}` endpoint.\n * For `GET /api/internal/managed_volume/?name=`, the name match is now\n exact instead of infix\n * Updated the list of available attribute and measure values for the `chart0`.\n and `chart1` parameters for the `PATCH /report/{id}` endpoint.\n * Updated the list of available column values for the `table` parameter for the\n `PATCH /report/{id}` endpoint.\n * Updated the `FolderHierarchy` response object to include\n `effectiveSlaDomainId`, `effectiveSlaDomainName`,\n `effectiveSlaSourceObjectId`, and `effectiveSlaSourceObjectName`.\n\n ## Feature Additions/improvements:\n * Added the field `pendingSnapshotCount` to ManagedVolumeSummary and\n ManagedVolumeDetail objects used in responses for endpoints\n `GET /managed_volume`, `POST /managed_volume`, `GET /managed_volume/{id}`,\n `PATCH /managed_volume/{id}`, `GET /organization/{id}/managed_volume`.\n * Introduced the `GET /managed_volume/snapshot/export/{id}` endpoint\n to retrieve details of a specific managed volume snapshot export.\n * Added the `name` filter for GET requests on the /replication/target endpoint.\n This filter allows users to filter results based on the name of a\n replication target.\n * Added the `name` filter for GET requests on the /archive/location endpoint.\n This filter allows users to filter results based on the name of an\n archival location.\n * Added new fields `replicas` and `availabilityGroupId` on GET /mssql\n and GET /mssql/{id}. If a database is an availability database,\n it will have some number of replicas, which are copies of the database\n running on different instances. Otherwise, there will only be one\n replica, which represents the single copy of the database. The field\n `availabilityGroupId` will be set only for availability databases\n and points to the availability group of the database. Also deprecated\n several fields on these endpoints, as they should now be accessed via\n the `replicas` field.\n * Added `Cluster` notification type.\n * Added optional `organizationId` parameter to to the grant/revoke and get\n authorization endpoints. This parameter can be used to\n grant/revoke/get authorizations with respect to a specific Organization.\n * Added endpoint to get/set whether the Rubrik Backup Service is automatically\n deployed to a guest OS.\n * Added cloudInstantiationSpec field to Hyper-V VM endpoint for configuring\n automatic cloud conversion\n * Introduced a new end point /cluster/{id}/platforminfo to GET information\n about the platform the current software is running on\n * Introduced the `GET /organization` and `GET /organization/{id}` endpoints\n for retrieving the list of organizations and a single organization.\n * Introduced the `POST /organization` endpoint for creating organizations,\n the `PATCH /organization/{id}` endpoint for updating organizations and the\n `DELETE /organization/{id}` endpoint for deleting organizations.\n * Introduced the `GET /organization/{id}/stats/storage_growth_timeseries`.\n endpoint and the `GET /organization/{id}/stats/total_storage_usage` for\n getting Physical Storage Growth over Time and Total Physical Storage Usage\n on a per Organization basis.\n * Introduced a number of endpoints of the format\n `GET /organization/{id}/` for retrieving all the resources of\n the corresponding type in a given organization.\n * Introduced a number of endpoints of the format\n `GET /organization/{id}//metric` for retrieving the protection\n counts of the resources of the corresponding type in a given organization.\n * Added the `reportTemplate` filter for GET requests on the /report endpoint.\n This allows queried reports to be filtered and sorted by report template.\n * Introduced the `POST /cluster/{id}/security/password/strength` endpoint\n for assessing the strength of passwords during bootstrap through rkcli.\n * Added a new `ipv6` field in the response of the `GET /cluster/{id}/discover`.\n endpoint.\n * Added relatedIds field for EventSummary object to give more context about\n the event.\n * Added operatingSystemType field for NutanixSummary object. This field\n represents the type of operating system on the Nutanix virtual machine.\n\n ### Changes to Internal API in Rubrik version 4.0\n ## Breaking changes:\n * For `GET /unmanaged_object` endpoint, replaced the `Fileset` of object_type\n filter with more specific object types: `WindowsFileset`, `LinuxFileset` and\n `ShareFileset`. Also added filter value for additional unmanaged objects\n we now support.\n * For /mssql/db/{id}/compatible_instance added recoveryType as mandatory\n query parameter\n\n ## Feature Additions/improvements:\n * Added QStar end points to support it as an archival location. The location\n is always encrypted and an encryption password must be set while adding the\n location. End points added:\n - `DELETE /archive/qstar` to clean up the data in the bucket in the QStar\n archival location.\n - `GET /archive/qstar` to retrieve a summary of all QStar archival locations.\n - `POST /archive/qstar` to add a QStar archival location.\n - `POST /archive/qstar/reconnect` to reconnect to a specific QStar archival\n location.\n - `POST /archive/qstar/remove_bucket` to remove buckets matching a prefix\n from QStar archival location.\n - `GET /archive/qstar/{id}` to retrieve a summary information from a specific\n QStar archival location.\n - `PATCH /archive/qstar/{id}` to update a specific QStar archival location.\n * Added the `name` filter for GET requests on the /archive/location endpoint.\n This filter allows users to filter results based on the name of an\n archival location.\n * Introduced an optional parameter `encryptionPassword` for the\n `/data_location/nfs` `POST` endpoint. This password is used for\n deriving the master key for encrypting the NFS archival location.\n * Introduced /managed\\_volume, /managed\\_volume/snapshot/export/{id},\n and other child endpoints for creating, deleting, and updating\n Managed Volumes and its exports and snapshots.\n * Added support for Hyper-V.\n * Add new /hierarchy endpoint to support universal hierarchy view.\n * Added support for Nutanix.\n * Moved and merged vCenter refresh status and delete status from independent\n internal endpoints to a single status field in v1 vCenter detail.\n * Added endpoint to get/set whether the Rubrik Backup Service is automatically\n deployed to a guest OS.\n * Introduced an optional parameter `minTolerableNodeFailures` for the\n `/cluster/decommissionNode` `POST` endpoint. This parameter specifies the\n minimum fault tolerance to node failures that must exist when a node is\n decommissioned.\n * Added `nodeId` to `AsyncRequestStatus` to improve debugging job failures.\n\n ### Changes to Internal API in Rubrik version 3.2.0\n ## Breaking changes:\n * Introduced endpoint /host/share/id/search to search for\n files on the network share.\n * Introduced endpoints /host/share and /host/share/id to\n support native network shares under /host endpoint.\n * For /unmanaged_object endpoints, change sort_attr to sort_by\n sort_attr used to accept a comma separated list of column names to sort.\n Now sort_by only accepts a single column name.\n * For /unmanaged_object endpoints, removed the need for object type when\n deleting unmanaged objects and its snapshots.\n\n ## Feature Additions/improvements:\n * Added internal local_ end points. These are used for\n handling operations on per-node auto-scaling config values.\n Please see src/spec/local-config/comments for details.\n * For the response of /mssql/db/{id}/restore_files, added two more fields\n for each file object. They are the original file name and file length\n of the file to be restore.\n * Introduced a new end point /cluster/{id}/is_registered to GET registration\n status. With this change, we can query if the cluster is registered in the\n Rubrik customer database.\n * Introduced a new end point /cluster/{id}/registration_details to POST\n registration details. Customers are expected to get the registration details\n from the support portal. On successful submission of registration details\n with a valid registration id, the cluster will mark itself as registered.\n * For the /mssql/instance/{id} end point, added fields configuredSlaDomainId,\n configuredSlaDomainName, logBackupFrequencyInSeconds, logRetentionHours,\n and copyOnly.\n * Introduced optional parameter keepMacAddresses to\n POST /vmware/vm/snapshot/{id}/mount, /vmware/vm/snapshot/{id}/export, and\n /vmware/vm/snapshot/{id}/instant_recovery endpints.\n This allows new VMs to have the same MAC address as their source VMs.\n\n ## Bug fixes:\n * Made path parameter required in GET /browse. Previously, an error was\n thrown when path was not passed in. This solves that bug.\n", + "x-logo": { + "url": "https://www.rubrik.com/wp-content/uploads/2016/11/Rubrik-Snowflake-small.png" + } + }, + "paths": { + "/polaris/replication/source/replicate_app/{snappable_id}": { + "post": { + "parameters": [ + { + "name": "snappable_id", + "in": "path", + "description": "Snappable ID of which we are replicating snapshots.", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "definition", + "description": "Polaris source pull replicate definition.", + "required": true, + "schema": { + "$ref": "#/definitions/PolarisPullReplicateDefinition" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "PolarisPullReplicateDefinition": { + "type": "object", + "required": [ + "accessKey", + "isOnDemand", + "polarisId", + "secretKey", + "snapshotInfo" + ], + "properties": { + "polarisId": { + "type": "string", + "description": "Managed ID of the Polaris source cluster." + }, + "snapshotInfo": { + "description": "Info of the snapshot which this cluster is replicating from Polaris.", + "$ref": "#/definitions/ReplicationSnapshotInfo" + }, + "accessKey": { + "type": "string", + "description": "The access key used for accessing customer's volumes to pull replicate snapshots." + }, + "secretKey": { + "type": "string", + "description": "The secret key used for accessing customer's volumes to pull replicate snapshots.", + "x-secret": true + }, + "isOnDemand": { + "type": "boolean", + "description": "Indicates if snapshot is on-demand." + } + } + }, + "ReplicationSnapshotInfo": { + "type": "object", + "required": ["snappableId", "snapshotId"], + "properties": { + "snappableId": { + "type": "string", + "description": "The ID of the snappable stored on this cluster." + }, + "snapshotId": { + "type": "string", + "description": "The ID of the snapshot that is being replicated." + }, + "snapshotDate": { + "type": "integer", + "format": "int64", + "description": "The date when the snapshot was taken in number of milliseconds since the UNIX epoch. This is a required field when the replication source is Polaris." + }, + "snapshotDiskInfos": { + "type": "array", + "description": "An array of the details of the snapshot disks that need to be replicated. This is a required field when the replication source is Polaris.", + "items": { + "$ref": "#/definitions/ReplicationSnapshotDiskInfo" + } + }, + "appMetadata": { + "type": "string", + "description": "Serialized metadata specific to the snappable which is being replicated. This is a required field when the replication source is Polaris." + }, + "childSnapshotInfos": { + "type": "array", + "description": "An array of child snapshots information.", + "items": { + "$ref": "#/definitions/ReplicationSnapshotInfo" + } + } + } + }, + "ReplicationSnapshotDiskInfo": { + "type": "object", + "required": [ + "diskFailoverInfo", + "diskId", + "isOsDisk", + "logicalSizeInBytes", + "snapshotDiskId" + ], + "properties": { + "diskId": { + "type": "string", + "description": "The ID of the disk/volume that is being replicated." + }, + "snapshotDiskId": { + "type": "string", + "description": "The ID of the snapshot of the disk/volume taken on the source that needs to be replicated." + }, + "logicalSizeInBytes": { + "type": "integer", + "format": "int64", + "description": "Size of the disk/volume that is being replicated." + }, + "isOsDisk": { + "type": "boolean", + "description": "Flag to specify if the disk is OS disk." + }, + "diskFailoverInfo": { + "description": "Details specific to the target snappable required to failover the EBS volumes.", + "$ref": "#/definitions/InstanceFailoverInfo" + } + } + }, + "InstanceFailoverInfo": { + "type": "object", + "required": ["originalDiskIdentifier"], + "properties": { + "originalDiskIdentifier": { + "type": "string", + "description": "The identifier used to map the original disks before failover to the disks being replicated. For vmware to AWS, this would be the deviceKey of the vmware virtual disk this EBS volume corresponds to." + } + } + } + } +} From e03b5a87ebc61c77dc93788e369deb771e052793 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 21 Sep 2022 15:52:14 +0200 Subject: [PATCH 180/260] Validate default values against schema (#610) --- openapi3/issue136_test.go | 53 +++++++++++++++++++++++++++++++++++++++ openapi3/schema.go | 6 +++++ 2 files changed, 59 insertions(+) create mode 100644 openapi3/issue136_test.go diff --git a/openapi3/issue136_test.go b/openapi3/issue136_test.go new file mode 100644 index 000000000..36c8b8588 --- /dev/null +++ b/openapi3/issue136_test.go @@ -0,0 +1,53 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue136(t *testing.T) { + specf := func(dflt string) string { + return ` +openapi: 3.0.2 +info: + title: "Hello World REST APIs" + version: "1.0" +paths: {} +components: + schemas: + SomeSchema: + type: string + default: ` + dflt + ` +` + } + + for _, testcase := range []struct { + dflt, err string + }{ + { + dflt: `"foo"`, + err: "", + }, + { + dflt: `1`, + err: "invalid components: invalid schema default: Field must be set to string or not be present", + }, + } { + t.Run(testcase.dflt, func(t *testing.T) { + spec := specf(testcase.dflt) + + sl := NewLoader() + + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(sl.Context) + if testcase.err == "" { + require.NoError(t, err) + } else { + require.Error(t, err, testcase.err) + } + }) + } +} diff --git a/openapi3/schema.go b/openapi3/schema.go index ded97f02a..9b36e604a 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -753,6 +753,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } + if v := schema.Default; v != nil { + if err := schema.VisitJSON(v); err != nil { + return fmt.Errorf("invalid schema default: %w", err) + } + } + if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled { if err := validateExampleValue(x, schema); err != nil { return fmt.Errorf("invalid schema example: %w", err) From b304f8caaee3655f254e07e46a9b1c7da7758bfe Mon Sep 17 00:00:00 2001 From: Nic Date: Wed, 21 Sep 2022 21:54:44 +0800 Subject: [PATCH 181/260] fix: only inject default value for matched oneOf or anyOf (#604) --- go.mod | 1 + go.sum | 2 + openapi3/schema.go | 39 +++++- openapi3filter/validate_set_default_test.go | 137 +++++++++++++++++++- 4 files changed, 166 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 50aba584b..8d93cf754 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 github.com/gorilla/mux v1.8.0 github.com/invopop/yaml v0.1.0 + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index a123aaff6..074de2aa8 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/openapi3/schema.go b/openapi3/schema.go index 9b36e604a..295180fdd 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -13,6 +13,7 @@ import ( "unicode/utf16" "github.com/go-openapi/jsonpointer" + "github.com/mohae/deepcopy" "github.com/getkin/kin-openapi/jsoninfo" ) @@ -915,9 +916,17 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val } } - ok := 0 - validationErrors := []error{} - for _, item := range v { + var ( + ok = 0 + validationErrors = []error{} + matchedOneOfIdx = 0 + tempValue = value + ) + // make a deep copy to protect origin value from being injected default value that defined in mismatched oneOf schema + if settings.asreq || settings.asrep { + tempValue = deepcopy.Copy(value) + } + for idx, item := range v { v := item.Value if v == nil { return foundUnresolvedRef(item.Ref) @@ -927,11 +936,12 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val continue } - if err := v.visitJSON(settings, value); err != nil { + if err := v.visitJSON(settings, tempValue); err != nil { validationErrors = append(validationErrors, err) continue } + matchedOneOfIdx = idx ok++ } @@ -962,17 +972,30 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return e } + + if settings.asreq || settings.asrep { + _ = v[matchedOneOfIdx].Value.visitJSON(settings, value) + } } if v := schema.AnyOf; len(v) > 0 { - ok := false - for _, item := range v { + var ( + ok = false + matchedAnyOfIdx = 0 + tempValue = value + ) + // make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema + if settings.asreq || settings.asrep { + tempValue = deepcopy.Copy(value) + } + for idx, item := range v { v := item.Value if v == nil { return foundUnresolvedRef(item.Ref) } - if err := v.visitJSON(settings, value); err == nil { + if err := v.visitJSON(settings, tempValue); err == nil { ok = true + matchedAnyOfIdx = idx break } } @@ -986,6 +1009,8 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val SchemaField: "anyOf", } } + + _ = v[matchedAnyOfIdx].Value.visitJSON(settings, value) } for _, item := range schema.AllOf { diff --git a/openapi3filter/validate_set_default_test.go b/openapi3filter/validate_set_default_test.go index 40714051a..4550b51b2 100644 --- a/openapi3filter/validate_set_default_test.go +++ b/openapi3filter/validate_set_default_test.go @@ -245,6 +245,78 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) { } } } + }, + "social_network": { + "oneOf": [ + { + "type": "object", + "required": ["platform"], + "properties": { + "platform": { + "type": "string", + "enum": [ + "twitter" + ] + }, + "tw_link": { + "type": "string", + "default": "www.twitter.com" + } + } + }, + { + "type": "object", + "required": ["platform"], + "properties": { + "platform": { + "type": "string", + "enum": [ + "facebook" + ] + }, + "fb_link": { + "type": "string", + "default": "www.facebook.com" + } + } + } + ] + }, + "social_network_2": { + "anyOf": [ + { + "type": "object", + "required": ["platform"], + "properties": { + "platform": { + "type": "string", + "enum": [ + "twitter" + ] + }, + "tw_link": { + "type": "string", + "default": "www.twitter.com" + } + } + }, + { + "type": "object", + "required": ["platform"], + "properties": { + "platform": { + "type": "string", + "enum": [ + "facebook" + ] + }, + "fb_link": { + "type": "string", + "default": "www.facebook.com" + } + } + } + ] } } } @@ -281,13 +353,20 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) { OP string `json:"op,omitempty"` Value int `json:"value,omitempty"` } + type socialNetwork struct { + Platform string `json:"platform,omitempty"` + FBLink string `json:"fb_link,omitempty"` + TWLink string `json:"tw_link,omitempty"` + } type body struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Code int `json:"code,omitempty"` - All bool `json:"all,omitempty"` - Page *page `json:"page,omitempty"` - Filters []filter `json:"filters,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Code int `json:"code,omitempty"` + All bool `json:"all,omitempty"` + Page *page `json:"page,omitempty"` + Filters []filter `json:"filters,omitempty"` + SocialNetwork *socialNetwork `json:"social_network,omitempty"` + SocialNetwork2 *socialNetwork `json:"social_network_2,omitempty"` } testCases := []struct { @@ -531,6 +610,52 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) { "value": 456 } ] +} + `, body) + }, + }, + { + name: "social_network(oneOf)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + SocialNetwork: &socialNetwork{ + Platform: "facebook", + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "social_network": { + "platform": "facebook", + "fb_link": "www.facebook.com" + } +} + `, body) + }, + }, + { + name: "social_network_2(anyOf)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + SocialNetwork2: &socialNetwork{ + Platform: "facebook", + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "social_network_2": { + "platform": "facebook", + "fb_link": "www.facebook.com" + } } `, body) }, From 62f85cfe75b720f25179e5272a7c551146bf7dd9 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 22 Sep 2022 15:13:44 +0200 Subject: [PATCH 182/260] Deterministic validation (#602) --- openapi3/callback.go | 9 +- openapi3/components.go | 73 ++- openapi3/content.go | 9 +- openapi3/encoding.go | 11 +- openapi3/issue601_test.go | 34 + openapi3/loader.go | 27 +- openapi3/media_type.go | 16 +- openapi3/openapi3.go | 77 +-- openapi3/operation.go | 4 + openapi3/parameter.go | 15 +- openapi3/path_item.go | 11 +- openapi3/paths.go | 21 +- openapi3/response.go | 27 +- openapi3/schema.go | 25 +- openapi3/security_requirements.go | 4 +- openapi3/server.go | 14 +- openapi3/testdata/lxkns.yaml | 988 ++++++++++++++++++++++++++++++ 17 files changed, 1284 insertions(+), 81 deletions(-) create mode 100644 openapi3/issue601_test.go create mode 100644 openapi3/testdata/lxkns.yaml diff --git a/openapi3/callback.go b/openapi3/callback.go index 718f47c1e..1e4736946 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -3,6 +3,7 @@ package openapi3 import ( "context" "fmt" + "sort" "github.com/go-openapi/jsonpointer" ) @@ -30,7 +31,13 @@ type Callback map[string]*PathItem // Validate returns an error if Callback does not comply with the OpenAPI spec. func (callback Callback) Validate(ctx context.Context) error { - for _, v := range callback { + keys := make([]string, 0, len(callback)) + for key := range callback { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + v := callback[key] if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/components.go b/openapi3/components.go index ce7a86990..3f883faf0 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "regexp" + "sort" "github.com/getkin/kin-openapi/jsoninfo" ) @@ -40,7 +41,13 @@ func (components *Components) UnmarshalJSON(data []byte) error { // Validate returns an error if Components does not comply with the OpenAPI spec. func (components *Components) Validate(ctx context.Context) (err error) { - for k, v := range components.Schemas { + schemas := make([]string, 0, len(components.Schemas)) + for name := range components.Schemas { + schemas = append(schemas, name) + } + sort.Strings(schemas) + for _, k := range schemas { + v := components.Schemas[k] if err = ValidateIdentifier(k); err != nil { return } @@ -49,7 +56,13 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } - for k, v := range components.Parameters { + parameters := make([]string, 0, len(components.Parameters)) + for name := range components.Parameters { + parameters = append(parameters, name) + } + sort.Strings(parameters) + for _, k := range parameters { + v := components.Parameters[k] if err = ValidateIdentifier(k); err != nil { return } @@ -58,7 +71,13 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } - for k, v := range components.RequestBodies { + requestBodies := make([]string, 0, len(components.RequestBodies)) + for name := range components.RequestBodies { + requestBodies = append(requestBodies, name) + } + sort.Strings(requestBodies) + for _, k := range requestBodies { + v := components.RequestBodies[k] if err = ValidateIdentifier(k); err != nil { return } @@ -67,7 +86,13 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } - for k, v := range components.Responses { + responses := make([]string, 0, len(components.Responses)) + for name := range components.Responses { + responses = append(responses, name) + } + sort.Strings(responses) + for _, k := range responses { + v := components.Responses[k] if err = ValidateIdentifier(k); err != nil { return } @@ -76,7 +101,13 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } - for k, v := range components.Headers { + headers := make([]string, 0, len(components.Headers)) + for name := range components.Headers { + headers = append(headers, name) + } + sort.Strings(headers) + for _, k := range headers { + v := components.Headers[k] if err = ValidateIdentifier(k); err != nil { return } @@ -85,7 +116,13 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } - for k, v := range components.SecuritySchemes { + securitySchemes := make([]string, 0, len(components.SecuritySchemes)) + for name := range components.SecuritySchemes { + securitySchemes = append(securitySchemes, name) + } + sort.Strings(securitySchemes) + for _, k := range securitySchemes { + v := components.SecuritySchemes[k] if err = ValidateIdentifier(k); err != nil { return } @@ -94,7 +131,13 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } - for k, v := range components.Examples { + examples := make([]string, 0, len(components.Examples)) + for name := range components.Examples { + examples = append(examples, name) + } + sort.Strings(examples) + for _, k := range examples { + v := components.Examples[k] if err = ValidateIdentifier(k); err != nil { return } @@ -103,7 +146,13 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } - for k, v := range components.Links { + links := make([]string, 0, len(components.Links)) + for name := range components.Links { + links = append(links, name) + } + sort.Strings(links) + for _, k := range links { + v := components.Links[k] if err = ValidateIdentifier(k); err != nil { return } @@ -112,7 +161,13 @@ func (components *Components) Validate(ctx context.Context) (err error) { } } - for k, v := range components.Callbacks { + callbacks := make([]string, 0, len(components.Callbacks)) + for name := range components.Callbacks { + callbacks = append(callbacks, name) + } + sort.Strings(callbacks) + for _, k := range callbacks { + v := components.Callbacks[k] if err = ValidateIdentifier(k); err != nil { return } diff --git a/openapi3/content.go b/openapi3/content.go index 10e3e6009..944325041 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -2,6 +2,7 @@ package openapi3 import ( "context" + "sort" "strings" ) @@ -106,7 +107,13 @@ func (content Content) Get(mime string) *MediaType { // Validate returns an error if Content does not comply with the OpenAPI spec. func (content Content) Validate(ctx context.Context) error { - for _, v := range content { + keys := make([]string, 0, len(content)) + for key := range content { + keys = append(keys, key) + } + sort.Strings(keys) + for _, k := range keys { + v := content[k] if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index e6453ecc1..7bdfaebc8 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -3,6 +3,7 @@ package openapi3 import ( "context" "fmt" + "sort" "github.com/getkin/kin-openapi/jsoninfo" ) @@ -69,7 +70,14 @@ func (encoding *Encoding) Validate(ctx context.Context) error { if encoding == nil { return nil } - for k, v := range encoding.Headers { + + headers := make([]string, 0, len(encoding.Headers)) + for k := range encoding.Headers { + headers = append(headers, k) + } + sort.Strings(headers) + for _, k := range headers { + v := encoding.Headers[k] if err := ValidateIdentifier(k); err != nil { return nil } @@ -88,7 +96,6 @@ func (encoding *Encoding) Validate(ctx context.Context) error { sm.Style == SerializationPipeDelimited && sm.Explode, sm.Style == SerializationPipeDelimited && !sm.Explode, sm.Style == SerializationDeepObject && sm.Explode: - // it is a valid default: return fmt.Errorf("serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode) } diff --git a/openapi3/issue601_test.go b/openapi3/issue601_test.go new file mode 100644 index 000000000..420ac9dc2 --- /dev/null +++ b/openapi3/issue601_test.go @@ -0,0 +1,34 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue601(t *testing.T) { + // Document is invalid: first validation error returned is because + // schema: + // example: {key: value} + // is not how schema examples are defined (but how components' examples are defined. Components are maps.) + // Correct code should be: + // schema: {example: value} + sl := NewLoader() + doc, err := sl.LoadFromFile("testdata/lxkns.yaml") + require.NoError(t, err) + + err = doc.Validate(sl.Context) + require.Contains(t, err.Error(), `invalid components: invalid schema example: Error at "/type": property "type" is missing`) + require.Contains(t, err.Error(), `| Error at "/nsid": property "nsid" is missing`) + + err = doc.Validate(sl.Context, DisableExamplesValidation()) + require.NoError(t, err) + + // Now let's remove all the invalid parts + for _, schema := range doc.Components.Schemas { + schema.Value.Example = nil + } + + err = doc.Validate(sl.Context) + require.NoError(t, err) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index eb1ebbd6c..28836f329 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -9,6 +9,7 @@ import ( "path" "path/filepath" "reflect" + "sort" "strconv" "strings" @@ -208,11 +209,19 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { return } } - for _, component := range components.Examples { + + examples := make([]string, 0, len(components.Examples)) + for name := range components.Examples { + examples = append(examples, name) + } + sort.Strings(examples) + for _, name := range examples { + component := components.Examples[name] if err = loader.resolveExampleRef(doc, component, location); err != nil { return } } + for _, component := range components.Callbacks { if err = loader.resolveCallbackRef(doc, component, location); err != nil { return @@ -587,7 +596,13 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d } for _, contentType := range value.Content { - for name, example := range contentType.Examples { + examples := make([]string, 0, len(contentType.Examples)) + for name := range contentType.Examples { + examples = append(examples, name) + } + sort.Strings(examples) + for _, name := range examples { + example := contentType.Examples[name] if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err } @@ -651,7 +666,13 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen if contentType == nil { continue } - for name, example := range contentType.Examples { + examples := make([]string, 0, len(contentType.Examples)) + for name := range contentType.Examples { + examples = append(examples, name) + } + sort.Strings(examples) + for _, name := range examples { + example := contentType.Examples[name] if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index b1a3417eb..4269a4e64 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "github.com/go-openapi/jsonpointer" @@ -82,18 +83,29 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { if err := schema.Validate(ctx); err != nil { return err } + if mediaType.Example != nil && mediaType.Examples != nil { return errors.New("example and examples are mutually exclusive") } + if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled { return nil } + if example := mediaType.Example; example != nil { if err := validateExampleValue(example, schema.Value); err != nil { return err } - } else if examples := mediaType.Examples; examples != nil { - for k, v := range examples { + } + + if examples := mediaType.Examples; examples != nil { + names := make([]string, 0, len(examples)) + for name := range examples { + names = append(names, name) + } + sort.Strings(names) + for _, k := range names { + v := examples[k] if err := v.Validate(ctx); err != nil { return fmt.Errorf("%s: %w", k, err) } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 09b6c2c64..963ef722c 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -66,70 +66,57 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { return errors.New("value of openapi must be a non-empty string") } + var wrap func(error) error // NOTE: only mention info/components/paths/... key in this func's errors. - { - wrap := func(e error) error { return fmt.Errorf("invalid components: %w", e) } - if err := doc.Components.Validate(ctx); err != nil { - return wrap(err) - } + wrap = func(e error) error { return fmt.Errorf("invalid components: %w", e) } + if err := doc.Components.Validate(ctx); err != nil { + return wrap(err) } - { - wrap := func(e error) error { return fmt.Errorf("invalid info: %w", e) } - if v := doc.Info; v != nil { - if err := v.Validate(ctx); err != nil { - return wrap(err) - } - } else { - return wrap(errors.New("must be an object")) + wrap = func(e error) error { return fmt.Errorf("invalid info: %w", e) } + if v := doc.Info; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) } + } else { + return wrap(errors.New("must be an object")) } - { - wrap := func(e error) error { return fmt.Errorf("invalid paths: %w", e) } - if v := doc.Paths; v != nil { - if err := v.Validate(ctx); err != nil { - return wrap(err) - } - } else { - return wrap(errors.New("must be an object")) + wrap = func(e error) error { return fmt.Errorf("invalid paths: %w", e) } + if v := doc.Paths; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) } + } else { + return wrap(errors.New("must be an object")) } - { - wrap := func(e error) error { return fmt.Errorf("invalid security: %w", e) } - if v := doc.Security; v != nil { - if err := v.Validate(ctx); err != nil { - return wrap(err) - } + wrap = func(e error) error { return fmt.Errorf("invalid security: %w", e) } + if v := doc.Security; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) } } - { - wrap := func(e error) error { return fmt.Errorf("invalid servers: %w", e) } - if v := doc.Servers; v != nil { - if err := v.Validate(ctx); err != nil { - return wrap(err) - } + wrap = func(e error) error { return fmt.Errorf("invalid servers: %w", e) } + if v := doc.Servers; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) } } - { - wrap := func(e error) error { return fmt.Errorf("invalid tags: %w", e) } - if v := doc.Tags; v != nil { - if err := v.Validate(ctx); err != nil { - return wrap(err) - } + wrap = func(e error) error { return fmt.Errorf("invalid tags: %w", e) } + if v := doc.Tags; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) } } - { - wrap := func(e error) error { return fmt.Errorf("invalid external docs: %w", e) } - if v := doc.ExternalDocs; v != nil { - if err := v.Validate(ctx); err != nil { - return wrap(err) - } + wrap = func(e error) error { return fmt.Errorf("invalid external docs: %w", e) } + if v := doc.ExternalDocs; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) } } diff --git a/openapi3/operation.go b/openapi3/operation.go index 58750ffbf..832339472 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -133,11 +133,13 @@ func (operation *Operation) Validate(ctx context.Context) error { return err } } + if v := operation.RequestBody; v != nil { if err := v.Validate(ctx); err != nil { return err } } + if v := operation.Responses; v != nil { if err := v.Validate(ctx); err != nil { return err @@ -145,10 +147,12 @@ func (operation *Operation) Validate(ctx context.Context) error { } else { return errors.New("value of responses must be an object") } + if v := operation.ExternalDocs; v != nil { if err := v.Validate(ctx); err != nil { return fmt.Errorf("invalid external docs: %w", err) } } + return nil } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 64092538f..6e2dbca08 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "strconv" "github.com/go-openapi/jsonpointer" @@ -70,8 +71,8 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter { // Validate returns an error if Parameters does not comply with the OpenAPI spec. func (parameters Parameters) Validate(ctx context.Context) error { dupes := make(map[string]struct{}) - for _, item := range parameters { - if v := item.Value; v != nil { + for _, parameterRef := range parameters { + if v := parameterRef.Value; v != nil { key := v.In + ":" + v.Name if _, ok := dupes[key]; ok { return fmt.Errorf("more than one %q parameter has name %q", v.In, v.Name) @@ -79,7 +80,7 @@ func (parameters Parameters) Validate(ctx context.Context) error { dupes[key] = struct{}{} } - if err := item.Validate(ctx); err != nil { + if err := parameterRef.Validate(ctx); err != nil { return err } } @@ -325,7 +326,13 @@ func (parameter *Parameter) Validate(ctx context.Context) error { return err } } else if examples := parameter.Examples; examples != nil { - for k, v := range examples { + names := make([]string, 0, len(examples)) + for name := range examples { + names = append(names, name) + } + sort.Strings(names) + for _, k := range names { + v := examples[k] if err := v.Validate(ctx); err != nil { return fmt.Errorf("%s: %w", k, err) } diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 6a8cc7336..4801e5f83 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "sort" "github.com/getkin/kin-openapi/jsoninfo" ) @@ -123,7 +124,15 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { // Validate returns an error if PathItem does not comply with the OpenAPI spec. func (pathItem *PathItem) Validate(ctx context.Context) error { - for _, operation := range pathItem.Operations() { + operations := pathItem.Operations() + + methods := make([]string, 0, len(operations)) + for method := range operations { + methods = append(methods, method) + } + sort.Strings(methods) + for _, method := range methods { + operation := operations[method] if err := operation.Validate(ctx); err != nil { return err } diff --git a/openapi3/paths.go b/openapi3/paths.go index be7f3dc42..b116f6cb6 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -3,6 +3,7 @@ package openapi3 import ( "context" "fmt" + "sort" "strings" ) @@ -12,8 +13,15 @@ type Paths map[string]*PathItem // Validate returns an error if Paths does not comply with the OpenAPI spec. func (paths Paths) Validate(ctx context.Context) error { - normalizedPaths := make(map[string]string) - for path, pathItem := range paths { + normalizedPaths := make(map[string]string, len(paths)) + + keys := make([]string, 0, len(paths)) + for key := range paths { + keys = append(keys, key) + } + sort.Strings(keys) + for _, path := range keys { + pathItem := paths[path] if path == "" || path[0] != '/' { return fmt.Errorf("path %q does not start with a forward slash (/)", path) } @@ -37,7 +45,14 @@ func (paths Paths) Validate(ctx context.Context) error { } } } - for method, operation := range pathItem.Operations() { + operations := pathItem.Operations() + methods := make([]string, 0, len(operations)) + for method := range operations { + methods = append(methods, method) + } + sort.Strings(methods) + for _, method := range methods { + operation := operations[method] var setParams []string for _, parameterRef := range operation.Parameters { if parameterRef != nil { diff --git a/openapi3/response.go b/openapi3/response.go index 31ea257d1..62361ad74 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "strconv" "github.com/go-openapi/jsonpointer" @@ -36,7 +37,14 @@ func (responses Responses) Validate(ctx context.Context) error { if len(responses) == 0 { return errors.New("the responses object MUST contain at least one response code") } - for _, v := range responses { + + keys := make([]string, 0, len(responses)) + for key := range responses { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + v := responses[key] if err := v.Validate(ctx); err != nil { return err } @@ -113,13 +121,26 @@ func (response *Response) Validate(ctx context.Context) error { return err } } - for _, header := range response.Headers { + + headers := make([]string, 0, len(response.Headers)) + for name := range response.Headers { + headers = append(headers, name) + } + sort.Strings(headers) + for _, name := range headers { + header := response.Headers[name] if err := header.Validate(ctx); err != nil { return err } } - for _, link := range response.Links { + links := make([]string, 0, len(response.Links)) + for name := range response.Links { + links = append(links, name) + } + sort.Strings(links) + for _, name := range links { + link := response.Links[name] if err := link.Validate(ctx); err != nil { return err } diff --git a/openapi3/schema.go b/openapi3/schema.go index 295180fdd..e9b6617fe 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -9,6 +9,7 @@ import ( "math" "math/big" "regexp" + "sort" "strconv" "unicode/utf16" @@ -728,7 +729,13 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } - for _, ref := range schema.Properties { + properties := make([]string, 0, len(schema.Properties)) + for name := range schema.Properties { + properties = append(properties, name) + } + sort.Strings(properties) + for _, name := range properties { + ref := schema.Properties[name] v := ref.Value if v == nil { return foundUnresolvedRef(ref.Ref) @@ -1442,7 +1449,13 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value } if settings.asreq || settings.asrep { - for propName, propSchema := range schema.Properties { + properties := make([]string, 0, len(schema.Properties)) + for propName := range schema.Properties { + properties = append(properties, propName) + } + sort.Strings(properties) + for _, propName := range properties { + propSchema := schema.Properties[propName] if value[propName] == nil { if dlft := propSchema.Value.Default; dlft != nil { value[propName] = dlft @@ -1499,7 +1512,13 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value if ref := schema.AdditionalProperties; ref != nil { additionalProperties = ref.Value } - for k, v := range value { + keys := make([]string, 0, len(value)) + for k := range value { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := value[k] if properties != nil { propertyRef := properties[k] if propertyRef != nil { diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index 42d832552..592997505 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -17,8 +17,8 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) * // Validate returns an error if SecurityRequirements does not comply with the OpenAPI spec. func (srs SecurityRequirements) Validate(ctx context.Context) error { - for _, item := range srs { - if err := item.Validate(ctx); err != nil { + for _, security := range srs { + if err := security.Validate(ctx); err != nil { return err } } diff --git a/openapi3/server.go b/openapi3/server.go index 478f8ffb7..88fdcc0f3 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "net/url" + "sort" "strings" "github.com/getkin/kin-openapi/jsoninfo" @@ -134,15 +135,24 @@ func (server *Server) Validate(ctx context.Context) (err error) { if server.URL == "" { return errors.New("value of url must be a non-empty string") } + opening, closing := strings.Count(server.URL, "{"), strings.Count(server.URL, "}") if opening != closing { return errors.New("server URL has mismatched { and }") } + if opening != len(server.Variables) { return errors.New("server has undeclared variables") } - for name, v := range server.Variables { - if !strings.Contains(server.URL, fmt.Sprintf("{%s}", name)) { + + variables := make([]string, 0, len(server.Variables)) + for name := range server.Variables { + variables = append(variables, name) + } + sort.Strings(variables) + for _, name := range variables { + v := server.Variables[name] + if !strings.Contains(server.URL, "{"+name+"}") { return errors.New("server has undeclared variables") } if err = v.Validate(ctx); err != nil { diff --git a/openapi3/testdata/lxkns.yaml b/openapi3/testdata/lxkns.yaml new file mode 100644 index 000000000..e8400592c --- /dev/null +++ b/openapi3/testdata/lxkns.yaml @@ -0,0 +1,988 @@ +# https://raw.githubusercontent.com/thediveo/lxkns/71e8fb5e40c612ecc89d972d211221137e92d5f0/api/openapi-spec/lxkns.yaml +openapi: 3.0.2 +security: + - {} +info: + title: lxkns + version: 0.22.0 + description: |- + Discover Linux-kernel namespaces, almost everywhere in a Linux host. Also look + for mount points and their hierarchy, as well as for containers. + contact: + url: 'https://github.com/thediveo/lxkns' + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +servers: + - + url: /api + description: lxkns as-a-service +paths: + /processes: + summary: Process discovery + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessTable' + description: |- + Returns information about all processes and their position within the process + tree. + summary: Linux processes + description: |- + Map of all processes in the process tree, with the keys being the PIDs in + decimal string format. + /pidmap: + summary: Discover the translation of PIDs between PID namespaces + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PIDMap' + description: |- + The namespaced PIDs of processes. For each process, the PIDs in their PID + namespaces along the PID namespace hierarchy are returned. + summary: PID translation data + description: | + Discovers the PIDs that processes have in different PID namespaces, + according to the hierarchy of PID namespaces. + + > **IMPORTANT:** The order of processes is undefined. However, the order of + > the namespaced PIDs of a particular process is well-defined. + /namespaces: + summary: Namespace discovery (includes process discovery for technical reasons) + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DiscoveryResult' + description: The discovered namespaces and processes. + summary: Linux kernel namespaces + description: |- + Information about the Linux-kernel namespaces and how they relate to processes + and vice versa. +components: + schemas: + PIDMap: + title: Root Type for PIDMap + description: |- + A "map" of the PIDs of processes in PID namespaces for translating a specific + PID from one PID namespace into another PID namespace. + + > **IMPORTANT:** The order of *processes* is undefined. However, the order of + > the namespaced PIDs of a particular process is well-defined: from the PID in + > the process' own PID namespace up the hierarchy to the PID in the initial + > PID namespace. + + The PID map is represented in a "condensed" format, which is designed to + minimize transfer volume. Consuming applications thus might want to transfer + this external representation into a performance-optimized internal + representation, optimized for translating PIDs. + type: array + items: + $ref: '#/components/schemas/NamespacedPIDs' + example: + - + - + pid: 12345 + nsid: 4026531905 + - + pid: 1 + nsid: 4026538371 + - + - + pid: 666 + nsid: 4026538371 + NamespacedPID: + title: Root Type for NamespacedPID + description: |- + A process identifier (PID) valid only in the accompanying PID namespace, + referenced by the ID (inode number) of the PID namespace. Outside that PID + namespace the PID is invalid and might be confused with some other process that + happens to have the same PID in the other PID namespace. For instance, PID 1 + can be found not only in the initial PID namespace, but usually also in all + other PID namespaces, but referencing completely different processes each time. + required: + - pid + - nsid + type: object + properties: + pid: + description: a process identifier + type: integer + nsid: + format: int64 + description: |- + a PID namespace identified and referenced by its inode number (without any + device number). + type: integer + example: + pid: 1 + nsid: 4026531905 + NamespacedPIDs: + description: |- + The list of namespaced PIDs of a process, ordered according to the PID + namespace hierarchy the process is in. The order is from the "bottom-most" PID + namespace a particular process is joined to up to the initial PID namespace. + Thus, the PID in the initial PID namespace always comes last. + type: array + items: + $ref: '#/components/schemas/NamespacedPID' + example: + - + pid: 12345 + nsid: 4026531905 + - + pid: 1 + nsid: 4026532382 + Process: + description: |- + Information about a specific process, such as its PID, name, and command line + arguments, the references (IDs) of the namespaces the process is joined to. + required: + - pid + - ppid + - name + - cmdline + - starttime + - namespaces + - cpucgroup + # - fridgecgroup + # - fridgefrozen + type: object + properties: + pid: + format: int32 + description: The process identifier (PID) of this process. + type: integer + ppid: + format: int32 + description: |- + The PID of the parent process, or 0 if there is no parent process. On Linux, the + only processes without a parent are the initial process PID 1 and the PID 2 + kthreadd kernel threads "process". + type: integer + name: + description: |- + A synthesized name of the process: + - a name set by the process itself, + - a name derived from the command line of the process. + type: string + cmdline: + description: |- + The command line arguments of the process, including the process binary file + name. Taken from /proc/$PID/cmdline, see also + [https://man7.org/linux/man-pages/man5/proc.5.html](proc(5)). + type: array + items: + type: string + starttime: + format: int64 + description: |- + The time this process started after system boot and expressed in clock ticks. + It is taken from /proc/$PID/stat, see also + [https://man7.org/linux/man-pages/man5/proc.5.html](proc(5)). + type: integer + cpucgroup: + description: |- + The (CPU) cgroup (control group) path name in the hierarchy this process is in. The + path name does not specify the root mount path of the complete hierarchy, but + only the (pseudo) absolute path starting from the root of the particular (v1) or + unified (v2) cgroup hierarchy. + type: string + namespaces: + $ref: '#/components/schemas/NamespacesSet' + description: |- + References the namespaces this process is joined to, in form of the namespace + IDs (inode numbers). + fridgecgroup: + description: The freezer cgroup path name in the hierarchy this process is in. + type: string + fridgefrozen: + description: The effective freezer state of this process. + type: boolean + example: + namespaces: + mnt: 4026531840 + cgroup: 4026531835 + uts: 4026531838 + ipc: 4026531839 + user: 4026531837 + pid: 4026531836 + net: 4026531905 + pid: 1 + ppid: 0 + name: systemd + cmdline: + - /sbin/init + - fixrtc + - splash + starttime: 0 + cpucgroup: /init.scope + ProcessTable: + description: |- + Information about all processes in the process tree, with each process item + being keyed by its PID in string form. Besides information about the process + itself and its position in the process tree, the processes also reference the + namespaces they are currently joined to. + type: object + additionalProperties: + $ref: '#/components/schemas/Process' + example: + '1': + namespaces: + mnt: 4026531840 + cgroup: 4026531835 + uts: 4026531838 + ipc: 4026531839 + user: 4026531837 + pid: 4026531836 + net: 4026531905 + pid: 1 + ppid: 0 + name: systemd + cmdline: + - /sbin/init + - fixrtc + - splash + starttime: 0 + cpucgroup: /init.scope + '137024': + namespaces: + mnt: 4026532517 + cgroup: 4026531835 + uts: 4026531838 + ipc: 4026531839 + user: 4026532518 + pid: 4026531836 + net: 4026531905 + pid: 137024 + ppid: 1 + name: upowerd + cmdline: + - /usr/lib/upower/upowerd + starttime: 3132568 + cpucgroup: /system.slice/upower.service + DiscoveryResult: + description: |- + The discovered namespaces and processes with their mutual relationships, and + optionally PID translation data. + required: + - namespaces + - processes + - containers + - container-engines + - container-groups + type: object + properties: + processes: + $ref: '#/components/schemas/ProcessTable' + description: 'Information about all processes, including the process hierarchy.' + namespaces: + $ref: '#/components/schemas/NamespacesDict' + description: Map of namespaces. + pidmap: + $ref: '#/components/schemas/PIDMap' + description: Data for translating PIDs between different PID namespaces. + options: + $ref: '#/components/schemas/DiscoveryOptions' + description: The options specified for discovery. + mounts: + $ref: '#/components/schemas/NamespacedMountPaths' + description: Map of mount namespace'd mount paths with mount points. + containers: + $ref: '#/components/schemas/ContainerMap' + description: Discovered containers. + container-engines: + $ref: '#/components/schemas/ContainerEngineMap' + description: Container engines managing the discovered containers. + container-groups: + $ref: '#/components/schemas/ContainerGroupMap' + description: Groups of containers. + example: + discovery-options: + skipped-procs: false + skipped-tasks: false + skipped-fds: false + skipped-bindmounts: false + skipped-hierarchy: false + skipped-ownership: false + skipped-freezer: false + scanned-namespace-types: + - time + - mnt + - cgroup + - uts + - ipc + - user + - pid + - net + namespaces: + '4026531835': + nsid: 4026531835 + type: cgroup + owner: 4026531837 + reference: /proc/2/ns/cgroup + leaders: + - 2 + - 1 + '4026531836': + nsid: 4026531836 + type: pid + owner: 4026531837 + reference: /proc/2/ns/pid + leaders: + - 2 + - 1 + children: + - 4026532338 + '4026531837': + nsid: 4026531837 + type: user + reference: /proc/1/ns/user + leaders: + - 1 + - 2 + children: + - 4026532518 + user-id: 0 + '4026531838': + nsid: 4026531838 + type: uts + owner: 4026531837 + reference: /proc/2/ns/uts + leaders: + - 2 + - 1 + '4026531839': + nsid: 4026531839 + type: ipc + owner: 4026531837 + reference: /proc/2/ns/ipc + leaders: + - 2 + - 1 + '4026532268': + nsid: 4026532268 + type: mnt + owner: 4026531837 + reference: /proc/1761/ns/mnt + leaders: + - 1761 + '4026532324': + nsid: 4026532324 + type: uts + owner: 4026531837 + reference: /proc/1781/ns/uts + leaders: + - 1781 + '4026532337': + nsid: 4026532337 + type: ipc + owner: 4026531837 + reference: /proc/33536/ns/ipc + leaders: + - 33536 + '4026532340': + nsid: 4026532340 + type: net + owner: 4026531837 + reference: /proc/33536/ns/net + leaders: + - 33536 + '4026532398': + nsid: 4026532398 + type: pid + owner: 4026531837 + reference: /proc/34110/ns/pid + leaders: + - 34110 + parent: 4026532338 + '4026532400': + nsid: 4026532400 + type: net + owner: 4026531837 + reference: /proc/34110/ns/net + leaders: + - 34110 + '4026532517': + nsid: 4026532517 + type: mnt + owner: 4026531837 + reference: /proc/137024/ns/mnt + leaders: + - 137024 + '4026532518': + nsid: 4026532518 + type: user + reference: /proc/137024/ns/user + leaders: + - 137024 + parent: 4026531837 + user-id: 0 + processes: + '1': + namespaces: + mnt: 4026531840 + cgroup: 4026531835 + uts: 4026531838 + ipc: 4026531839 + user: 4026531837 + pid: 4026531836 + net: 4026531905 + pid: 1 + ppid: 0 + name: systemd + cmdline: + - /sbin/init + - fixrtc + - splash + starttime: 0 + cpucgroup: /init.scope + '17': + namespaces: + mnt: 4026531840 + cgroup: 4026531835 + uts: 4026531838 + ipc: 4026531839 + user: 4026531837 + pid: 4026531836 + net: 4026531905 + pid: 17 + ppid: 2 + name: migration/1 + cmdline: + - '' + starttime: 0 + cpucgroup: '' + '1692': + namespaces: + mnt: 4026532246 + cgroup: 4026531835 + uts: 4026532247 + ipc: 4026531839 + user: 4026531837 + pid: 4026531836 + net: 4026531905 + pid: 1692 + ppid: 1 + name: systemd-timesyn + cmdline: + - /lib/systemd/systemd-timesyncd + starttime: 2032 + cpucgroup: /system.slice/systemd-timesyncd.service + Namespace: + description: |- + Information about a single Linux-kernel namespace. Depending on the extent of + the discovery, not all namespace types might have been discovered, or data might + be missing about the PID and user namespace hierarchies as well as which user + namespace owns other namespaces. + + For more details, please see also: + https://man7.org/linux/man-pages/man7/namespaces.7.html. + required: + - type + - nsid + type: object + properties: + nsid: + format: int64 + description: |- + Identifier of this namespace: an inode number. + + - lxkns only uses the inode number in the API, following current Linux kernel + and CLI tool practise, which generally identify individual namespaces only by + inode numbers (and leaving out the device number). + - Namespace identifiers are not UUIDs, but instead reused by the kernel after a + namespace has been destroyed. + type: integer + type: + $ref: '#/components/schemas/NamespaceType' + description: Type of this namespace. + owner: + format: int64 + description: The ID of the owning user namespace. + type: integer + reference: + description: |- + File system reference to the namespace, if available. The hierarchical PID and + user namespaces can also exist without any file system references, as long as + there are still child namespaces present for such a PID or user namespace. + type: array + items: + type: string + leaders: + description: |- + List of PIDs of "leader" processes joined to this namespace. + + Instead of listing all processes joined to this namespace, lxkns only lists the + "most senior" processes: these processes are the highest processes in the + process tree still joined to a namespace. Child processes also joined to this + namespace can then be found using the child process relations from the process + table information. + type: array + items: + format: int32 + type: integer + ealdorman: + format: int32 + description: PID of the most senior leader process joined to this namespace. + type: integer + parent: + format: int64 + description: 'Only for PID and user namespaces: the ID of the parent namespace.' + type: integer + user-id: + description: |- + Only for user namespaces: the UID of the Linux user who created this user + namespace. + type: integer + user-name: + description: |- + Only for user namespaces: the name of the Linux user who created this user + namespace. + type: string + children: + description: 'For user and PID namespaces: the list of child namespace IDs.' + type: array + items: + format: int64 + type: integer + possessions: + description: 'Only user namespaces: list of namespace IDs of owned (non-user) namespaces.' + type: array + items: + format: int64 + type: integer + example: + '4026532338': + nsid: 4026532338 + type: pid + owner: 4026531837 + reference: /proc/33536/ns/pid + leaders: + - 33536 + parent: 4026531836 + children: + - 4026532398 + NamespaceType: + description: |- + Type of Linux-kernel namespace. For more information about namespaces, please + see also: https://man7.org/linux/man-pages/man7/namespaces.7.html. + enum: + - cgroup + - ipc + - net + - mnt + - pid + - user + - uts + - time + type: string + example: 'net' + NamespacesDict: + description: | + "Dictionary" or "map" of Linux-kernel namespaces, keyed by their namespace IDs in stringified + form. Contrary to what the term "namespace" might suggest, namespaces do not + have names but are identified by their (transient) inode numbers. + + > **Note:** following current best practice of the Linux kernel and CLI tools, + > namespace references are only in the form of the inode number, without the + > device number. + + For further details, please see also: + https://man7.org/linux/man-pages/man7/namespaces.7.html. + type: object + additionalProperties: + $ref: '#/components/schemas/Namespace' + example: + '4026532267': + nsid: 4026532267 + type: mnt + owner: 4026531837 + reference: /proc/1714/ns/mnt + leaders: + - 1714 + '4026532268': + nsid: 4026532268 + type: mnt + owner: 4026531837 + reference: /proc/1761/ns/mnt + leaders: + - 1761 + DiscoveryOptions: + title: Root Type for DiscoveryOptions + description: '' + required: + - scanned-namespace-types + type: object + properties: + from-procs: + type: boolean + from-tasks: + type: boolean + from-fds: + type: boolean + from-bindmounts: + type: boolean + with-hierarchy: + type: boolean + with-ownership: + type: boolean + with-freezer: + description: |- + true if the discovery of the (effective) freezer states of processes has been + skipped, so that all processes always appear to be "thawed" (running). + type: boolean + scanned-namespace-types: + description: |- + List of namespace types included in the discovery. This information might help + consuming tools to understand which types of namespaces were scanned and which + were not scanned for at all. + type: array + items: + $ref: '#/components/schemas/NamespaceType' + with-mounts: + description: true if mount namespace'd mount paths with mount points were discovered. + type: boolean + labels: + description: |- + Dictionary of key=value pairs passed to decorators to optionally control the + decoration of discovered containers. + example: + skipped-procs: false + skipped-tasks: false + skipped-fds: false + skipped-bindmounts: false + skipped-hierarchy: false + skipped-ownership: false + skipped-freezer: false + scanned-namespace-types: + - time + - mnt + - cgroup + - uts + - ipc + - user + - pid + - net + NamespacesSet: + description: |- + The set of 7 namespaces (8 namespaces since Linux 5.6+) every process is always + joined to. The namespaces are referenced by their IDs (inode numbers): + - cgroup namespace + - IPC namespace + - network namespace + - mount namespace + - PID namespace + - user namespace + - UTS namespace + - time namespace (Linux kernel 5.6+) + + > **Note:** Since lxkns doesn't officially support Linux kernels before 4.9 + > all namespaces except the "time" namespace can safely be assumed to be + > always present. + + For more details about namespaces, please see also: + https://man7.org/linux/man-pages/man7/namespaces.7.html. + type: object + properties: + cgroup: + format: int64 + description: |- + References a cgroup namespace by ID (inode number). Please see also: + https://www.man7.org/linux/man-pages/man7/cgroup_namespaces.7.html. + type: integer + ipc: + format: int64 + description: |- + References an IPC namespace by ID (inode number). Please see also: + https://www.man7.org/linux/man-pages/man7/ipc_namespaces.7.html. + type: integer + net: + format: int64 + description: |- + References a network namespace by ID (inode number). Please see also: + https://www.man7.org/linux/man-pages/man7/network_namespaces.7.html. + type: integer + mnt: + format: int64 + description: |- + References a mount namespace by ID (inode number). Please see also: + https://www.man7.org/linux/man-pages/man7/mount_namespaces.7.html. + type: integer + pid: + format: int64 + description: |- + References a PID namespace by ID (inode number). Please see also: + https://www.man7.org/linux/man-pages/man7/pid_namespaces.7.html. + type: integer + user: + format: int64 + description: |- + References a user namespace by ID (inode number). Please see also: + https://www.man7.org/linux/man-pages/man7/user_namespaces.7.html. + type: integer + uts: + format: int64 + description: |- + References a UTS (*nix timesharing system) namespace by ID (inode number). + Please see also: https://www.man7.org/linux/man-pages/man7/uts_namespaces.7.html. + type: integer + time: + format: int64 + description: |- + References a (monotonous) time namespace by ID (inode number). Time namespaces + are only supported on Linux kernels 5.6 or later. Please see also: + https://www.man7.org/linux/man-pages/man7/time_namespaces.7.html. + type: integer + example: + mnt: 4026531840 + cgroup: 4026531835 + uts: 4026531838 + ipc: 4026531839 + user: 4026531837 + pid: 4026531836 + net: 4026531905 + MountPoint: + description: |- + Information about a mount point as discovered from the proc filesystem. See also + [proc(5)](https://man7.org/linux/man-pages/man5/procfs.5.html), and details about + `/proc/[PID]/mountinfo` in particular. + required: + - mountid + - parentid + - major + - minor + - root + - mountpoint + - mountoptions + - tags + - source + - fstype + - superoptions + - hidden + type: object + properties: + parentid: + description: |- + ID of the parent mount. Please note that the parent mount might be outside a + mount namespace. + type: integer + mountid: + description: 'unique ID for the mount, might be reused after umount(2).' + type: integer + major: + description: major ID for the st_dev for files on this filesystem. + type: integer + minor: + description: minor ID for the st_dev for filed on this filesystem. + type: integer + root: + description: pathname of the directory in the filesystem which forms the root of this mount. + type: string + mountpoint: + description: pathname of the mount point relative to root directory of the process. + type: string + mountoptions: + description: mount options specific to this mount. + type: array + items: + type: string + tags: + $ref: '#/components/schemas/MountTags' + description: |- + optional tags with even more optional values. Tags cannot be a single hyphen + "-". + fstype: + description: 'filesystem type in the form "type[.subtype]".' + type: string + source: + description: filesystem-specific information or "none". + type: string + superoptions: + description: per-superblock options. + type: string + hidden: + description: |- + true if this mount point is hidden by an "overmount" either at the same mount + path or higher up the path hierarchy. + type: boolean + MountTags: + description: |- + dictionary of mount point tags with optional values. Tag names cannot be a single + hyphen "-". + type: object + additionalProperties: + type: string + MountPath: + description: |- + path of one or more mount points in the Virtual File System (VFS). In case of + multiple mount points at the same path, only at most one of them can be visible + and all others (or all in case of an overmount higher up the path) will be hidden. + required: + - mounts + - pathid + - parentid + type: object + properties: + mounts: + description: one or more mount points at this path in the Virtual File System (VFS). + type: array + items: + $ref: '#/components/schemas/MountPoint' + pathid: + description: 'unique mount path identifier, per mount namespace.' + type: integer + parentid: + description: 'identifier of parent mount path, if any, otherwise 0.' + type: integer + MountPathsDict: + description: |- + "Dictionary" or "map" of mount paths with their corresponding mount points, keyed + by the mount paths. + + Please note that additionally the mount path entries are organized in a "sparse" + hierarchy with the help of mount path identifiers (these are user-space generated + by lxkns). + type: object + additionalProperties: + $ref: '#/components/schemas/MountPath' + NamespacedMountPaths: + description: 'the mount paths of each discovered mount namespace, separated by mount namespace.' + type: object + additionalProperties: + $ref: '#/components/schemas/MountPathsDict' + Container: + description: 'Alive container with process(es), either running or paused.' + required: + - id + - name + - type + - flavor + - pid + - paused + - labels + - groups + - engine + type: object + properties: + id: + description: Container identifier + type: string + name: + description: 'Container name as opposed to its id, might be the same for some container engines.' + type: string + type: + description: 'Type of container identifier, such as "docker.com", et cetera.' + type: string + flavor: + description: 'Flavor of container, might be the same as the type or different.' + type: string + pid: + description: Process ID of initial container process. + type: integer + paused: + description: Indicates whether the container is running or paused. + type: boolean + labels: + $ref: '#/components/schemas/Labels' + description: Label name=value pairs attached to this container. + groups: + description: |- + List of group reference identifiers this container is a member of. For instance, + (Docker) composer projects, Kubernetes pods, ... + type: array + items: + type: integer + engine: + description: Reference identifier of the container engine managing this container. + type: integer + Labels: + description: 'Dictionary (map) of KEY=VALUE pairs, with KEY and VALUE both strings.' + type: object + additionalProperties: + type: string + ContainerEngine: + description: Information about a container engine managing a set of discovered containers. + required: + - id + - type + - version + - api + - pid + - containers + type: object + properties: + id: + description: 'Container engine instance identifier, such as UUID, unique string, et cetera.' + type: string + type: + description: 'Engine type identifier, such as "containerd.io", et cetera.' + type: string + version: + description: 'Engine version information.' + type: string + api: + description: Engine API path. + type: string + pid: + description: 'Engine''s PID (in initial PID namespace) when known, otherwise zero.' + type: integer + containers: + description: List of reference IDs (=PIDs) of containers managed by this engine. + type: array + items: + type: integer + ContainerGroup: + description: A group of containers somehow related. + required: + - name + - type + - flavor + - containers + - labels + type: object + properties: + name: + description: |- + Name of group, such as a (Docker) composer project name, Kubernetes pod + namespace/name, et cetera. + type: string + type: + description: Group type identifier. + type: string + flavor: + description: 'Group flavor identifier, might be identical with group type identifier.' + type: string + containers: + description: List of reference IDs (=PIDs) of containers belonging to this group. + type: array + items: + type: integer + labels: + $ref: '#/components/schemas/Labels' + description: Additional KEY=VALUE information. + ContainerMap: + description: |- + Maps container PIDs to containers. Container PIDs are the PIDs of initial + container processes only, but not any child processes. + type: object + additionalProperties: + $ref: '#/components/schemas/Container' + ContainerEngineMap: + description: Maps reference IDs to container engines. + type: object + additionalProperties: + $ref: '#/components/schemas/ContainerEngine' + ContainerGroupMap: + description: Maps reference IDs to container groups. + type: object + additionalProperties: + $ref: '#/components/schemas/ContainerGroup' From fa5d9a9b251edabf09f20bebd8aebba923c4da44 Mon Sep 17 00:00:00 2001 From: Praneet Loke <1466314+praneetloke@users.noreply.github.com> Date: Thu, 22 Sep 2022 11:02:45 -0700 Subject: [PATCH 183/260] Improve error message when path validation fails (#605) --- openapi3/example_validation_test.go | 4 ++-- openapi3/loader_test.go | 2 +- openapi3/path_item.go | 2 +- openapi3/paths.go | 2 +- openapi3/response_issue224_test.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index 85e158e6b..177360c13 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -26,7 +26,7 @@ func TestExamplesSchemaValidation(t *testing.T) { param1example: value: abcd `, - errContains: "invalid paths: param1example", + errContains: "invalid paths: invalid path /user: invalid operation POST: param1example", }, { name: "valid_parameter_examples", @@ -64,7 +64,7 @@ func TestExamplesSchemaValidation(t *testing.T) { email: bad password: short `, - errContains: "invalid paths: BadUser", + errContains: "invalid paths: invalid path /user: invalid operation POST: BadUser", }, { name: "valid_component_examples", diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 64a923c39..d492e2471 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -96,7 +96,7 @@ func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) { doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) - require.EqualError(t, err, `invalid paths: found unresolved ref: ""`) + require.EqualError(t, err, `invalid paths: invalid path /foo: invalid operation POST: found unresolved ref: ""`) } func TestResolveResponseExampleRef(t *testing.T) { diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 4801e5f83..940b1592a 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -134,7 +134,7 @@ func (pathItem *PathItem) Validate(ctx context.Context) error { for _, method := range methods { operation := operations[method] if err := operation.Validate(ctx); err != nil { - return err + return fmt.Errorf("invalid operation %s: %v", method, err) } } return nil diff --git a/openapi3/paths.go b/openapi3/paths.go index b116f6cb6..e3da7d05b 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -96,7 +96,7 @@ func (paths Paths) Validate(ctx context.Context) error { } if err := pathItem.Validate(ctx); err != nil { - return err + return fmt.Errorf("invalid path %s: %v", path, err) } } diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go index 97c7e6b20..5de0525e4 100644 --- a/openapi3/response_issue224_test.go +++ b/openapi3/response_issue224_test.go @@ -457,5 +457,5 @@ func TestEmptyResponsesAreInvalid(t *testing.T) { require.NoError(t, err) require.Equal(t, doc.ExternalDocs.Description, "See AsyncAPI example") err = doc.Validate(context.Background()) - require.EqualError(t, err, `invalid paths: the responses object MUST contain at least one response code`) + require.EqualError(t, err, `invalid paths: invalid path /pet: invalid operation POST: the responses object MUST contain at least one response code`) } From db526731fa018d23eb9778fd4c651049b2e82648 Mon Sep 17 00:00:00 2001 From: Davor Sauer Date: Fri, 7 Oct 2022 14:38:50 +0200 Subject: [PATCH 184/260] Correctly resolve path of yaml resource if double referenced. (#611) --- openapi3/internalize_refs_test.go | 16 ++++----- openapi3/loader.go | 9 ++++- openapi3/loader_recursive_ref_test.go | 2 ++ .../recursiveRef/components/models/error.yaml | 2 ++ openapi3/testdata/recursiveRef/openapi.yml | 16 +++++++++ .../recursiveRef/openapi.yml.internalized.yml | 34 +++++++++++++++++++ openapi3/testdata/recursiveRef/paths/foo.yml | 2 ++ 7 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 openapi3/testdata/recursiveRef/components/models/error.yaml diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go index b1ca846d2..fe6b29d90 100644 --- a/openapi3/internalize_refs_test.go +++ b/openapi3/internalize_refs_test.go @@ -2,7 +2,7 @@ package openapi3 import ( "context" - "io/ioutil" + "os" "regexp" "testing" @@ -41,25 +41,25 @@ func TestInternalizeRefs(t *testing.T) { err = doc.Validate(ctx) require.NoError(t, err, "validating internalized spec") - data, err := doc.MarshalJSON() + actual, err := doc.MarshalJSON() require.NoError(t, err, "marshalling internalized spec") // run a static check over the file, making sure each occurence of a // reference is followed by a # - numRefs := len(regexpRef.FindAll(data, -1)) - numInternalRefs := len(regexpRefInternal.FindAll(data, -1)) + numRefs := len(regexpRef.FindAll(actual, -1)) + numInternalRefs := len(regexpRefInternal.FindAll(actual, -1)) require.Equal(t, numRefs, numInternalRefs, "checking all references are internal") - // load from data, but with the path set to the current directory - doc2, err := sl.LoadFromData(data) + // load from actual, but with the path set to the current directory + doc2, err := sl.LoadFromData(actual) require.NoError(t, err, "reloading spec") err = doc2.Validate(ctx) require.NoError(t, err, "validating reloaded spec") // compare with expected - data0, err := ioutil.ReadFile(test.filename + ".internalized.yml") + expected, err := os.ReadFile(test.filename + ".internalized.yml") require.NoError(t, err) - require.JSONEq(t, string(data), string(data0)) + require.JSONEq(t, string(expected), string(actual)) }) } } diff --git a/openapi3/loader.go b/openapi3/loader.go index 28836f329..9ed8e3076 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -36,7 +36,8 @@ type Loader struct { Context context.Context - rootDir string + rootDir string + rootLocation string visitedPathItemRefs map[string]struct{} @@ -148,6 +149,7 @@ func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T, func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.URL) (*T, error) { if loader.visitedDocuments == nil { loader.visitedDocuments = make(map[string]*T) + loader.rootLocation = location.Path } uri := location.String() if doc, ok := loader.visitedDocuments[uri]; ok { @@ -420,6 +422,11 @@ func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef if loader.rootDir == "" { return current } + + if resolvedRef == "" { + return &url.URL{Path: loader.rootLocation} + } + return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)} } diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go index 5b7c1506e..924cb6be8 100644 --- a/openapi3/loader_recursive_ref_test.go +++ b/openapi3/loader_recursive_ref_test.go @@ -14,6 +14,8 @@ func TestLoaderSupportsRecursiveReference(t *testing.T) { 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) } func TestIssue447(t *testing.T) { diff --git a/openapi3/testdata/recursiveRef/components/models/error.yaml b/openapi3/testdata/recursiveRef/components/models/error.yaml new file mode 100644 index 000000000..b4d404793 --- /dev/null +++ b/openapi3/testdata/recursiveRef/components/models/error.yaml @@ -0,0 +1,2 @@ +type: object +title: ErrorDetails diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml index 675722a60..9f884c710 100644 --- a/openapi3/testdata/recursiveRef/openapi.yml +++ b/openapi3/testdata/recursiveRef/openapi.yml @@ -5,6 +5,13 @@ info: paths: /foo: $ref: ./paths/foo.yml + /double-ref-foo: + get: + summary: Double ref response + description: Reference response with double reference. + responses: + "400": + $ref: "#/components/responses/400" components: schemas: Foo: @@ -15,3 +22,12 @@ components: $ref: ./components/Bar.yml Cat: $ref: ./components/Cat.yml + Error: + $ref: ./components/models/error.yaml + responses: + "400": + description: 400 Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" diff --git a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml index 073059025..0d508527a 100644 --- a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml +++ b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml @@ -9,11 +9,27 @@ } } }, + "responses": { + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "400 Bad Request" + } + }, "schemas": { "Bar": { "example": "bar", "type": "string" }, + "Error":{ + "title":"ErrorDetails", + "type":"object" + }, "Foo": { "properties": { "bar": { @@ -30,6 +46,10 @@ }, "type": "object" }, + "error":{ + "title":"ErrorDetails", + "type":"object" + }, "Cat": { "properties": { "cat": { @@ -46,6 +66,17 @@ }, "openapi": "3.0.3", "paths": { + "/double-ref-foo": { + "get": { + "description": "Reference response with double reference.", + "responses": { + "400": { + "$ref": "#/components/responses/400" + } + }, + "summary": "Double ref response" + } + }, "/foo": { "get": { "responses": { @@ -63,6 +94,9 @@ } }, "description": "OK" + }, + "400": { + "$ref": "#/components/responses/400" } } }, diff --git a/openapi3/testdata/recursiveRef/paths/foo.yml b/openapi3/testdata/recursiveRef/paths/foo.yml index 43e03b7ab..4c845b532 100644 --- a/openapi3/testdata/recursiveRef/paths/foo.yml +++ b/openapi3/testdata/recursiveRef/paths/foo.yml @@ -11,3 +11,5 @@ get: properties: foo2: $ref: ../openapi.yml#/components/schemas/Foo2 + "400": + $ref: "../openapi.yml#/components/responses/400" From 2fd9aa21893ee6faf5daeaa1ecf116cd2e87fee5 Mon Sep 17 00:00:00 2001 From: wtertius Date: Fri, 7 Oct 2022 15:46:18 +0300 Subject: [PATCH 185/260] Fix second level relative ref in property resolving (#622) Co-authored-by: Dmitriy Lukiyanchuk --- openapi3/loader.go | 4 + openapi3/loader_test.go | 11 +++ .../problem-details-0.0.1.schema.json | 93 +++++++++++++++++++ .../refInRefInProperty/components/errors.yaml | 66 +++++++++++++ .../testdata/refInRefInProperty/openapi.yaml | 29 ++++++ 5 files changed, 203 insertions(+) create mode 100644 openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json create mode 100644 openapi3/testdata/refInRefInProperty/components/errors.yaml create mode 100644 openapi3/testdata/refInRefInProperty/openapi.yaml diff --git a/openapi3/loader.go b/openapi3/loader.go index 9ed8e3076..637e1acf9 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -790,6 +790,10 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) string { if referencedFilename := strings.Split(ref, "#")[0]; referencedFilename == "" { if cur != nil { + if loader.rootDir != "" && strings.HasPrefix(cur.Path, loader.rootDir) { + return cur.Path[len(loader.rootDir)+1:] + } + return path.Base(cur.Path) } return "" diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index d492e2471..684e1b44d 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -263,6 +263,17 @@ func TestLoadWithReferenceInReference(t *testing.T) { require.Equal(t, "string", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) } +func TestLoadWithReferenceInReferenceInProperty(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadFromFile("testdata/refInRefInProperty/openapi.yaml") + require.NoError(t, err) + 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) +} + func TestLoadFileWithExternalSchemaRef(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true diff --git a/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json b/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json new file mode 100644 index 000000000..47547ab08 --- /dev/null +++ b/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json @@ -0,0 +1,93 @@ +{ + "title": "Problem details", + "description": "Common data object for describing an error details", + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "title", + "status" + ], + "properties": { + "type": { + "type": "string", + "description": "Unique error code", + "minLength": 1, + "example": "unauthorized", + "x-docs-examples": [ + "validation-error", + "unauthorized", + "forbidden", + "internal-server-error", + "wrong-basket", + "not-found" + ] + }, + "title": { + "type": "string", + "description": "Human readable error message", + "minLength": 1, + "example": "Your request parameters didn't validate", + "x-docs-examples": [ + "Your request parameters didn't validate", + "Requested resource is not available", + "Internal server error" + ] + }, + "status": { + "type": "integer", + "description": "HTTP status code", + "maximum": 599, + "minimum": 100, + "example": 200, + "x-docs-examples": [ + "200", + "201", + "400", + "503" + ] + }, + "detail": { + "type": "string", + "description": "Human readable error description. Only for human", + "example": "Basket must have more then 1 item", + "x-docs-examples": [ + "Basket must have more then 1 item" + ] + }, + "invalid-params": { + "type": "array", + "description": "Param list with errors", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "reason" + ], + "properties": { + "name": { + "type": "string", + "description": "field name", + "minLength": 1, + "example": "age", + "x-docs-examples": [ + "age", + "color" + ] + }, + "reason": { + "type": "string", + "description": "Field validation error text", + "minLength": 1, + "example": "must be a positive integer", + "x-docs-examples": [ + "must be a positive integer", + "must be 'green', 'red' or 'blue'" + ] + } + } + } + } + } +} diff --git a/openapi3/testdata/refInRefInProperty/components/errors.yaml b/openapi3/testdata/refInRefInProperty/components/errors.yaml new file mode 100644 index 000000000..1dc8fa7e3 --- /dev/null +++ b/openapi3/testdata/refInRefInProperty/components/errors.yaml @@ -0,0 +1,66 @@ +components: + schemas: + Error: + type: object + description: Error info in problem-details-0.0.1 format + properties: + error: + $ref: "../common-data-objects/problem-details-0.0.1.schema.json" + + responses: + 400: + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + json: + $ref: "#/components/examples/BadRequest" + 401: + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + json: + $ref: "#/components/examples/Unauthorized" + 500: + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + json: + $ref: "#/components/examples/InternalServerError" + + examples: + BadRequest: + summary: Wrong format + value: + error: + type: validation-error + title: Your request parameters didn't validate. + status: 400 + invalid-params: + - name: age + reason: must be a positive integer + - name: color + reason: must be 'green', 'red' or 'blue' + Unauthorized: + summary: Not authenticated + value: + error: + type: unauthorized + title: The request has not been applied because it lacks valid authentication credentials + for the target resource. + status: 401 + InternalServerError: + summary: Not handled internal server error + value: + error: + type: internal-server-error + title: Internal server error. + status: 500 diff --git a/openapi3/testdata/refInRefInProperty/openapi.yaml b/openapi3/testdata/refInRefInProperty/openapi.yaml new file mode 100644 index 000000000..d44b21687 --- /dev/null +++ b/openapi3/testdata/refInRefInProperty/openapi.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.3 + +info: + title: "Reference in reference in property example" + version: "1.0.0" +paths: + /api/test/ref/in/ref/in/property: + post: + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: array + items: + type: string + format: binary + required: true + responses: + 200: + description: "Files are saved successfully" + 400: + $ref: "./components/errors.yaml#/components/responses/400" + 401: + $ref: "./components/errors.yaml#/components/responses/401" + 500: + $ref: "./components/errors.yaml#/components/responses/500" From 8588ab89086c774233d44cfe9b2e5b6675945e0c Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 7 Oct 2022 17:16:30 +0200 Subject: [PATCH 186/260] rework convertError Example code to show query schema error (#626) --- openapi3/errors.go | 6 ++- openapi3filter/testdata/petstore.yaml | 6 +++ openapi3filter/unpack_errors_test.go | 53 ++++++++++++--------------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/openapi3/errors.go b/openapi3/errors.go index da0970abc..b530101df 100644 --- a/openapi3/errors.go +++ b/openapi3/errors.go @@ -11,9 +11,11 @@ type MultiError []error func (me MultiError) Error() string { buff := &bytes.Buffer{} - for _, e := range me { + for i, e := range me { buff.WriteString(e.Error()) - buff.WriteString(" | ") + if i != len(me)-1 { + buff.WriteString(" | ") + } } return buff.String() } diff --git a/openapi3filter/testdata/petstore.yaml b/openapi3filter/testdata/petstore.yaml index e3b61bff9..4a554488e 100644 --- a/openapi3filter/testdata/petstore.yaml +++ b/openapi3filter/testdata/petstore.yaml @@ -30,6 +30,12 @@ paths: summary: "Add a new pet to the store" description: "" operationId: "addPet" + parameters: + - name: num + in: query + schema: + type: integer + minimum: 1 requestBody: required: true content: diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go index 4242177f9..552e20ef8 100644 --- a/openapi3filter/unpack_errors_test.go +++ b/openapi3filter/unpack_errors_test.go @@ -66,7 +66,7 @@ func Example() { // (note invalid type for name and invalid status) body := strings.NewReader(`{"name": 100, "photoUrls": [], "status": "invalidStatus"}`) - req, err := http.NewRequest("POST", ts.URL+"/pet", body) + req, err := http.NewRequest("POST", ts.URL+"/pet?num=0", body) if err != nil { panic(err) } @@ -108,17 +108,25 @@ func Example() { // Value: // "invalidStatus" // + // ===== Start New Error ===== + // query.num: + // parameter "num" in query has an error: number must be at least 1 + // Schema: + // { + // "minimum": 1, + // "type": "integer" + // } + // + // Value: + // 0 + // // response: 400 {} } -const ( - prefixBody = "@body" - unknown = "@unknown" -) - func convertError(me openapi3.MultiError) map[string][]string { issues := make(map[string][]string) for _, err := range me { + const prefixBody = "@body" switch err := err.(type) { case *openapi3.SchemaError: // Can inspect schema validation errors here, e.g. err.Value @@ -126,47 +134,32 @@ func convertError(me openapi3.MultiError) map[string][]string { if path := err.JSONPointer(); len(path) > 0 { field = fmt.Sprintf("%s.%s", field, strings.Join(path, ".")) } - if _, ok := issues[field]; !ok { - issues[field] = make([]string, 0, 3) - } issues[field] = append(issues[field], err.Error()) case *openapi3filter.RequestError: // possible there were multiple issues that failed validation - if err, ok := err.Err.(openapi3.MultiError); ok { - for k, v := range convertError(err) { - if _, ok := issues[k]; !ok { - issues[k] = make([]string, 0, 3) - } - issues[k] = append(issues[k], v...) - } - continue - } // check if invalid HTTP parameter if err.Parameter != nil { prefix := err.Parameter.In name := fmt.Sprintf("%s.%s", prefix, err.Parameter.Name) - if _, ok := issues[name]; !ok { - issues[name] = make([]string, 0, 3) - } issues[name] = append(issues[name], err.Error()) continue } + if err, ok := err.Err.(openapi3.MultiError); ok { + for k, v := range convertError(err) { + issues[k] = append(issues[k], v...) + } + continue + } + // check if requestBody if err.RequestBody != nil { - if _, ok := issues[prefixBody]; !ok { - issues[prefixBody] = make([]string, 0, 3) - } issues[prefixBody] = append(issues[prefixBody], err.Error()) continue } default: - reasons, ok := issues[unknown] - if !ok { - reasons = make([]string, 0, 3) - } - reasons = append(reasons, err.Error()) - issues[unknown] = reasons + const unknown = "@unknown" + issues[unknown] = append(issues[unknown], err.Error()) } } return issues From d4c06afa13d18802678b5fd399888a7a76f43ad3 Mon Sep 17 00:00:00 2001 From: Yannick Clybouw Date: Fri, 7 Oct 2022 17:23:48 +0200 Subject: [PATCH 187/260] Allow validations options when creating legace Router (#614) --- routers/issue356_test.go | 5 ++++- routers/legacy/router.go | 4 ++-- routers/legacy/router_test.go | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/routers/issue356_test.go b/routers/issue356_test.go index 3e4b9fa1d..de9f06b91 100644 --- a/routers/issue356_test.go +++ b/routers/issue356_test.go @@ -68,8 +68,11 @@ paths: require.NoError(t, err) err = doc.Validate(context.Background()) require.NoError(t, err) + gorillamuxNewRouterWrapped := func(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error) { + return gorillamux.NewRouter(doc) + } - for i, newRouter := range []func(*openapi3.T) (routers.Router, error){gorillamux.NewRouter, legacy.NewRouter} { + for i, newRouter := range []func(*openapi3.T, ...openapi3.ValidationOption) (routers.Router, error){gorillamuxNewRouterWrapped, legacy.NewRouter} { t.Logf("using NewRouter from %s", map[int]string{0: "gorillamux", 1: "legacy"}[i]) router, err := newRouter(doc) require.NoError(t, err) diff --git a/routers/legacy/router.go b/routers/legacy/router.go index fb8d4621e..74e387323 100644 --- a/routers/legacy/router.go +++ b/routers/legacy/router.go @@ -58,8 +58,8 @@ type Router struct { // // If the given OpenAPIv3 document has servers, router will use them. // All operations of the document will be added to the router. -func NewRouter(doc *openapi3.T) (routers.Router, error) { - if err := doc.Validate(context.Background()); err != nil { +func NewRouter(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error) { + if err := doc.Validate(context.Background(), opts...); err != nil { return nil, fmt.Errorf("validating OpenAPI failed: %w", err) } router := &Router{doc: doc} diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go index d4779f58a..e9b875986 100644 --- a/routers/legacy/router_test.go +++ b/routers/legacy/router_test.go @@ -193,4 +193,21 @@ func TestRouter(t *testing.T) { require.Nil(t, route) require.Nil(t, pathParams) } + + schema := &openapi3.Schema{ + Type: "string", + Example: 3, + } + content := openapi3.NewContentWithJSONSchema(schema) + responses := openapi3.NewResponses() + responses["default"].Value.Content = content + doc.Paths["/withExamples"] = &openapi3.PathItem{ + Get: &openapi3.Operation{Responses: responses}, + } + err = doc.Validate(context.Background()) + require.Error(t, err) + r, err = NewRouter(doc) + require.Error(t, err) + r, err = NewRouter(doc, openapi3.DisableExamplesValidation()) + require.NoError(t, err) } From 59dbc9af76d25e6f1e8e4f9c9100b74df77645bd Mon Sep 17 00:00:00 2001 From: danicc097 <71724149+danicc097@users.noreply.github.com> Date: Fri, 7 Oct 2022 08:26:11 -0700 Subject: [PATCH 188/260] Additional error information (#617) --- openapi3/components.go | 36 ++++++++++++++--------------- openapi3/example.go | 2 +- openapi3/example_validation_test.go | 16 ++++++------- openapi3/issue601_test.go | 2 +- openapi3/media_type.go | 6 ++--- openapi3/parameter.go | 2 +- openapi3/schema.go | 4 ++-- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/openapi3/components.go b/openapi3/components.go index 3f883faf0..b7ad11c80 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -49,10 +49,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range schemas { v := components.Schemas[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("schema %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("schema %q: %w", k, err) } } @@ -64,10 +64,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range parameters { v := components.Parameters[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("parameter %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("parameter %q: %w", k, err) } } @@ -79,10 +79,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range requestBodies { v := components.RequestBodies[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("request body %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("request body %q: %w", k, err) } } @@ -94,10 +94,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range responses { v := components.Responses[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("response %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("response %q: %w", k, err) } } @@ -109,10 +109,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range headers { v := components.Headers[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("header %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("header %q: %w", k, err) } } @@ -124,10 +124,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range securitySchemes { v := components.SecuritySchemes[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("security scheme %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("security scheme %q: %w", k, err) } } @@ -139,10 +139,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range examples { v := components.Examples[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("example %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return fmt.Errorf("%s: %w", k, err) + return fmt.Errorf("example %q: %w", k, err) } } @@ -154,10 +154,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range links { v := components.Links[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("link %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("link %q: %w", k, err) } } @@ -169,10 +169,10 @@ func (components *Components) Validate(ctx context.Context) (err error) { for _, k := range callbacks { v := components.Callbacks[k] if err = ValidateIdentifier(k); err != nil { - return + return fmt.Errorf("callback %q: %w", k, err) } if err = v.Validate(ctx); err != nil { - return + return fmt.Errorf("callback %q: %w", k, err) } } diff --git a/openapi3/example.go b/openapi3/example.go index e63c78fa6..2161b688b 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -60,7 +60,7 @@ func (example *Example) Validate(ctx context.Context) error { return errors.New("value and externalValue are mutually exclusive") } if example.Value == nil && example.ExternalValue == "" { - return errors.New("example has no value or externalValue field") + return errors.New("no value or externalValue field") } return nil diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index 177360c13..f6a495ace 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -41,7 +41,7 @@ func TestExamplesSchemaValidation(t *testing.T) { parametersExample: ` example: abcd `, - errContains: "invalid paths", + errContains: "invalid path /user: invalid operation POST: invalid example", }, { name: "valid_parameter_example", @@ -64,7 +64,7 @@ func TestExamplesSchemaValidation(t *testing.T) { email: bad password: short `, - errContains: "invalid paths: invalid path /user: invalid operation POST: BadUser", + errContains: "invalid paths: invalid path /user: invalid operation POST: example BadUser", }, { name: "valid_component_examples", @@ -90,7 +90,7 @@ func TestExamplesSchemaValidation(t *testing.T) { email: bad password: short `, - errContains: "invalid paths", + errContains: "invalid path /user: invalid operation POST: invalid example", }, { name: "valid_mediatype_examples", @@ -109,7 +109,7 @@ func TestExamplesSchemaValidation(t *testing.T) { email: good@email.com # missing password `, - errContains: "invalid schema example", + errContains: "schema \"CreateUserRequest\": invalid example", }, { name: "valid_schema_request_example", @@ -127,7 +127,7 @@ func TestExamplesSchemaValidation(t *testing.T) { user_id: 1 # missing access_token `, - errContains: "invalid schema example", + errContains: "schema \"CreateUserResponse\": invalid example", }, { name: "valid_schema_response_example", @@ -278,7 +278,7 @@ func TestExampleObjectValidation(t *testing.T) { email: real@email.com password: validpassword `, - errContains: "example and examples are mutually exclusive", + errContains: "invalid path /user: invalid operation POST: example and examples are mutually exclusive", componentExamples: ` examples: BadUser: @@ -295,7 +295,7 @@ func TestExampleObjectValidation(t *testing.T) { BadUser: description: empty user example `, - errContains: "example has no value or externalValue field", + errContains: "invalid components: example \"BadUser\": no value or externalValue field", }, { name: "value_externalValue_mutual_exclusion", @@ -308,7 +308,7 @@ func TestExampleObjectValidation(t *testing.T) { password: validpassword externalValue: 'http://example.com/examples/example' `, - errContains: "value and externalValue are mutually exclusive", + errContains: "invalid components: example \"BadUser\": value and externalValue are mutually exclusive", }, } diff --git a/openapi3/issue601_test.go b/openapi3/issue601_test.go index 420ac9dc2..ef841c25f 100644 --- a/openapi3/issue601_test.go +++ b/openapi3/issue601_test.go @@ -18,7 +18,7 @@ func TestIssue601(t *testing.T) { require.NoError(t, err) err = doc.Validate(sl.Context) - require.Contains(t, err.Error(), `invalid components: invalid schema example: Error at "/type": property "type" is missing`) + require.Contains(t, err.Error(), `invalid components: schema "DiscoveryResult": invalid example: Error at "/type": property "type" is missing`) require.Contains(t, err.Error(), `| Error at "/nsid": property "nsid" is missing`) err = doc.Validate(sl.Context, DisableExamplesValidation()) diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 4269a4e64..1ae7c996a 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -94,7 +94,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { if example := mediaType.Example; example != nil { if err := validateExampleValue(example, schema.Value); err != nil { - return err + return fmt.Errorf("invalid example: %w", err) } } @@ -107,10 +107,10 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { for _, k := range names { v := examples[k] if err := v.Validate(ctx); err != nil { - return fmt.Errorf("%s: %w", k, err) + return fmt.Errorf("example %s: %w", k, err) } if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { - return fmt.Errorf("%s: %w", k, err) + return fmt.Errorf("example %s: %w", k, err) } } } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 6e2dbca08..73a39a54f 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -323,7 +323,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error { } if example := parameter.Example; example != nil { if err := validateExampleValue(example, schema.Value); err != nil { - return err + return fmt.Errorf("invalid example: %w", err) } } else if examples := parameter.Examples; examples != nil { names := make([]string, 0, len(examples)) diff --git a/openapi3/schema.go b/openapi3/schema.go index e9b6617fe..641ce8507 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -763,13 +763,13 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v := schema.Default; v != nil { if err := schema.VisitJSON(v); err != nil { - return fmt.Errorf("invalid schema default: %w", err) + return fmt.Errorf("invalid default: %w", err) } } if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled { if err := validateExampleValue(x, schema); err != nil { - return fmt.Errorf("invalid schema example: %w", err) + return fmt.Errorf("invalid example: %w", err) } } From ac594bc3f348ac97475114a7baeed9a6eea873d3 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 7 Oct 2022 17:26:58 +0200 Subject: [PATCH 189/260] Add SIMITGROUP`s repo to dependants shortlist (#627) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 850876fbd..9d2cd0193 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Here's some projects that depend on _kin-openapi_: * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Design-based APIs and microservices in Go" * [github.com/hashicorp/nomad-openapi](https://github.com/hashicorp/nomad-openapi) - "Nomad is an easy-to-use, flexible, and performant workload orchestrator that can deploy a mix of microservice, batch, containerized, and non-containerized applications. Nomad is easy to operate and scale and has native Consul and Vault integrations." * [gitlab.com/jamietanna/httptest-openapi](https://gitlab.com/jamietanna/httptest-openapi) ([*blog post*](https://www.jvt.me/posts/2022/05/22/go-openapi-contract-test/)) - "Go OpenAPI Contract Verification for use with `net/http`" + * [github.com/SIMITGROUP/openapigenerator](https://github.com/SIMITGROUP/openapigenerator) - "Openapi v3 microservices generator" * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request) ## Alternatives From b31a4bb2dcd523b1477da8854f0f75859cd314b3 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 7 Oct 2022 18:46:36 +0200 Subject: [PATCH 190/260] Introduce package-wide CircularReferenceCounter to work around #615 (#628) Co-authored-by: sorintm --- .github/workflows/go.yml | 4 +- openapi3/issue615_test.go | 33 ++++++++++++ openapi3/loader.go | 7 +-- openapi3/testdata/recursiveRef/issue615.yml | 60 +++++++++++++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 openapi3/issue615_test.go create mode 100644 openapi3/testdata/recursiveRef/issue615.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d34061a04..649a14846 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -62,10 +62,10 @@ jobs: - run: git --no-pager diff --exit-code - if: runner.os == 'Linux' - run: go test ./... + run: go test -count=10 ./... env: GOARCH: '386' - - run: go test ./... + - run: go test -count=10 ./... - run: go test -count=2 -covermode=atomic ./... - run: go test -v -run TestRaceyPatternSchema -race ./... env: diff --git a/openapi3/issue615_test.go b/openapi3/issue615_test.go new file mode 100644 index 000000000..ceb317ab0 --- /dev/null +++ b/openapi3/issue615_test.go @@ -0,0 +1,33 @@ +package openapi3_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestIssue615(t *testing.T) { + for { + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + _, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml") + if err == nil { + continue + } + // Test currently reproduces the issue 615: failure to load a valid spec + // Upon issue resolution, this check should be changed to require.NoError + require.Error(t, err, openapi3.CircularReferenceError) + break + } + + var old int + old, openapi3.CircularReferenceCounter = openapi3.CircularReferenceCounter, 4 + defer func() { openapi3.CircularReferenceCounter = old }() + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + _, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml") + require.NoError(t, err) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 637e1acf9..0c083312d 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -17,6 +17,7 @@ import ( ) var CircularReferenceError = "kin-openapi bug found: circular schema reference not handled" +var CircularReferenceCounter = 3 func foundUnresolvedRef(ref string) error { return fmt.Errorf("found unresolved ref: %q", ref) @@ -724,7 +725,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat } component.Value = &schema } else { - if visitedLimit(visited, ref, 3) { + if visitedLimit(visited, ref) { visited = append(visited, ref) return fmt.Errorf("%s - %s", CircularReferenceError, strings.Join(visited, " -> ")) } @@ -1088,12 +1089,12 @@ func unescapeRefString(ref string) string { return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1) } -func visitedLimit(visited []string, ref string, limit int) bool { +func visitedLimit(visited []string, ref string) bool { visitedCount := 0 for _, v := range visited { if v == ref { visitedCount++ - if visitedCount >= limit { + if visitedCount >= CircularReferenceCounter { return true } } diff --git a/openapi3/testdata/recursiveRef/issue615.yml b/openapi3/testdata/recursiveRef/issue615.yml new file mode 100644 index 000000000..d1370e32e --- /dev/null +++ b/openapi3/testdata/recursiveRef/issue615.yml @@ -0,0 +1,60 @@ +openapi: "3.0.3" +info: + title: Deep recursive cyclic refs example + version: "1.0" +paths: + /foo: + $ref: ./paths/foo.yml +components: + schemas: + FilterColumnIncludes: + type: object + properties: + $includes: + $ref: '#/components/schemas/FilterPredicate' + additionalProperties: false + maxProperties: 1 + minProperties: 1 + FilterPredicate: + oneOf: + - $ref: '#/components/schemas/FilterValue' + - type: array + items: + $ref: '#/components/schemas/FilterPredicate' + minLength: 1 + - $ref: '#/components/schemas/FilterPredicateOp' + - $ref: '#/components/schemas/FilterPredicateRangeOp' + FilterPredicateOp: + type: object + properties: + $any: + oneOf: + - type: array + items: + $ref: '#/components/schemas/FilterPredicate' + $none: + oneOf: + - $ref: '#/components/schemas/FilterPredicate' + - type: array + items: + $ref: '#/components/schemas/FilterPredicate' + additionalProperties: false + maxProperties: 1 + minProperties: 1 + FilterPredicateRangeOp: + type: object + properties: + $lt: + $ref: '#/components/schemas/FilterRangeValue' + additionalProperties: false + maxProperties: 2 + minProperties: 2 + FilterRangeValue: + oneOf: + - type: number + - type: string + FilterValue: + oneOf: + - type: number + - type: string + - type: boolean \ No newline at end of file From fc05f1cd5e03451eb74a3085c480c5a71209aba6 Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Tue, 11 Oct 2022 20:21:01 +0200 Subject: [PATCH 191/260] fix: embedded struct handling (#630) --- jsoninfo/field_info.go | 9 ++++++--- openapi3gen/openapi3gen_test.go | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/jsoninfo/field_info.go b/jsoninfo/field_info.go index 2382b731c..6b45f8c69 100644 --- a/jsoninfo/field_info.go +++ b/jsoninfo/field_info.go @@ -35,11 +35,14 @@ iteration: // See whether this is an embedded field if f.Anonymous { - if f.Tag.Get("json") == "-" { + jsonTag := f.Tag.Get("json") + if jsonTag == "-" { continue } - fields = AppendFields(fields, index, f.Type) - continue iteration + if jsonTag == "" { + fields = AppendFields(fields, index, f.Type) + continue iteration + } } // Ignore certain types diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index a4c2d52e9..bfa3120ec 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -18,6 +18,12 @@ import ( func ExampleGenerator_SchemaRefs() { type SomeOtherType string + type Embedded struct { + Z string `json:"z"` + } + type Embedded2 struct { + A string `json:"a"` + } type SomeStruct struct { Bool bool `json:"bool"` Int int `json:"int"` @@ -38,6 +44,10 @@ func ExampleGenerator_SchemaRefs() { Y string } `json:"structWithoutFields"` + Embedded `json:"embedded"` + + Embedded2 + Ptr *SomeOtherType `json:"ptr"` } @@ -54,9 +64,12 @@ func ExampleGenerator_SchemaRefs() { } fmt.Printf("schemaRef: %s\n", data) // Output: - // g.SchemaRefs: 15 + // g.SchemaRefs: 16 // schemaRef: { // "properties": { + // "a": { + // "type": "string" + // }, // "bool": { // "type": "boolean" // }, @@ -64,6 +77,14 @@ func ExampleGenerator_SchemaRefs() { // "format": "byte", // "type": "string" // }, + // "embedded": { + // "properties": { + // "z": { + // "type": "string" + // } + // }, + // "type": "object" + // }, // "float64": { // "format": "double", // "type": "number" From 138bfa0a5d7b6148337f0b43a160e71868186cb3 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 12 Oct 2022 13:52:07 +0200 Subject: [PATCH 192/260] openapi3filter: Fallback to string when decoding request parameters (#631) --- README.md | 5 ++- openapi3/loader.go | 4 +- openapi3filter/issue624_test.go | 64 ++++++++++++++++++++++++++++++ openapi3filter/req_resp_decoder.go | 22 ++++++++-- openapi3filter/validate_request.go | 6 +-- 5 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 openapi3filter/issue624_test.go diff --git a/README.md b/README.md index 9d2cd0193..c8431f807 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ [![Join Gitter Chat Channel -](https://badges.gitter.im/getkin/kin.svg)](https://gitter.im/getkin/kin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # Introduction -A [Go](https://golang.org) project for handling [OpenAPI](https://www.openapis.org/) files. We target the latest OpenAPI version (currently 3), but the project contains support for older OpenAPI versions too. +A [Go](https://golang.org) project for handling [OpenAPI](https://www.openapis.org/) files. We target: +* [OpenAPI `v2.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md) (formerly known as Swagger) +* [OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) +* [OpenAPI `v3.1`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md) Soon! [Tracking issue here.](https://github.com/getkin/kin-openapi/issues/230) Licensed under the [MIT License](./LICENSE). diff --git a/openapi3/loader.go b/openapi3/loader.go index 0c083312d..1ab693b0f 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -56,7 +56,9 @@ type Loader struct { // NewLoader returns an empty Loader func NewLoader() *Loader { - return &Loader{} + return &Loader{ + Context: context.Background(), + } } func (loader *Loader) resetVisitedPathItemRefs() { diff --git a/openapi3filter/issue624_test.go b/openapi3filter/issue624_test.go new file mode 100644 index 000000000..d93682e52 --- /dev/null +++ b/openapi3filter/issue624_test.go @@ -0,0 +1,64 @@ +package openapi3filter + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue624(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + spec := ` +openapi: 3.0.0 +info: + version: 1.0.0 + title: Sample API +paths: + /items: + get: + description: Returns a list of stuff + parameters: + - description: "test non object" + explode: true + style: form + in: query + name: test + required: false + content: + application/json: + schema: + anyOf: + - type: string + - type: integer + responses: + '200': + description: Successful response +`[1:] + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + httpReq, err := http.NewRequest(http.MethodGet, `/items?test=test1`, nil) + require.NoError(t, err) + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + requestValidationInput := &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } + err = ValidateRequest(ctx, requestValidationInput) + require.NoError(t, err) +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 73eb73e2b..515d09cd1 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -158,7 +158,10 @@ func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationI } func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) ( - outValue interface{}, outSchema *openapi3.Schema, err error) { + outValue interface{}, + outSchema *openapi3.Schema, + err error, +) { // Only query parameters can have multiple values. if len(values) > 1 && param.In != openapi3.ParameterInQuery { err = fmt.Errorf("%s parameter %q cannot have multiple values", param.In, param.Name) @@ -170,7 +173,6 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) err = fmt.Errorf("parameter %q expected to have content", param.Name) return } - // We only know how to decode a parameter if it has one content, application/json if len(content) != 1 { err = fmt.Errorf("multiple content types for parameter %q", param.Name) @@ -184,8 +186,20 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) } outSchema = mt.Schema.Value + unmarshal := func(encoded string) (decoded interface{}, err error) { + if err = json.Unmarshal([]byte(encoded), &decoded); err != nil { + const specialJSONChars = `[]{}":,` + if !strings.ContainsAny(encoded, specialJSONChars) { + // A string in a query parameter is not serialized with (double) quotes + // as JSON would expect, so let's fallback to that. + decoded, err = encoded, nil + } + } + return + } + if len(values) == 1 { - if err = json.Unmarshal([]byte(values[0]), &outValue); err != nil { + if outValue, err = unmarshal(values[0]); err != nil { err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return } @@ -193,7 +207,7 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) outArray := make([]interface{}, 0, len(values)) for _, v := range values { var item interface{} - if err = json.Unmarshal([]byte(v), &item); err != nil { + if item, err = unmarshal(v); err != nil { err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 4f2232645..b09987f74 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -148,7 +148,9 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param value = schema.Default req := input.Request switch parameter.In { - // case openapi3.ParameterInPath: TODO: no idea how to handle this + case openapi3.ParameterInPath: + // Path parameters are required. + // Next check `parameter.Required && !found` will catch this. case openapi3.ParameterInQuery: q := req.URL.Query() q.Add(parameter.Name, fmt.Sprintf("%v", value)) @@ -160,8 +162,6 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param Name: parameter.Name, Value: fmt.Sprintf("%v", value), }) - default: - return fmt.Errorf("unsupported parameter's 'in': %s", parameter.In) } } From e887ba8d7526c0b841670f0136ffc2f07d2fbabe Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 13 Oct 2022 11:59:21 +0200 Subject: [PATCH 193/260] Introduce `(openapi3.*Server).BasePath()` and `(openapi3.Servers).BasePath()` (#633) --- openapi3/server.go | 32 ++++++++++++++++ openapi3/server_test.go | 85 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/openapi3/server.go b/openapi3/server.go index 88fdcc0f3..3a1a7cef9 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -25,6 +25,14 @@ func (servers Servers) Validate(ctx context.Context) error { return nil } +// BasePath returns the base path of the first server in the list, or /. +func (servers Servers) BasePath() (string, error) { + for _, server := range servers { + return server.BasePath() + } + return "/", nil +} + func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) { rawURL := parsedURL.String() if i := strings.IndexByte(rawURL, '?'); i >= 0 { @@ -49,6 +57,30 @@ type Server struct { Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` } +// BasePath returns the base path extracted from the default values of variables, if any. +// Assumes a valid struct (per Validate()). +func (server *Server) BasePath() (string, error) { + if server == nil { + return "/", nil + } + + uri := server.URL + for name, svar := range server.Variables { + uri = strings.ReplaceAll(uri, "{"+name+"}", svar.Default) + } + + u, err := url.ParseRequestURI(uri) + if err != nil { + return "", err + } + + if bp := u.Path; bp != "" { + return bp, nil + } + + return "/", nil +} + // MarshalJSON returns the JSON encoding of Server. func (server *Server) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(server) diff --git a/openapi3/server_test.go b/openapi3/server_test.go index 0ace0345f..c59b86e56 100644 --- a/openapi3/server_test.go +++ b/openapi3/server_test.go @@ -116,3 +116,88 @@ func newServerMatch(remaining string, args ...string) *serverMatch { Args: args, } } + +func TestServersBasePath(t *testing.T) { + for _, testcase := range []struct { + title string + servers Servers + expected string + }{ + { + title: "empty servers", + servers: nil, + expected: "/", + }, + { + title: "URL set, missing trailing slash", + servers: Servers{&Server{URL: "https://example.com"}}, + expected: "/", + }, + { + title: "URL set, with trailing slash", + servers: Servers{&Server{URL: "https://example.com/"}}, + expected: "/", + }, + { + title: "URL set", + servers: Servers{&Server{URL: "https://example.com/b/l/a"}}, + expected: "/b/l/a", + }, + { + title: "URL set with variables", + servers: Servers{&Server{ + URL: "{scheme}://example.com/b/l/a", + Variables: map[string]*ServerVariable{ + "scheme": { + Enum: []string{"http", "https"}, + Default: "https", + }, + }, + }}, + expected: "/b/l/a", + }, + { + title: "URL set with variables in path", + servers: Servers{&Server{ + URL: "http://example.com/b/{var1}/a", + Variables: map[string]*ServerVariable{ + "var1": { + Default: "lllll", + }, + }, + }}, + expected: "/b/lllll/a", + }, + { + title: "URLs set with variables in path", + servers: Servers{ + &Server{ + URL: "http://example.com/b/{var2}/a", + Variables: map[string]*ServerVariable{ + "var2": { + Default: "LLLLL", + }, + }, + }, + &Server{ + URL: "https://example.com/b/{var1}/a", + Variables: map[string]*ServerVariable{ + "var1": { + Default: "lllll", + }, + }, + }, + }, + expected: "/b/LLLLL/a", + }, + } { + t.Run(testcase.title, func(t *testing.T) { + err := testcase.servers.Validate(context.Background()) + require.NoError(t, err) + + got, err := testcase.servers.BasePath() + require.NoError(t, err) + require.Exactly(t, testcase.expected, got) + }) + } +} From 330c14297a9f2cb0d31d1516106be573ab05f17d Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 13 Oct 2022 15:30:45 +0200 Subject: [PATCH 194/260] Actually #624, thanks to @orensolo (#634) --- openapi3filter/issue624_test.go | 25 +++++++++++++++---------- openapi3filter/req_resp_decoder.go | 18 +++++++++--------- openapi3filter/validate_request.go | 22 +++++++--------------- openapi3filter/validation_test.go | 13 ++++++------- 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/openapi3filter/issue624_test.go b/openapi3filter/issue624_test.go index d93682e52..1fdbdea34 100644 --- a/openapi3filter/issue624_test.go +++ b/openapi3filter/issue624_test.go @@ -48,17 +48,22 @@ paths: router, err := gorillamux.NewRouter(doc) require.NoError(t, err) - httpReq, err := http.NewRequest(http.MethodGet, `/items?test=test1`, nil) - require.NoError(t, err) - route, pathParams, err := router.FindRoute(httpReq) - require.NoError(t, err) + for _, testcase := range []string{`test1`, `test[1`} { + t.Run(testcase, func(t *testing.T) { + httpReq, err := http.NewRequest(http.MethodGet, `/items?test=`+testcase, nil) + require.NoError(t, err) - requestValidationInput := &RequestValidationInput{ - Request: httpReq, - PathParams: pathParams, - Route: route, + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + requestValidationInput := &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } + err = ValidateRequest(ctx, requestValidationInput) + require.NoError(t, err) + }) } - err = ValidateRequest(ctx, requestValidationInput) - require.NoError(t, err) } diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 515d09cd1..ca8194432 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -110,8 +110,11 @@ func invalidSerializationMethodErr(sm *openapi3.SerializationMethod) error { // Decodes a parameter defined via the content property as an object. It uses // the user specified decoder, or our build-in decoder for application/json func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationInput) ( - value interface{}, schema *openapi3.Schema, found bool, err error) { - + value interface{}, + schema *openapi3.Schema, + found bool, + err error, +) { var paramValues []string switch param.In { case openapi3.ParameterInPath: @@ -186,12 +189,9 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) } outSchema = mt.Schema.Value - unmarshal := func(encoded string) (decoded interface{}, err error) { + unmarshal := func(encoded string, paramSchema *openapi3.SchemaRef) (decoded interface{}, err error) { if err = json.Unmarshal([]byte(encoded), &decoded); err != nil { - const specialJSONChars = `[]{}":,` - if !strings.ContainsAny(encoded, specialJSONChars) { - // A string in a query parameter is not serialized with (double) quotes - // as JSON would expect, so let's fallback to that. + if paramSchema != nil && paramSchema.Value.Type != "object" { decoded, err = encoded, nil } } @@ -199,7 +199,7 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) } if len(values) == 1 { - if outValue, err = unmarshal(values[0]); err != nil { + if outValue, err = unmarshal(values[0], mt.Schema); err != nil { err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return } @@ -207,7 +207,7 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) outArray := make([]interface{}, 0, len(values)) for _, v := range values { var item interface{} - if item, err = unmarshal(v); err != nil { + if item, err = unmarshal(v, outSchema.Items); err != nil { err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index b09987f74..4b0bd3413 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -28,11 +28,8 @@ var ErrInvalidEmptyValue = errors.New("empty value is not allowed") // // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker -func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { - var ( - err error - me openapi3.MultiError - ) +func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error) { + var me openapi3.MultiError options := input.Options if options == nil { @@ -52,9 +49,8 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { } if security != nil { if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError { - return err + return } - if err != nil { me = append(me, err) } @@ -70,9 +66,8 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { } if err = ValidateParameter(ctx, input, parameter); err != nil && !options.MultiError { - return err + return } - if err != nil { me = append(me, err) } @@ -81,9 +76,8 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { // For each parameter of the Operation for _, parameter := range operationParameters { if err = ValidateParameter(ctx, input, parameter.Value); err != nil && !options.MultiError { - return err + return } - if err != nil { me = append(me, err) } @@ -93,9 +87,8 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { requestBody := operation.RequestBody if requestBody != nil && !options.ExcludeRequestBody { if err = ValidateRequestBody(ctx, input, requestBody.Value); err != nil && !options.MultiError { - return err + return } - if err != nil { me = append(me, err) } @@ -104,8 +97,7 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { if len(me) > 0 { return me } - - return nil + return } // ValidateParameter validates a parameter's value by JSON schema. diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index cd1fa8990..cdbeb1262 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -198,7 +198,7 @@ func TestFilter(t *testing.T) { } err = ValidateResponse(context.Background(), responseValidationInput) require.NoError(t, err) - return err + return nil } expect := func(req ExampleRequest, resp ExampleResponse) error { return expectWithDecoder(req, resp, nil) @@ -207,13 +207,12 @@ func TestFilter(t *testing.T) { resp := ExampleResponse{ Status: 200, } - // Test paths + // Test paths req := ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix", } - err = expect(req, resp) require.NoError(t, err) @@ -328,7 +327,7 @@ func TestFilter(t *testing.T) { // enough. req = ExampleRequest{ Method: "POST", - URL: "http://example.com/api/prefix/v/suffix?contentArg={\"name\":\"bob\", \"id\":\"a\"}", + URL: `http://example.com/api/prefix/v/suffix?contentArg={"name":"bob", "id":"a"}`, } err = expect(req, resp) require.NoError(t, err) @@ -336,7 +335,7 @@ func TestFilter(t *testing.T) { // Now it should fail due the ID being too long req = ExampleRequest{ Method: "POST", - URL: "http://example.com/api/prefix/v/suffix?contentArg={\"name\":\"bob\", \"id\":\"EXCEEDS_MAX_LENGTH\"}", + URL: `http://example.com/api/prefix/v/suffix?contentArg={"name":"bob", "id":"EXCEEDS_MAX_LENGTH"}`, } err = expect(req, resp) require.IsType(t, &RequestError{}, err) @@ -351,7 +350,7 @@ func TestFilter(t *testing.T) { req = ExampleRequest{ Method: "POST", - URL: "http://example.com/api/prefix/v/suffix?contentArg2={\"name\":\"bob\", \"id\":\"a\"}", + URL: `http://example.com/api/prefix/v/suffix?contentArg2={"name":"bob", "id":"a"}`, } err = expectWithDecoder(req, resp, customDecoder) require.NoError(t, err) @@ -359,7 +358,7 @@ func TestFilter(t *testing.T) { // Now it should fail due the ID being too long req = ExampleRequest{ Method: "POST", - URL: "http://example.com/api/prefix/v/suffix?contentArg2={\"name\":\"bob\", \"id\":\"EXCEEDS_MAX_LENGTH\"}", + URL: `http://example.com/api/prefix/v/suffix?contentArg2={"name":"bob", "id":"EXCEEDS_MAX_LENGTH"}`, } err = expectWithDecoder(req, resp, customDecoder) require.IsType(t, &RequestError{}, err) From e95ed3471cf52fb8df78cbf0f374d6b83d412b2d Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 13 Oct 2022 18:09:51 +0200 Subject: [PATCH 195/260] Check for superfluous trailing whitespace (#636) --- .github/workflows/go.yml | 5 ++ openapi3/testdata/link-example.yaml | 96 +++++++++++++-------------- openapi3/testdata/lxkns.yaml | 2 +- openapi3filter/testdata/petstore.yaml | 2 +- 4 files changed, 55 insertions(+), 50 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 649a14846..aa40035a0 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -91,6 +91,11 @@ jobs: name: nilness run: go run golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@latest ./... + - if: runner.os == 'Linux' + name: Check for superfluous trailing whitespace + run: | + ! grep -IErn '\s$' --exclude-dir={.git,target,pgdata} + - if: runner.os == 'Linux' name: Missing specification object link to definition run: | diff --git a/openapi3/testdata/link-example.yaml b/openapi3/testdata/link-example.yaml index 5837d705e..735e7dbb7 100644 --- a/openapi3/testdata/link-example.yaml +++ b/openapi3/testdata/link-example.yaml @@ -1,23 +1,23 @@ openapi: 3.0.0 -info: +info: title: Link Example version: 1.0.0 -paths: - /2.0/users/{username}: - get: +paths: + /2.0/users/{username}: + get: operationId: getUserByName - parameters: + parameters: - name: username in: path required: true schema: type: string - responses: + responses: '200': description: The User content: application/json: - schema: + schema: $ref: '#/components/schemas/user' links: userRepositories: @@ -34,7 +34,7 @@ paths: responses: '200': description: repositories owned by the supplied user - content: + content: application/json: schema: type: array @@ -43,10 +43,10 @@ paths: links: userRepository: $ref: '#/components/links/UserRepository' - /2.0/repositories/{username}/{slug}: - get: + /2.0/repositories/{username}/{slug}: + get: operationId: getRepository - parameters: + parameters: - name: username in: path required: true @@ -57,20 +57,20 @@ paths: required: true schema: type: string - responses: + responses: '200': description: The repository content: - application/json: - schema: + application/json: + schema: $ref: '#/components/schemas/repository' links: repositoryPullRequests: $ref: '#/components/links/RepositoryPullRequests' - /2.0/repositories/{username}/{slug}/pullrequests: - get: + /2.0/repositories/{username}/{slug}/pullrequests: + get: operationId: getPullRequestsByRepository - parameters: + parameters: - name: username in: path required: true @@ -85,23 +85,23 @@ paths: in: query schema: type: string - enum: + enum: - open - merged - declined - responses: + responses: '200': description: an array of pull request objects content: - application/json: - schema: + application/json: + schema: type: array - items: + items: $ref: '#/components/schemas/pullrequest' - /2.0/repositories/{username}/{slug}/pullrequests/{pid}: - get: + /2.0/repositories/{username}/{slug}/pullrequests/{pid}: + get: operationId: getPullRequestsById - parameters: + parameters: - name: username in: path required: true @@ -117,20 +117,20 @@ paths: required: true schema: type: string - responses: + responses: '200': description: a pull request object content: - application/json: - schema: + application/json: + schema: $ref: '#/components/schemas/pullrequest' links: pullRequestMerge: $ref: '#/components/links/PullRequestMerge' - /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge: - post: + /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge: + post: operationId: mergePullRequest - parameters: + parameters: - name: username in: path required: true @@ -146,7 +146,7 @@ paths: required: true schema: type: string - responses: + responses: '204': description: the PR was successfully merged components: @@ -165,7 +165,7 @@ components: RepositoryPullRequests: # returns '#/components/schemas/pullrequest' operationId: getPullRequestsByRepository - parameters: + parameters: username: $response.body#/owner/username slug: $response.body#/slug PullRequestMerge: @@ -175,29 +175,29 @@ components: username: $response.body#/author/username slug: $response.body#/repository/slug pid: $response.body#/id - schemas: - user: + schemas: + user: type: object - properties: - username: + properties: + username: type: string - uuid: + uuid: type: string - repository: + repository: type: object - properties: - slug: + properties: + slug: type: string - owner: + owner: $ref: '#/components/schemas/user' - pullrequest: + pullrequest: type: object - properties: - id: + properties: + id: type: integer - title: + title: type: string - repository: + repository: $ref: '#/components/schemas/repository' - author: + author: $ref: '#/components/schemas/user' diff --git a/openapi3/testdata/lxkns.yaml b/openapi3/testdata/lxkns.yaml index e8400592c..6e1bee5d6 100644 --- a/openapi3/testdata/lxkns.yaml +++ b/openapi3/testdata/lxkns.yaml @@ -1,6 +1,6 @@ # https://raw.githubusercontent.com/thediveo/lxkns/71e8fb5e40c612ecc89d972d211221137e92d5f0/api/openapi-spec/lxkns.yaml openapi: 3.0.2 -security: +security: - {} info: title: lxkns diff --git a/openapi3filter/testdata/petstore.yaml b/openapi3filter/testdata/petstore.yaml index 4a554488e..026c37e27 100644 --- a/openapi3filter/testdata/petstore.yaml +++ b/openapi3filter/testdata/petstore.yaml @@ -38,7 +38,7 @@ paths: minimum: 1 requestBody: required: true - content: + content: 'application/json': schema: $ref: '#/components/schemas/Pet' From ac1811368b118e6c46fa9eede7541c9c949ec4f8 Mon Sep 17 00:00:00 2001 From: danicc097 <71724149+danicc097@users.noreply.github.com> Date: Sun, 16 Oct 2022 04:04:46 -0700 Subject: [PATCH 196/260] show errors in security requirements (#637) --- openapi3filter/errors.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openapi3filter/errors.go b/openapi3filter/errors.go index 1094bcf75..b5454a75c 100644 --- a/openapi3filter/errors.go +++ b/openapi3filter/errors.go @@ -1,6 +1,7 @@ package openapi3filter import ( + "bytes" "fmt" "github.com/getkin/kin-openapi/openapi3" @@ -78,5 +79,14 @@ type SecurityRequirementsError struct { } func (err *SecurityRequirementsError) Error() string { - return "Security requirements failed" + buff := &bytes.Buffer{} + buff.WriteString("security requirements failed: ") + for i, e := range err.Errors { + buff.WriteString(e.Error()) + if i != len(err.Errors)-1 { + buff.WriteString(" | ") + } + } + + return buff.String() } From e56a195ae04c6df938651ecfc274ee26fd0cbd1f Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 24 Oct 2022 15:55:59 +0200 Subject: [PATCH 197/260] Fix validation of complex enum values (#647) --- openapi3/schema.go | 3 ++- openapi3/schema_test.go | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 641ce8507..b38d25707 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -8,6 +8,7 @@ import ( "fmt" "math" "math/big" + "reflect" "regexp" "sort" "strconv" @@ -870,7 +871,7 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) { if enum := schema.Enum; len(enum) != 0 { for _, v := range enum { - if value == v { + if reflect.DeepEqual(v, value) { return } } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 4c14dcb10..abec30477 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1314,3 +1314,45 @@ func TestValidationFailsOnInvalidPattern(t *testing.T) { err := schema.Validate(context.Background()) require.Error(t, err) } + +func TestIssue646(t *testing.T) { + data := []byte(` +enum: +- 42 +- [] +- [a] +- {} +- {b: c} +`[1:]) + + var schema Schema + err := yaml.Unmarshal(data, &schema) + require.NoError(t, err) + + err = schema.Validate(context.Background()) + require.NoError(t, err) + + err = schema.VisitJSON(42) + require.NoError(t, err) + + err = schema.VisitJSON(1337) + require.Error(t, err) + + err = schema.VisitJSON([]interface{}{}) + require.NoError(t, err) + + err = schema.VisitJSON([]interface{}{"a"}) + require.NoError(t, err) + + err = schema.VisitJSON([]interface{}{"b"}) + require.Error(t, err) + + err = schema.VisitJSON(map[string]interface{}{}) + require.NoError(t, err) + + err = schema.VisitJSON(map[string]interface{}{"b": "c"}) + require.NoError(t, err) + + err = schema.VisitJSON(map[string]interface{}{"d": "e"}) + require.Error(t, err) +} From 285135d2190e2a39281a7e06564d4dbccc117b43 Mon Sep 17 00:00:00 2001 From: danicc097 <71724149+danicc097@users.noreply.github.com> Date: Thu, 27 Oct 2022 07:47:57 -0700 Subject: [PATCH 198/260] readOnly writeOnly validation (#599) --- openapi3/example_validation.go | 15 +- openapi3/example_validation_test.go | 140 +++++++++-- openapi3/media_type.go | 6 +- openapi3/parameter.go | 7 +- openapi3/request_body.go | 5 + openapi3/response.go | 3 + openapi3/schema.go | 19 +- openapi3/validation_options.go | 9 +- openapi3filter/validate_readonly_test.go | 281 +++++++++++++++++------ openapi3filter/validate_response.go | 2 +- openapi3filter/validation_error_test.go | 9 - 11 files changed, 379 insertions(+), 117 deletions(-) diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go index 4c75e360b..fb7a1da16 100644 --- a/openapi3/example_validation.go +++ b/openapi3/example_validation.go @@ -1,5 +1,16 @@ package openapi3 -func validateExampleValue(input interface{}, schema *Schema) error { - return schema.VisitJSON(input, MultiErrors()) +import "context" + +func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error { + opts := make([]SchemaValidationOption, 0, 2) + + if vo := getValidationOptions(ctx); vo.examplesValidationAsReq { + opts = append(opts, VisitAsRequest()) + } else if vo.examplesValidationAsRes { + opts = append(opts, VisitAsResponse()) + } + opts = append(opts, MultiErrors()) + + return schema.VisitJSON(input, opts...) } diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index f6a495ace..79288c299 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -9,13 +9,16 @@ import ( func TestExamplesSchemaValidation(t *testing.T) { type testCase struct { - name string - requestSchemaExample string - responseSchemaExample string - mediaTypeRequestExample string - parametersExample string - componentExamples string - errContains string + name string + requestSchemaExample string + responseSchemaExample string + mediaTypeRequestExample string + mediaTypeResponseExample string + readWriteOnlyMediaTypeRequestExample string + readWriteOnlyMediaTypeResponseExample string + parametersExample string + componentExamples string + errContains string } testCases := []testCase{ @@ -26,7 +29,7 @@ func TestExamplesSchemaValidation(t *testing.T) { param1example: value: abcd `, - errContains: "invalid paths: invalid path /user: invalid operation POST: param1example", + errContains: `invalid paths: invalid path /user: invalid operation POST: param1example`, }, { name: "valid_parameter_examples", @@ -41,7 +44,7 @@ func TestExamplesSchemaValidation(t *testing.T) { parametersExample: ` example: abcd `, - errContains: "invalid path /user: invalid operation POST: invalid example", + errContains: `invalid path /user: invalid operation POST: invalid example`, }, { name: "valid_parameter_example", @@ -64,7 +67,7 @@ func TestExamplesSchemaValidation(t *testing.T) { email: bad password: short `, - errContains: "invalid paths: invalid path /user: invalid operation POST: example BadUser", + errContains: `invalid paths: invalid path /user: invalid operation POST: example BadUser`, }, { name: "valid_component_examples", @@ -90,7 +93,7 @@ func TestExamplesSchemaValidation(t *testing.T) { email: bad password: short `, - errContains: "invalid path /user: invalid operation POST: invalid example", + errContains: `invalid path /user: invalid operation POST: invalid example`, }, { name: "valid_mediatype_examples", @@ -109,7 +112,7 @@ func TestExamplesSchemaValidation(t *testing.T) { email: good@email.com # missing password `, - errContains: "schema \"CreateUserRequest\": invalid example", + errContains: `schema "CreateUserRequest": invalid example`, }, { name: "valid_schema_request_example", @@ -127,7 +130,7 @@ func TestExamplesSchemaValidation(t *testing.T) { user_id: 1 # missing access_token `, - errContains: "schema \"CreateUserResponse\": invalid example", + errContains: `schema "CreateUserResponse": invalid example`, }, { name: "valid_schema_response_example", @@ -135,7 +138,64 @@ func TestExamplesSchemaValidation(t *testing.T) { example: user_id: 1 access_token: "abcd" - `, + `, + }, + { + name: "valid_readonly_writeonly_examples", + readWriteOnlyMediaTypeRequestExample: ` + examples: + ReadWriteOnlyRequest: + $ref: '#/components/examples/ReadWriteOnlyRequestData' +`, + readWriteOnlyMediaTypeResponseExample: ` + examples: + ReadWriteOnlyResponse: + $ref: '#/components/examples/ReadWriteOnlyResponseData' +`, + componentExamples: ` + examples: + ReadWriteOnlyRequestData: + value: + username: user + password: password + ReadWriteOnlyResponseData: + value: + user_id: 4321 + `, + }, + { + name: "invalid_readonly_request_examples", + readWriteOnlyMediaTypeRequestExample: ` + examples: + ReadWriteOnlyRequest: + $ref: '#/components/examples/ReadWriteOnlyRequestData' +`, + componentExamples: ` + examples: + ReadWriteOnlyRequestData: + value: + username: user + password: password + user_id: 4321 +`, + errContains: `ReadWriteOnlyRequest: readOnly property "user_id" in request`, + }, + { + name: "invalid_writeonly_response_examples", + readWriteOnlyMediaTypeResponseExample: ` + examples: + ReadWriteOnlyResponse: + $ref: '#/components/examples/ReadWriteOnlyResponseData' +`, + componentExamples: ` + examples: + ReadWriteOnlyResponseData: + value: + password: password + user_id: 4321 +`, + + errContains: `ReadWriteOnlyResponse: writeOnly property "password" in response`, }, } @@ -198,7 +258,28 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/CreateUserResponse" + $ref: "#/components/schemas/CreateUserResponse"`) + spec.WriteString(tc.mediaTypeResponseExample) + spec.WriteString(` + /readWriteOnly: + post: + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ReadWriteOnlyData" + required: true`) + spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample) + spec.WriteString(` + responses: + '201': + description: a response + content: + application/json: + schema: + $ref: "#/components/schemas/ReadWriteOnlyData"`) + spec.WriteString(tc.readWriteOnlyMediaTypeResponseExample) + spec.WriteString(` components: schemas: CreateUserRequest:`) @@ -223,7 +304,6 @@ components: CreateUserResponse:`) spec.WriteString(tc.responseSchemaExample) spec.WriteString(` - description: represents the response to a User creation required: - access_token - user_id @@ -234,6 +314,28 @@ components: format: int64 type: integer type: object + ReadWriteOnlyData: + required: + # only required in request + - username + - password + # only required in response + - user_id + properties: + username: + type: string + default: default + writeOnly: true # only sent in a request + password: + type: string + default: default + writeOnly: true # only sent in a request + user_id: + format: int64 + default: 1 + type: integer + readOnly: true # only returned in a response + type: object `) spec.WriteString(tc.componentExamples) @@ -278,7 +380,7 @@ func TestExampleObjectValidation(t *testing.T) { email: real@email.com password: validpassword `, - errContains: "invalid path /user: invalid operation POST: example and examples are mutually exclusive", + errContains: `invalid path /user: invalid operation POST: example and examples are mutually exclusive`, componentExamples: ` examples: BadUser: @@ -295,7 +397,7 @@ func TestExampleObjectValidation(t *testing.T) { BadUser: description: empty user example `, - errContains: "invalid components: example \"BadUser\": no value or externalValue field", + errContains: `invalid components: example "BadUser": no value or externalValue field`, }, { name: "value_externalValue_mutual_exclusion", @@ -308,7 +410,7 @@ func TestExampleObjectValidation(t *testing.T) { password: validpassword externalValue: 'http://example.com/examples/example' `, - errContains: "invalid components: example \"BadUser\": value and externalValue are mutually exclusive", + errContains: `invalid components: example "BadUser": value and externalValue are mutually exclusive`, }, } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 1ae7c996a..3500334f7 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -88,12 +88,12 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { return errors.New("example and examples are mutually exclusive") } - if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled { return nil } if example := mediaType.Example; example != nil { - if err := validateExampleValue(example, schema.Value); err != nil { + if err := validateExampleValue(ctx, example, schema.Value); err != nil { return fmt.Errorf("invalid example: %w", err) } } @@ -109,7 +109,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { if err := v.Validate(ctx); err != nil { return fmt.Errorf("example %s: %w", k, err) } - if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { + if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil { return fmt.Errorf("example %s: %w", k, err) } } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 73a39a54f..fa07d6555 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -318,11 +318,12 @@ func (parameter *Parameter) Validate(ctx context.Context) error { if parameter.Example != nil && parameter.Examples != nil { return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name) } - if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled { + + if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled { return nil } if example := parameter.Example; example != nil { - if err := validateExampleValue(example, schema.Value); err != nil { + if err := validateExampleValue(ctx, example, schema.Value); err != nil { return fmt.Errorf("invalid example: %w", err) } } else if examples := parameter.Examples; examples != nil { @@ -336,7 +337,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error { if err := v.Validate(ctx); err != nil { return fmt.Errorf("%s: %w", k, err) } - if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { + if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil { return fmt.Errorf("%s: %w", k, err) } } diff --git a/openapi3/request_body.go b/openapi3/request_body.go index c97563a11..d28133c96 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -109,5 +109,10 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error { if requestBody.Content == nil { return errors.New("content of the request body is required") } + + if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled { + vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false + } + return requestBody.Content.Validate(ctx) } diff --git a/openapi3/response.go b/openapi3/response.go index 62361ad74..37325bbb7 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -115,6 +115,9 @@ func (response *Response) Validate(ctx context.Context) error { if response.Description == nil { return errors.New("a short description of the response is required") } + if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled { + vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true + } if content := response.Content; content != nil { if err := content.Validate(ctx); err != nil { diff --git a/openapi3/schema.go b/openapi3/schema.go index b38d25707..596ec04b6 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -769,7 +769,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled { - if err := validateExampleValue(x, schema); err != nil { + if err := validateExampleValue(ctx, x, schema); err != nil { return fmt.Errorf("invalid example: %w", err) } } @@ -1449,6 +1449,8 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return schema.expectedType(settings, TypeObject) } + var me MultiError + if settings.asreq || settings.asrep { properties := make([]string, 0, len(schema.Properties)) for propName := range schema.Properties { @@ -1457,19 +1459,28 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value sort.Strings(properties) for _, propName := range properties { propSchema := schema.Properties[propName] + reqRO := settings.asreq && propSchema.Value.ReadOnly + repWO := settings.asrep && propSchema.Value.WriteOnly + if value[propName] == nil { - if dlft := propSchema.Value.Default; dlft != nil { + if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO { value[propName] = dlft if f := settings.defaultsSet; f != nil { settings.onceSettingDefaults.Do(f) } } } + + if value[propName] != nil { + if reqRO { + me = append(me, fmt.Errorf("readOnly property %q in request", propName)) + } else if repWO { + me = append(me, fmt.Errorf("writeOnly property %q in response", propName)) + } + } } } - var me MultiError - // "properties" properties := schema.Properties lenValue := int64(len(value)) diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 5c0d01d2f..d8900878a 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -5,11 +5,12 @@ import "context" // ValidationOption allows the modification of how the OpenAPI document is validated. type ValidationOption func(options *ValidationOptions) -// ValidationOptions provide configuration for validating OpenAPI documents. +// ValidationOptions provides configuration for validating OpenAPI documents. type ValidationOptions struct { - SchemaFormatValidationEnabled bool - SchemaPatternValidationDisabled bool - ExamplesValidationDisabled bool + SchemaFormatValidationEnabled bool + SchemaPatternValidationDisabled bool + ExamplesValidationDisabled bool + examplesValidationAsReq, examplesValidationAsRes bool } type validationOptionsKey struct{} diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go index 1152ec886..bad6c961a 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -2,8 +2,9 @@ package openapi3filter import ( "bytes" - "encoding/json" + "io" "net/http" + "strings" "testing" "github.com/stretchr/testify/require" @@ -12,82 +13,218 @@ import ( legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) -func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { - const spec = `{ - "openapi": "3.0.3", - "info": { - "version": "1.0.0", - "title": "title", - "description": "desc", - "contact": { - "email": "email" - } - }, - "paths": { - "/accounts": { - "post": { - "description": "Create a new account", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["_id"], - "properties": { - "_id": { - "type": "string", - "description": "Unique identifier for this object.", - "pattern": "[0-9a-v]+$", - "minLength": 20, - "maxLength": 20, - "readOnly": true - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Successfully created a new account" - }, - "400": { - "description": "The server could not understand the request due to invalid syntax", - } - } - } - } - } -} -` +func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { + type testCase struct { + name string + requestSchema string + responseSchema string + requestBody string + responseBody string + responseErrContains string + requestErrContains string + } - type Request struct { - ID string `json:"_id"` + testCases := []testCase{ + { + name: "valid_readonly_in_response_and_valid_writeonly_in_request", + requestSchema: ` + "schema":{ + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "writeOnly": true + } + } + }`, + responseSchema: ` + "schema":{ + "type": "object", + "required": ["access_token"], + "properties": { + "access_token": { + "type": "string", + "readOnly": true + } + } + }`, + requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`, + responseBody: `{"access_token": "abcd"}`, + }, + { + name: "valid_readonly_in_response_and_invalid_readonly_in_request", + requestSchema: ` + "schema":{ + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "readOnly": true + } + } + }`, + responseSchema: ` + "schema":{ + "type": "object", + "required": ["access_token"], + "properties": { + "access_token": { + "type": "string", + "readOnly": true + } + } + }`, + requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`, + responseBody: `{"access_token": "abcd"}`, + requestErrContains: `readOnly property "_id" in request`, + }, + { + name: "invalid_writeonly_in_response_and_valid_writeonly_in_request", + requestSchema: ` + "schema":{ + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "writeOnly": true + } + } + }`, + responseSchema: ` + "schema":{ + "type": "object", + "required": ["access_token"], + "properties": { + "access_token": { + "type": "string", + "writeOnly": true + } + } + }`, + requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`, + responseBody: `{"access_token": "abcd"}`, + responseErrContains: `writeOnly property "access_token" in response`, + }, + { + name: "invalid_writeonly_in_response_and_invalid_readonly_in_request", + requestSchema: ` + "schema":{ + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "readOnly": true + } + } + }`, + responseSchema: ` + "schema":{ + "type": "object", + "required": ["access_token"], + "properties": { + "access_token": { + "type": "string", + "writeOnly": true + } + } + }`, + requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`, + responseBody: `{"access_token": "abcd"}`, + responseErrContains: `writeOnly property "access_token" in response`, + requestErrContains: `readOnly property "_id" in request`, + }, } - sl := openapi3.NewLoader() - doc, err := sl.LoadFromData([]byte(spec)) - require.NoError(t, err) - err = doc.Validate(sl.Context) - require.NoError(t, err) - router, err := legacyrouter.NewRouter(doc) - require.NoError(t, err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + spec := bytes.Buffer{} + spec.WriteString(`{ + "openapi": "3.0.3", + "info": { + "version": "1.0.0", + "title": "title" + }, + "paths": { + "/accounts": { + "post": { + "description": "Create a new account", + "requestBody": { + "required": true, + "content": { + "application/json": {`) + spec.WriteString(tc.requestSchema) + spec.WriteString(`} + } + }, + "responses": { + "201": { + "description": "Successfully created a new account", + "content": { + "application/json": {`) + spec.WriteString(tc.responseSchema) + spec.WriteString(`} + } + }, + "400": { + "description": "The server could not understand the request due to invalid syntax", + } + } + } + } + } + }`) + + sl := openapi3.NewLoader() + doc, err := sl.LoadFromData(spec.Bytes()) + require.NoError(t, err) + err = doc.Validate(sl.Context) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(doc) + require.NoError(t, err) + + httpReq, err := http.NewRequest(http.MethodPost, "/accounts", strings.NewReader(tc.requestBody)) + require.NoError(t, err) + httpReq.Header.Add(headerCT, "application/json") - b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) - require.NoError(t, err) + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) - httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b)) - require.NoError(t, err) - httpReq.Header.Add(headerCT, "application/json") + reqValidationInput := &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } - route, pathParams, err := router.FindRoute(httpReq) - require.NoError(t, err) + if tc.requestSchema != "" { + err = ValidateRequest(sl.Context, reqValidationInput) - err = ValidateRequest(sl.Context, &RequestValidationInput{ - Request: httpReq, - PathParams: pathParams, - Route: route, - }) - require.NoError(t, err) + if tc.requestErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.requestErrContains) + } else { + require.NoError(t, err) + } + } + + if tc.responseSchema != "" { + err = ValidateResponse(sl.Context, &ResponseValidationInput{ + RequestValidationInput: reqValidationInput, + Status: 201, + Header: httpReq.Header, + Body: io.NopCloser(strings.NewReader(tc.responseBody)), + }) + + if tc.responseErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.responseErrContains) + } else { + require.NoError(t, err) + } + } + }) + } } diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index f19123e53..ffb7a1f5a 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -121,7 +121,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error } opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here - opts = append(opts, openapi3.VisitAsRequest()) + opts = append(opts, openapi3.VisitAsResponse()) if options.MultiError { opts = append(opts, openapi3.MultiErrors()) } diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index b9151b878..c1d4630c1 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -372,15 +372,6 @@ func getValidationTests(t *testing.T) []*validationTest { Title: `property "name" is missing`, Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"}}, }, - { - // TODO: Add support for validating readonly properties to upstream validator. - name: "error - readonly object attribute", - args: validationArgs{ - r: newPetstoreRequest(t, http.MethodPost, "/pet", - bytes.NewBufferString(`{"id":213,"name":"Bahama","photoUrls":[]}}`)), - }, - //wantErr: true, - }, { name: "error - wrong attribute type", args: validationArgs{ From 0a4abfc91ea65332d247587bda355ab2ed461ef0 Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Fri, 28 Oct 2022 10:50:20 +0100 Subject: [PATCH 199/260] fix: yaml marshal output (#649) --- .gitattributes | 1 + openapi3/components.go | 2 +- openapi3/discriminator.go | 2 +- openapi3/encoding.go | 2 +- openapi3/example.go | 2 +- openapi3/external_docs.go | 2 +- openapi3/info.go | 6 ++-- openapi3/issue241_test.go | 29 +++++++++++++++++++ openapi3/link.go | 2 +- openapi3/media_type.go | 2 +- openapi3/openapi3.go | 2 +- openapi3/operation.go | 2 +- openapi3/parameter.go | 2 +- openapi3/path_item.go | 2 +- openapi3/refs.go | 53 ++++++++++++++++++++++++++++++++++ openapi3/request_body.go | 2 +- openapi3/response.go | 2 +- openapi3/schema.go | 2 +- openapi3/security_scheme.go | 6 ++-- openapi3/server.go | 4 +-- openapi3/tag.go | 2 +- openapi3/testdata/issue241.yml | 15 ++++++++++ openapi3/xml.go | 2 +- 23 files changed, 122 insertions(+), 24 deletions(-) create mode 100644 .gitattributes create mode 100644 openapi3/issue241_test.go create mode 100644 openapi3/testdata/issue241.yml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..c68529d34 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.yml text eol=lf diff --git a/openapi3/components.go b/openapi3/components.go index b7ad11c80..02ae458f7 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -12,7 +12,7 @@ import ( // Components is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject type Components struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 4cc4df903..28a2148c1 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -9,7 +9,7 @@ import ( // Discriminator is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject type Discriminator struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` PropertyName string `json:"propertyName" yaml:"propertyName"` Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 7bdfaebc8..bc4985cb7 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -11,7 +11,7 @@ import ( // Encoding is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject type Encoding struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` diff --git a/openapi3/example.go b/openapi3/example.go index 2161b688b..561f09b97 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -30,7 +30,7 @@ func (e Examples) JSONLookup(token string) (interface{}, error) { // Example is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject type Example struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index 17a38eec0..75ae0d707 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -12,7 +12,7 @@ import ( // ExternalDocs is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object type ExternalDocs struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` diff --git a/openapi3/info.go b/openapi3/info.go index e60f8882f..fa6593cb4 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -10,7 +10,7 @@ import ( // Info is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject type Info struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -58,7 +58,7 @@ func (info *Info) Validate(ctx context.Context) error { // Contact is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contactObject type Contact struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -83,7 +83,7 @@ func (contact *Contact) Validate(ctx context.Context) error { // License is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#licenseObject type License struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` diff --git a/openapi3/issue241_test.go b/openapi3/issue241_test.go new file mode 100644 index 000000000..14caa9b26 --- /dev/null +++ b/openapi3/issue241_test.go @@ -0,0 +1,29 @@ +package openapi3_test + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestIssue241(t *testing.T) { + data, err := ioutil.ReadFile("testdata/issue241.yml") + require.NoError(t, err) + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + spec, err := loader.LoadFromData(data) + require.NoError(t, err) + + var buf bytes.Buffer + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + err = enc.Encode(spec) + require.NoError(t, err) + require.Equal(t, string(data), buf.String()) +} diff --git a/openapi3/link.go b/openapi3/link.go index 7f0c49d4d..3fb4d78d8 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -30,7 +30,7 @@ var _ jsonpointer.JSONPointable = (*Links)(nil) // Link is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#linkObject type Link struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 3500334f7..1a9bb51e9 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -14,7 +14,7 @@ import ( // MediaType is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject type MediaType struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 963ef722c..510df09a8 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -11,7 +11,7 @@ import ( // T is the root of an OpenAPI v3 document // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oasObject type T struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` OpenAPI string `json:"openapi" yaml:"openapi"` // Required Components Components `json:"components,omitempty" yaml:"components,omitempty"` diff --git a/openapi3/operation.go b/openapi3/operation.go index 832339472..3abc3c4e1 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -14,7 +14,7 @@ import ( // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object type Operation struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` diff --git a/openapi3/parameter.go b/openapi3/parameter.go index fa07d6555..dc82a4980 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -90,7 +90,7 @@ func (parameters Parameters) Validate(ctx context.Context) error { // Parameter is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameterObject type Parameter struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 940b1592a..28ee4b8a8 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -12,7 +12,7 @@ import ( // PathItem is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject type PathItem struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` diff --git a/openapi3/refs.go b/openapi3/refs.go index 0bf737ff7..e85f37e03 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -23,6 +23,11 @@ type CallbackRef struct { var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) +// MarshalYAML returns the YAML encoding of CallbackRef. +func (value *CallbackRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of CallbackRef. func (value *CallbackRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -60,6 +65,11 @@ type ExampleRef struct { var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) +// MarshalYAML returns the YAML encoding of ExampleRef. +func (value *ExampleRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of ExampleRef. func (value *ExampleRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -97,6 +107,11 @@ type HeaderRef struct { var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) +// MarshalYAML returns the YAML encoding of HeaderRef. +func (value *HeaderRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of HeaderRef. func (value *HeaderRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -132,6 +147,11 @@ type LinkRef struct { Value *Link } +// MarshalYAML returns the YAML encoding of LinkRef. +func (value *LinkRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of LinkRef. func (value *LinkRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -159,6 +179,11 @@ type ParameterRef struct { var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) +// MarshalYAML returns the YAML encoding of ParameterRef. +func (value *ParameterRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of ParameterRef. func (value *ParameterRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -196,6 +221,11 @@ type ResponseRef struct { var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) +// MarshalYAML returns the YAML encoding of ResponseRef. +func (value *ResponseRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of ResponseRef. func (value *ResponseRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -233,6 +263,11 @@ type RequestBodyRef struct { var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) +// MarshalYAML returns the YAML encoding of RequestBodyRef. +func (value *RequestBodyRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of RequestBodyRef. func (value *RequestBodyRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -277,6 +312,11 @@ func NewSchemaRef(ref string, value *Schema) *SchemaRef { } } +// MarshalYAML returns the YAML encoding of SchemaRef. +func (value *SchemaRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of SchemaRef. func (value *SchemaRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -314,6 +354,11 @@ type SecuritySchemeRef struct { var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) +// MarshalYAML returns the YAML encoding of SecuritySchemeRef. +func (value *SecuritySchemeRef) MarshalYAML() (interface{}, error) { + return marshalRefYAML(value.Ref, value.Value) +} + // MarshalJSON returns the JSON encoding of SecuritySchemeRef. func (value *SecuritySchemeRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) @@ -341,3 +386,11 @@ func (value SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } + +// marshalRefYAML returns the YAML encoding of ref values. +func marshalRefYAML(value string, otherwise interface{}) (interface{}, error) { + if value != "" { + return &Ref{Ref: value}, nil + } + return otherwise, nil +} diff --git a/openapi3/request_body.go b/openapi3/request_body.go index d28133c96..3e7c0d620 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -30,7 +30,7 @@ func (r RequestBodies) JSONLookup(token string) (interface{}, error) { // RequestBody is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#requestBodyObject type RequestBody struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` diff --git a/openapi3/response.go b/openapi3/response.go index 37325bbb7..287e2909f 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -68,7 +68,7 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) { // Response is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responseObject type Response struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` diff --git a/openapi3/schema.go b/openapi3/schema.go index 596ec04b6..ba846d0cd 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -113,7 +113,7 @@ func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { // Schema is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject type Schema struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 790b21a73..3797389bf 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -30,7 +30,7 @@ var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) // SecurityScheme is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securitySchemeObject type SecurityScheme struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -176,7 +176,7 @@ func (ss *SecurityScheme) Validate(ctx context.Context) error { // OAuthFlows is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowsObject type OAuthFlows struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -223,7 +223,7 @@ func (flows *OAuthFlows) Validate(ctx context.Context) error { // OAuthFlow is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowObject type OAuthFlow struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` diff --git a/openapi3/server.go b/openapi3/server.go index 3a1a7cef9..3f989d857 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -50,7 +50,7 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) // Server is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject type Server struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` URL string `json:"url" yaml:"url"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -197,7 +197,7 @@ func (server *Server) Validate(ctx context.Context) (err error) { // ServerVariable is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object type ServerVariable struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` diff --git a/openapi3/tag.go b/openapi3/tag.go index 8fb5ac36c..b6c24c807 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -32,7 +32,7 @@ func (tags Tags) Validate(ctx context.Context) error { // Tag is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject type Tag struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` diff --git a/openapi3/testdata/issue241.yml b/openapi3/testdata/issue241.yml new file mode 100644 index 000000000..07609c1d8 --- /dev/null +++ b/openapi3/testdata/issue241.yml @@ -0,0 +1,15 @@ +openapi: 3.0.3 +components: + schemas: + FooBar: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte +info: + title: sample + version: version not set +paths: {} diff --git a/openapi3/xml.go b/openapi3/xml.go index 03686ad9a..f1ab96b44 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -9,7 +9,7 @@ import ( // XML is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject type XML struct { - ExtensionProps + ExtensionProps `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` From 9ea22aedcb3f9fa0b06acb08981c11bc8fde4269 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 28 Oct 2022 16:40:47 +0200 Subject: [PATCH 200/260] openapi3filter: add missing response headers validation (#650) --- go.mod | 2 +- go.sum | 9 +- openapi3filter/issue201_test.go | 138 ++++++++++++++++++++++++++++ openapi3filter/validate_response.go | 42 +++++++-- 4 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 openapi3filter/issue201_test.go diff --git a/go.mod b/go.mod index 8d93cf754..942b3195c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/invopop/yaml v0.1.0 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.8.1 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 074de2aa8..4982bc738 100644 --- a/go.sum +++ b/go.sum @@ -22,15 +22,20 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/openapi3filter/issue201_test.go b/openapi3filter/issue201_test.go new file mode 100644 index 000000000..8db8620ae --- /dev/null +++ b/openapi3filter/issue201_test.go @@ -0,0 +1,138 @@ +package openapi3filter + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue201(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + spec := ` +openapi: '3' +info: + version: 1.0.0 + title: Sample API +paths: + /_: + get: + description: '' + responses: + default: + description: '' + content: + application/json: + schema: + type: object + headers: + X-Blip: + description: '' + required: true + schema: + pattern: '^blip$' + x-blop: + description: '' + schema: + pattern: '^blop$' + X-Blap: + description: '' + required: true + schema: + pattern: '^blap$' + X-Blup: + description: '' + required: true + schema: + pattern: '^blup$' +`[1:] + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + for name, testcase := range map[string]struct { + headers map[string]string + err string + }{ + + "no error": { + headers: map[string]string{ + "X-Blip": "blip", + "x-blop": "blop", + "X-Blap": "blap", + "X-Blup": "blup", + }, + }, + + "missing non-required header": { + headers: map[string]string{ + "X-Blip": "blip", + // "x-blop": "blop", + "X-Blap": "blap", + "X-Blup": "blup", + }, + }, + + "missing required header": { + err: `response header "X-Blip" missing`, + headers: map[string]string{ + // "X-Blip": "blip", + "x-blop": "blop", + "X-Blap": "blap", + "X-Blup": "blup", + }, + }, + + "invalid required header": { + err: `response header "X-Blup" doesn't match the schema: string doesn't match the regular expression "^blup$"`, + headers: map[string]string{ + "X-Blip": "blip", + "x-blop": "blop", + "X-Blap": "blap", + "X-Blup": "bluuuuuup", + }, + }, + } { + t.Run(name, func(t *testing.T) { + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + r, err := http.NewRequest(http.MethodGet, `/_`, nil) + require.NoError(t, err) + + r.Header.Add(headerCT, "application/json") + for k, v := range testcase.headers { + r.Header.Add(k, v) + } + + route, pathParams, err := router.FindRoute(r) + require.NoError(t, err) + + err = ValidateResponse(context.Background(), &ResponseValidationInput{ + RequestValidationInput: &RequestValidationInput{ + Request: r, + PathParams: pathParams, + Route: route, + }, + Status: 200, + Header: r.Header, + Body: io.NopCloser(strings.NewReader(`{}`)), + }) + if e := testcase.err; e != "" { + require.ErrorContains(t, err, e) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index ffb7a1f5a..e90b5d60e 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "net/http" + "sort" "github.com/getkin/kin-openapi/openapi3" ) @@ -61,6 +62,39 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error return &ResponseError{Input: input, Reason: "response has not been resolved"} } + opts := make([]openapi3.SchemaValidationOption, 0, 2) + if options.MultiError { + opts = append(opts, openapi3.MultiErrors()) + } + + headers := make([]string, 0, len(response.Headers)) + for k := range response.Headers { + if k != headerCT { + headers = append(headers, k) + } + } + sort.Strings(headers) + for _, k := range headers { + s := response.Headers[k] + h := input.Header.Get(k) + if h == "" { + if s.Value.Required { + return &ResponseError{ + Input: input, + Reason: fmt.Sprintf("response header %q missing", k), + } + } + continue + } + if err := s.Value.Schema.Value.VisitJSON(h, opts...); err != nil { + return &ResponseError{ + Input: input, + Reason: fmt.Sprintf("response header %q doesn't match the schema", k), + Err: err, + } + } + } + if options.ExcludeResponseBody { // A user turned off validation of a response's body. return nil @@ -120,14 +154,8 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error } } - opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here - opts = append(opts, openapi3.VisitAsResponse()) - if options.MultiError { - opts = append(opts, openapi3.MultiErrors()) - } - // Validate data with the schema. - if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { + if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil { return &ResponseError{ Input: input, Reason: "response body doesn't match the schema", From d5c7ac580c544ec93eafaa2b09b25fb8333896de Mon Sep 17 00:00:00 2001 From: Stepan I <2688692+micronull@users.noreply.github.com> Date: Mon, 7 Nov 2022 21:57:59 +0500 Subject: [PATCH 201/260] Fix lost error types in oneOf (#658) --- openapi3/errors.go | 20 ++++++++-- openapi3/issue657_test.go | 79 +++++++++++++++++++++++++++++++++++++++ openapi3/schema.go | 11 +----- 3 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 openapi3/issue657_test.go diff --git a/openapi3/errors.go b/openapi3/errors.go index b530101df..74baab9a5 100644 --- a/openapi3/errors.go +++ b/openapi3/errors.go @@ -10,11 +10,15 @@ import ( type MultiError []error func (me MultiError) Error() string { + return spliceErr(" | ", me) +} + +func spliceErr(sep string, errs []error) string { buff := &bytes.Buffer{} - for i, e := range me { + for i, e := range errs { buff.WriteString(e.Error()) - if i != len(me)-1 { - buff.WriteString(" | ") + if i != len(errs)-1 { + buff.WriteString(sep) } } return buff.String() @@ -43,3 +47,13 @@ func (me MultiError) As(target interface{}) bool { } return false } + +type multiErrorForOneOf MultiError + +func (meo multiErrorForOneOf) Error() string { + return spliceErr(" Or ", meo) +} + +func (meo multiErrorForOneOf) Unwrap() error { + return MultiError(meo) +} diff --git a/openapi3/issue657_test.go b/openapi3/issue657_test.go new file mode 100644 index 000000000..195ccd19c --- /dev/null +++ b/openapi3/issue657_test.go @@ -0,0 +1,79 @@ +package openapi3_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestOneOf_Warning_Errors(t *testing.T) { + t.Parallel() + + loader := openapi3.NewLoader() + spec := ` +components: + schemas: + Something: + type: object + properties: + field: + title: Some field + oneOf: + - title: First rule + type: string + minLength: 10 + maxLength: 10 + - title: Second rule + type: string + minLength: 15 + maxLength: 15 +`[1:] + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + tests := [...]struct { + name string + value string + checkErr require.ErrorAssertionFunc + }{ + { + name: "valid value", + value: "ABCDE01234", + checkErr: require.NoError, + }, + { + name: "valid value", + value: "ABCDE0123456789", + checkErr: require.NoError, + }, + { + name: "no valid value", + value: "ABCDE", + checkErr: func(t require.TestingT, err error, i ...interface{}) { + require.Equal(t, "doesn't match schema due to: minimum string length is 10\nSchema:\n {\n \"maxLength\": 10,\n \"minLength\": 10,\n \"title\": \"First rule\",\n \"type\": \"string\"\n }\n\nValue:\n \"ABCDE\"\n Or minimum string length is 15\nSchema:\n {\n \"maxLength\": 15,\n \"minLength\": 15,\n \"title\": \"Second rule\",\n \"type\": \"string\"\n }\n\nValue:\n \"ABCDE\"\n", err.Error()) + + wErr := &openapi3.MultiError{} + require.ErrorAs(t, err, wErr) + + require.Len(t, *wErr, 2) + + require.Equal(t, "minimum string length is 10", (*wErr)[0].(*openapi3.SchemaError).Reason) + require.Equal(t, "minimum string length is 15", (*wErr)[1].(*openapi3.SchemaError).Reason) + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err = doc.Components.Schemas["Something"].Value.Properties["field"].Value.VisitJSON(test.value) + + test.checkErr(t, err) + }) + } +} diff --git a/openapi3/schema.go b/openapi3/schema.go index ba846d0cd..e5cded877 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -926,7 +926,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val var ( ok = 0 - validationErrors = []error{} + validationErrors = multiErrorForOneOf{} matchedOneOfIdx = 0 tempValue = value ) @@ -955,14 +955,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if ok != 1 { if len(validationErrors) > 1 { - errorMessage := "" - for _, err := range validationErrors { - if errorMessage != "" { - errorMessage += " Or " - } - errorMessage += err.Error() - } - return errors.New("doesn't match schema due to: " + errorMessage) + return fmt.Errorf("doesn't match schema due to: %w", validationErrors) } if settings.failfast { return errSchema From ee909dc7af75ca88e726dfcea18015a202c8c133 Mon Sep 17 00:00:00 2001 From: Omar Ramadan Date: Wed, 9 Nov 2022 02:11:48 -0800 Subject: [PATCH 202/260] Add RegisterBodyEncoder (#656) --- openapi3filter/req_resp_encoder.go | 30 +++++++++++++++-- openapi3filter/req_resp_encoder_test.go | 43 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 openapi3filter/req_resp_encoder_test.go diff --git a/openapi3filter/req_resp_encoder.go b/openapi3filter/req_resp_encoder.go index dd410f588..36b7db6fd 100644 --- a/openapi3filter/req_resp_encoder.go +++ b/openapi3filter/req_resp_encoder.go @@ -16,8 +16,34 @@ func encodeBody(body interface{}, mediaType string) ([]byte, error) { return encoder(body) } -type bodyEncoder func(body interface{}) ([]byte, error) +type BodyEncoder func(body interface{}) ([]byte, error) -var bodyEncoders = map[string]bodyEncoder{ +var bodyEncoders = map[string]BodyEncoder{ "application/json": json.Marshal, } + +func RegisterBodyEncoder(contentType string, encoder BodyEncoder) { + if contentType == "" { + panic("contentType is empty") + } + if encoder == nil { + panic("encoder is not defined") + } + bodyEncoders[contentType] = encoder +} + +// This call is not thread-safe: body encoders should not be created/destroyed by multiple goroutines. +func UnregisterBodyEncoder(contentType string) { + if contentType == "" { + panic("contentType is empty") + } + delete(bodyEncoders, contentType) +} + +// RegisteredBodyEncoder returns the registered body encoder for the given content type. +// +// If no encoder was registered for the given content type, nil is returned. +// This call is not thread-safe: body encoders should not be created/destroyed by multiple goroutines. +func RegisteredBodyEncoder(contentType string) BodyEncoder { + return bodyEncoders[contentType] +} diff --git a/openapi3filter/req_resp_encoder_test.go b/openapi3filter/req_resp_encoder_test.go new file mode 100644 index 000000000..11fe2afa9 --- /dev/null +++ b/openapi3filter/req_resp_encoder_test.go @@ -0,0 +1,43 @@ +package openapi3filter + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRegisterAndUnregisterBodyEncoder(t *testing.T) { + var encoder BodyEncoder + encoder = func(body interface{}) (data []byte, err error) { + return []byte(strings.Join(body.([]string), ",")), nil + } + contentType := "text/csv" + h := make(http.Header) + h.Set(headerCT, contentType) + + originalEncoder := RegisteredBodyEncoder(contentType) + require.Nil(t, originalEncoder) + + RegisterBodyEncoder(contentType, encoder) + require.Equal(t, fmt.Sprintf("%v", encoder), fmt.Sprintf("%v", RegisteredBodyEncoder(contentType))) + + body := []string{"foo", "bar"} + got, err := encodeBody(body, contentType) + + require.NoError(t, err) + require.Equal(t, []byte("foo,bar"), got) + + UnregisterBodyEncoder(contentType) + + originalEncoder = RegisteredBodyEncoder(contentType) + require.Nil(t, originalEncoder) + + _, err = encodeBody(body, contentType) + require.Equal(t, &ParseError{ + Kind: KindUnsupportedFormat, + Reason: prefixUnsupportedCT + ` "text/csv"`, + }, err) +} From bd74bbfb0d36677ee541c476e81d11107d54edc2 Mon Sep 17 00:00:00 2001 From: nk2ge5k Date: Wed, 9 Nov 2022 14:14:00 +0400 Subject: [PATCH 203/260] fix panic slice out of range error #652 (#654) --- openapi3/issue652_test.go | 29 ++++++++++++++++++++ openapi3/loader.go | 22 ++++++++++----- openapi3/testdata/issue652/definitions.yml | 4 +++ openapi3/testdata/issue652/nested/schema.yml | 4 +++ 4 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 openapi3/issue652_test.go create mode 100644 openapi3/testdata/issue652/definitions.yml create mode 100644 openapi3/testdata/issue652/nested/schema.yml diff --git a/openapi3/issue652_test.go b/openapi3/issue652_test.go new file mode 100644 index 000000000..f36e92005 --- /dev/null +++ b/openapi3/issue652_test.go @@ -0,0 +1,29 @@ +package openapi3_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestIssue652(t *testing.T) { + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + + // Test checks that no slice bounds out of range error occurs while loading + // from file that contains reference to file in the parent directory. + require.NotPanics(t, func() { + const schemaName = "ReferenceToParentDirectory" + + spec, err := loader.LoadFromFile("testdata/issue652/nested/schema.yml") + require.NoError(t, err) + require.Contains(t, spec.Components.Schemas, schemaName) + + schema := spec.Components.Schemas[schemaName] + assert.Equal(t, schema.Ref, "../definitions.yml#/components/schemas/TestSchema") + assert.Equal(t, schema.Value.Type, "string") + }) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 1ab693b0f..2bb9999ee 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -742,7 +742,10 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } component.Value = resolved.Value - foundPath := loader.getResolvedRefPath(ref, &resolved, documentPath, componentPath) + foundPath, rerr := loader.getResolvedRefPath(ref, &resolved, documentPath, componentPath) + if rerr != nil { + return fmt.Errorf("failed to resolve file from reference %q: %w", ref, rerr) + } documentPath = loader.documentPathForRecursiveRef(documentPath, foundPath) } } @@ -790,23 +793,28 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return nil } -func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) string { +func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) (string, error) { if referencedFilename := strings.Split(ref, "#")[0]; referencedFilename == "" { if cur != nil { if loader.rootDir != "" && strings.HasPrefix(cur.Path, loader.rootDir) { - return cur.Path[len(loader.rootDir)+1:] + return cur.Path[len(loader.rootDir)+1:], nil } - return path.Base(cur.Path) + return path.Base(cur.Path), nil } - return "" + return "", nil } // ref. to external file if resolved.Ref != "" { - return resolved.Ref + return resolved.Ref, nil } + + if loader.rootDir == "" { + return found.Path, nil + } + // found dest spec. file - return path.Dir(found.Path)[len(loader.rootDir):] + return filepath.Rel(loader.rootDir, found.Path) } func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) { diff --git a/openapi3/testdata/issue652/definitions.yml b/openapi3/testdata/issue652/definitions.yml new file mode 100644 index 000000000..98ef69254 --- /dev/null +++ b/openapi3/testdata/issue652/definitions.yml @@ -0,0 +1,4 @@ +components: + schemas: + TestSchema: + type: string diff --git a/openapi3/testdata/issue652/nested/schema.yml b/openapi3/testdata/issue652/nested/schema.yml new file mode 100644 index 000000000..ef321a101 --- /dev/null +++ b/openapi3/testdata/issue652/nested/schema.yml @@ -0,0 +1,4 @@ +components: + schemas: + ReferenceToParentDirectory: + $ref: "../definitions.yml#/components/schemas/TestSchema" From d89a84e1874cd5d42c39dc3fc0b54f7885fa9453 Mon Sep 17 00:00:00 2001 From: Derbylock Date: Wed, 9 Nov 2022 13:24:56 +0300 Subject: [PATCH 204/260] =?UTF-8?q?Fixed=20recurive=20reference=20resolvin?= =?UTF-8?q?g=20when=20property=20referencies=20local=20co=E2=80=A6=20(#660?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Anton Tolokan --- openapi3/loader.go | 4 ++ openapi3/loader_test.go | 23 ++++++++++ .../testdata/refInLocalRef/messages/data.json | 12 +++++ .../refInLocalRef/messages/dataPart.json | 9 ++++ .../refInLocalRef/messages/request.json | 11 +++++ .../refInLocalRef/messages/response.json | 9 ++++ openapi3/testdata/refInLocalRef/openapi.json | 46 +++++++++++++++++++ .../messages/data.json | 12 +++++ .../messages/dataPart.json | 9 ++++ .../messages/request.json | 11 +++++ .../messages/response.json | 9 ++++ .../spec/openapi.json | 46 +++++++++++++++++++ 12 files changed, 201 insertions(+) create mode 100644 openapi3/testdata/refInLocalRef/messages/data.json create mode 100644 openapi3/testdata/refInLocalRef/messages/dataPart.json create mode 100644 openapi3/testdata/refInLocalRef/messages/request.json create mode 100644 openapi3/testdata/refInLocalRef/messages/response.json create mode 100644 openapi3/testdata/refInLocalRef/openapi.json create mode 100644 openapi3/testdata/refInLocalRefInParentsSubdir/messages/data.json create mode 100644 openapi3/testdata/refInLocalRefInParentsSubdir/messages/dataPart.json create mode 100644 openapi3/testdata/refInLocalRefInParentsSubdir/messages/request.json create mode 100644 openapi3/testdata/refInLocalRefInParentsSubdir/messages/response.json create mode 100644 openapi3/testdata/refInLocalRefInParentsSubdir/spec/openapi.json diff --git a/openapi3/loader.go b/openapi3/loader.go index 2bb9999ee..138431fc0 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -748,6 +748,10 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat } documentPath = loader.documentPathForRecursiveRef(documentPath, foundPath) } + if loader.visitedSchema == nil { + loader.visitedSchema = make(map[*Schema]struct{}) + } + loader.visitedSchema[component.Value] = struct{}{} } value := component.Value if value == nil { diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 684e1b44d..e792767fd 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -263,6 +263,29 @@ func TestLoadWithReferenceInReference(t *testing.T) { require.Equal(t, "string", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) } +func TestLoadWithRecursiveReferenceInLocalReferenceInParentSubdir(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadFromFile("testdata/refInLocalRefInParentsSubdir/spec/openapi.json") + require.NoError(t, err) + 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) +} + +func TestLoadWithRecursiveReferenceInRefrerenceInLocalReference(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadFromFile("testdata/refInLocalRef/openapi.json") + require.NoError(t, err) + 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) +} + func TestLoadWithReferenceInReferenceInProperty(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true diff --git a/openapi3/testdata/refInLocalRef/messages/data.json b/openapi3/testdata/refInLocalRef/messages/data.json new file mode 100644 index 000000000..cfdc18efb --- /dev/null +++ b/openapi3/testdata/refInLocalRef/messages/data.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "ref_prop_part": { + "$ref": "./dataPart.json" + } + } +} diff --git a/openapi3/testdata/refInLocalRef/messages/dataPart.json b/openapi3/testdata/refInLocalRef/messages/dataPart.json new file mode 100644 index 000000000..9ecb5850a --- /dev/null +++ b/openapi3/testdata/refInLocalRef/messages/dataPart.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "idPart": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/openapi3/testdata/refInLocalRef/messages/request.json b/openapi3/testdata/refInLocalRef/messages/request.json new file mode 100644 index 000000000..7225ff190 --- /dev/null +++ b/openapi3/testdata/refInLocalRef/messages/request.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "required": [ + "definition_reference" + ], + "properties": { + "definition_reference": { + "$ref": "./data.json" + } + } +} diff --git a/openapi3/testdata/refInLocalRef/messages/response.json b/openapi3/testdata/refInLocalRef/messages/response.json new file mode 100644 index 000000000..b636f528b --- /dev/null +++ b/openapi3/testdata/refInLocalRef/messages/response.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + } + } +} diff --git a/openapi3/testdata/refInLocalRef/openapi.json b/openapi3/testdata/refInLocalRef/openapi.json new file mode 100644 index 000000000..f0c9915c7 --- /dev/null +++ b/openapi3/testdata/refInLocalRef/openapi.json @@ -0,0 +1,46 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Reference in reference example", + "version": "1.0.0" + }, + "paths": { + "/api/test/ref/in/ref": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties" : { + "data": { + "$ref": "#/components/schemas/Request" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "messages/response.json" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Request": { + "$ref": "messages/request.json" + } + } + } +} diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/data.json b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/data.json new file mode 100644 index 000000000..cfdc18efb --- /dev/null +++ b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/data.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "ref_prop_part": { + "$ref": "./dataPart.json" + } + } +} diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/dataPart.json b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/dataPart.json new file mode 100644 index 000000000..9ecb5850a --- /dev/null +++ b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/dataPart.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "idPart": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/request.json b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/request.json new file mode 100644 index 000000000..7225ff190 --- /dev/null +++ b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/request.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "required": [ + "definition_reference" + ], + "properties": { + "definition_reference": { + "$ref": "./data.json" + } + } +} diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/response.json b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/response.json new file mode 100644 index 000000000..b636f528b --- /dev/null +++ b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/response.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + } + } +} diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/spec/openapi.json b/openapi3/testdata/refInLocalRefInParentsSubdir/spec/openapi.json new file mode 100644 index 000000000..0bf9bd36e --- /dev/null +++ b/openapi3/testdata/refInLocalRefInParentsSubdir/spec/openapi.json @@ -0,0 +1,46 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Reference in reference example", + "version": "1.0.0" + }, + "paths": { + "/api/test/ref/in/ref": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "../messages/request.json" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ref_prop": { + "$ref": "#/components/schemas/Data" + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Data": { + "$ref": "../messages/data.json" + } + } + } +} From 871e029d62a4d47239d11006431407e08edb1259 Mon Sep 17 00:00:00 2001 From: "lgtm-com[bot]" <43144390+lgtm-com[bot]@users.noreply.github.com> Date: Fri, 11 Nov 2022 09:40:14 +0100 Subject: [PATCH 205/260] Add CodeQL workflow for GitHub code scanning (#661) Co-authored-by: LGTM Migrator --- .github/workflows/codeql.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..a09546a40 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: "4 8 * * 4" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ go ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" From cadbddacb449522cd43dc78706eca9a8e554827c Mon Sep 17 00:00:00 2001 From: Chris Reeves Date: Thu, 17 Nov 2022 20:58:31 +0000 Subject: [PATCH 206/260] Support x-nullable (#670) --- openapi2conv/openapi2_conv.go | 15 +++++++++++++++ openapi2conv/openapi2_conv_test.go | 6 ++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 53b4b40cc..8c082053f 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -485,6 +485,16 @@ func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { for i, v := range schema.Value.AllOf { schema.Value.AllOf[i] = ToV3SchemaRef(v) } + if val, ok := schema.Value.Extensions["x-nullable"]; ok { + var nullable bool + + if err := json.Unmarshal(val.(json.RawMessage), &nullable); err == nil { + schema.Value.Nullable = nullable + } + + delete(schema.Value.Extensions, "x-nullable") + } + return schema } @@ -824,6 +834,11 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components for i, v := range schema.Value.AllOf { schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) } + if schema.Value.Nullable { + schema.Value.Nullable = false + schema.Value.Extensions["x-nullable"] = true + } + return schema, nil } diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index 9edce54de..317772152 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -89,7 +89,8 @@ const exampleV2 = ` "additionalProperties": true, "properties": { "foo": { - "type": "string" + "type": "string", + "x-nullable": true }, "quux": { "$ref": "#/definitions/ItemExtension" @@ -463,7 +464,8 @@ const exampleV3 = ` "additionalProperties": true, "properties": { "foo": { - "type": "string" + "type": "string", + "nullable": true }, "quux": { "$ref": "#/components/schemas/ItemExtension" From 657743e5310eb4db0621fd493f0d241764c3f4c9 Mon Sep 17 00:00:00 2001 From: Andriy Borodiychuk Date: Mon, 21 Nov 2022 00:13:42 +0100 Subject: [PATCH 207/260] Update content length after replacing request body (#672) --- openapi3filter/validate_request.go | 1 + openapi3filter/validate_request_test.go | 50 ++++++++++++++++++++----- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 4b0bd3413..beb47aaad 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -285,6 +285,7 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req } // Put the data back into the input req.Body = ioutil.NopCloser(bytes.NewReader(data)) + req.ContentLength = int64(len(data)) } return nil diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index 957b6925e..450ee5988 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -59,6 +59,9 @@ paths: properties: subCategory: type: string + category: + type: string + default: Sweets responses: '201': description: Created @@ -95,6 +98,7 @@ components: type testRequestBody struct { SubCategory string `json:"subCategory"` + Category string `json:"category,omitempty"` } type args struct { requestBody *testRequestBody @@ -102,18 +106,30 @@ components: apiKey string } tests := []struct { - name string - args args - expectedErr error + name string + args args + expectedModification bool + expectedErr error }{ { - name: "Valid request", + name: "Valid request with all fields set", + args: args{ + requestBody: &testRequestBody{SubCategory: "Chocolate", Category: "Food"}, + url: "/category?category=cookies", + apiKey: "SomeKey", + }, + expectedModification: false, + expectedErr: nil, + }, + { + name: "Valid request without certain fields", args: args{ requestBody: &testRequestBody{SubCategory: "Chocolate"}, url: "/category?category=cookies", apiKey: "SomeKey", }, - expectedErr: nil, + expectedModification: true, + expectedErr: nil, }, { name: "Invalid operation params", @@ -122,7 +138,8 @@ components: url: "/category?invalidCategory=badCookie", apiKey: "SomeKey", }, - expectedErr: &RequestError{}, + expectedModification: false, + expectedErr: &RequestError{}, }, { name: "Invalid request body", @@ -131,7 +148,8 @@ components: url: "/category?category=cookies", apiKey: "SomeKey", }, - expectedErr: &RequestError{}, + expectedModification: false, + expectedErr: &RequestError{}, }, { name: "Invalid security", @@ -140,7 +158,8 @@ components: url: "/category?category=cookies", apiKey: "", }, - expectedErr: &SecurityRequirementsError{}, + expectedModification: false, + expectedErr: &SecurityRequirementsError{}, }, { name: "Invalid request body and security", @@ -149,16 +168,19 @@ components: url: "/category?category=cookies", apiKey: "", }, - expectedErr: &SecurityRequirementsError{}, + expectedModification: false, + expectedErr: &SecurityRequirementsError{}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var requestBody io.Reader + var originalBodySize int if tc.args.requestBody != nil { testingBody, err := json.Marshal(tc.args.requestBody) require.NoError(t, err) requestBody = bytes.NewReader(testingBody) + originalBodySize = len(testingBody) } req, err := http.NewRequest(http.MethodPost, tc.args.url, requestBody) require.NoError(t, err) @@ -180,6 +202,16 @@ components: } err = ValidateRequest(context.Background(), validationInput) assert.IsType(t, tc.expectedErr, err, "ValidateRequest(): error = %v, expectedError %v", err, tc.expectedErr) + if tc.expectedErr != nil { + return + } + body, err := io.ReadAll(validationInput.Request.Body) + contentLen := int(validationInput.Request.ContentLength) + bodySize := len(body) + assert.NoError(t, err, "unable to read request body: %v", err) + assert.Equal(t, contentLen, bodySize, "expect ContentLength %d to equal body size %d", contentLen, bodySize) + bodyModified := originalBodySize != bodySize + assert.Equal(t, bodyModified, tc.expectedModification, "expect request body modification happened: %t, expected %t", bodyModified, tc.expectedModification) }) } } From 4a7405dd77894e637300b94035f3ecfde3fe286a Mon Sep 17 00:00:00 2001 From: orensolo <46680749+orensolo@users.noreply.github.com> Date: Tue, 22 Nov 2022 16:42:00 +0200 Subject: [PATCH 208/260] fix: optional defaults (#662) --- openapi3filter/issue639_test.go | 100 +++++++++++++++++++++++++++++ openapi3filter/options.go | 4 ++ openapi3filter/validate_request.go | 4 +- 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 openapi3filter/issue639_test.go diff --git a/openapi3filter/issue639_test.go b/openapi3filter/issue639_test.go new file mode 100644 index 000000000..2caf1bd14 --- /dev/null +++ b/openapi3filter/issue639_test.go @@ -0,0 +1,100 @@ +package openapi3filter + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue639(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + spec := ` + openapi: 3.0.0 + info: + version: 1.0.0 + title: Sample API + paths: + /items: + put: + requestBody: + content: + application/json: + schema: + properties: + testWithdefault: + default: false + type: boolean + testNoDefault: + type: boolean + type: object + responses: + '200': + description: Successful respons +`[1:] + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + tests := []struct { + name string + options *Options + expectedDefaultVal interface{} + }{ + { + name: "no defaults are added to requests", + options: &Options{ + SkipSettingDefaults: true, + }, + expectedDefaultVal: nil, + }, + + { + name: "defaults are added to requests", + expectedDefaultVal: false, + }, + } + + for _, testcase := range tests { + t.Run(testcase.name, func(t *testing.T) { + body := "{\"testNoDefault\": true}" + httpReq, err := http.NewRequest(http.MethodPut, "/items", strings.NewReader(body)) + require.NoError(t, err) + httpReq.Header.Set("Content-Type", "application/json") + require.NoError(t, err) + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + requestValidationInput := &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + Options: testcase.options, + } + err = ValidateRequest(ctx, requestValidationInput) + require.NoError(t, err) + bodyAfterValidation, err := ioutil.ReadAll(httpReq.Body) + require.NoError(t, err) + + raw := map[string]interface{}{} + err = json.Unmarshal(bodyAfterValidation, &raw) + require.NoError(t, err) + require.Equal(t, testcase.expectedDefaultVal, + raw["testWithdefault"], "default value must not be included") + }) + } +} diff --git a/openapi3filter/options.go b/openapi3filter/options.go index 1622339e2..14843dd1b 100644 --- a/openapi3filter/options.go +++ b/openapi3filter/options.go @@ -21,4 +21,8 @@ type Options struct { // See NoopAuthenticationFunc AuthenticationFunc AuthenticationFunc + + // Indicates whether default values are set in the + // request. If true, then they are not set + SkipSettingDefaults bool } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index beb47aaad..83bea98ad 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -258,7 +258,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req defaultsSet := false opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential opts here opts = append(opts, openapi3.VisitAsRequest()) - opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true })) + if !options.SkipSettingDefaults { + opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true })) + } if options.MultiError { opts = append(opts, openapi3.MultiErrors()) } From 0d3c1791c081407d63391f0e526c21b50b140f2b Mon Sep 17 00:00:00 2001 From: Stepan I Date: Tue, 22 Nov 2022 19:51:37 +0500 Subject: [PATCH 209/260] fix: wrap the error that came back from the callback (#674) (#675) --- openapi3/schema.go | 14 ++++++++------ openapi3/schema_formats_test.go | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index e5cded877..1d77e816a 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1298,29 +1298,31 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } // "format" - var formatErr string + var formatStrErr string + var formatErr error if format := schema.Format; format != "" { if f, ok := SchemaStringFormats[format]; ok { switch { case f.regexp != nil && f.callback == nil: if cp := f.regexp; !cp.MatchString(value) { - formatErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String()) + formatStrErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String()) } case f.regexp == nil && f.callback != nil: if err := f.callback(value); err != nil { - formatErr = err.Error() + formatErr = err } default: - formatErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format) + formatStrErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format) } } } - if formatErr != "" { + if formatStrErr != "" || formatErr != nil { err := &SchemaError{ Value: value, Schema: schema, SchemaField: "format", - Reason: formatErr, + Reason: formatStrErr, + Origin: formatErr, } if !settings.multiError { return err diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go index 5cceb8cf0..eeb7894a7 100644 --- a/openapi3/schema_formats_test.go +++ b/openapi3/schema_formats_test.go @@ -2,8 +2,10 @@ package openapi3 import ( "context" + "errors" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -58,3 +60,18 @@ func TestIssue430(t *testing.T) { } } } + +func TestFormatCallback_WrapError(t *testing.T) { + var errSomething = errors.New("something error") + + DefineStringFormatCallback("foobar", func(value string) error { + return errSomething + }) + + s := &Schema{Format: "foobar"} + err := s.VisitJSONString("blablabla") + + assert.ErrorIs(t, err, errSomething) + + delete(SchemaStringFormats, "foobar") +} From cd217fe551cc36fcc74326076ec9fa0a38cc0920 Mon Sep 17 00:00:00 2001 From: Stepan I Date: Tue, 22 Nov 2022 20:05:37 +0500 Subject: [PATCH 210/260] fix: openapi3.SchemaError message customize (#678) (#679) Co-authored-by: Pierre Fenoll --- openapi3/schema.go | 247 +++++++++++--------- openapi3/schema_validation_settings.go | 8 + openapi3/schema_validation_settings_test.go | 36 +++ openapi3filter/options.go | 13 ++ openapi3filter/options_test.go | 83 +++++++ openapi3filter/validate_request.go | 6 + openapi3filter/validate_response.go | 3 + 7 files changed, 289 insertions(+), 107 deletions(-) create mode 100644 openapi3/schema_validation_settings_test.go create mode 100644 openapi3filter/options_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 1d77e816a..600cb9de2 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -861,10 +861,11 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf } } return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "type", - Reason: fmt.Sprintf("unhandled value of type %T", value), + Value: value, + Schema: schema, + SchemaField: "type", + Reason: fmt.Sprintf("unhandled value of type %T", value), + customizeMessageError: settings.customizeMessageError, } } @@ -879,10 +880,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return errSchema } return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "enum", - Reason: "value is not one of the allowed values", + Value: value, + Schema: schema, + SchemaField: "enum", + Reason: "value is not one of the allowed values", + customizeMessageError: settings.customizeMessageError, } } @@ -896,9 +898,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return errSchema } return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "not", + Value: value, + Schema: schema, + SchemaField: "not", + customizeMessageError: settings.customizeMessageError, } } } @@ -961,9 +964,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return errSchema } e := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "oneOf", + Value: value, + Schema: schema, + SchemaField: "oneOf", + customizeMessageError: settings.customizeMessageError, } if ok > 1 { e.Origin = ErrOneOfConflict @@ -1005,9 +1009,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return errSchema } return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "anyOf", + Value: value, + Schema: schema, + SchemaField: "anyOf", + customizeMessageError: settings.customizeMessageError, } } @@ -1024,10 +1029,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return errSchema } return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "allOf", - Origin: err, + Value: value, + Schema: schema, + SchemaField: "allOf", + Origin: err, + customizeMessageError: settings.customizeMessageError, } } } @@ -1042,10 +1048,11 @@ func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err err return errSchema } return &SchemaError{ - Value: nil, - Schema: schema, - SchemaField: "nullable", - Reason: "Value is not nullable", + Value: nil, + Schema: schema, + SchemaField: "nullable", + Reason: "Value is not nullable", + customizeMessageError: settings.customizeMessageError, } } @@ -1075,10 +1082,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "type", - Reason: "Value must be an integer", + Value: value, + Schema: schema, + SchemaField: "type", + Reason: "Value must be an integer", + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1110,10 +1118,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "format", - Reason: fmt.Sprintf("number must be an %s", schema.Format), + Value: value, + Schema: schema, + SchemaField: "format", + Reason: fmt.Sprintf("number must be an %s", schema.Format), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1128,10 +1137,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "exclusiveMinimum", - Reason: fmt.Sprintf("number must be more than %g", *schema.Min), + Value: value, + Schema: schema, + SchemaField: "exclusiveMinimum", + Reason: fmt.Sprintf("number must be more than %g", *schema.Min), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1145,10 +1155,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "exclusiveMaximum", - Reason: fmt.Sprintf("number must be less than %g", *schema.Max), + Value: value, + Schema: schema, + SchemaField: "exclusiveMaximum", + Reason: fmt.Sprintf("number must be less than %g", *schema.Max), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1162,10 +1173,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minimum", - Reason: fmt.Sprintf("number must be at least %g", *v), + Value: value, + Schema: schema, + SchemaField: "minimum", + Reason: fmt.Sprintf("number must be at least %g", *v), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1179,10 +1191,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maximum", - Reason: fmt.Sprintf("number must be at most %g", *v), + Value: value, + Schema: schema, + SchemaField: "maximum", + Reason: fmt.Sprintf("number must be at most %g", *v), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1199,9 +1212,10 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "multipleOf", + Value: value, + Schema: schema, + SchemaField: "multipleOf", + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1247,10 +1261,11 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minLength", - Reason: fmt.Sprintf("minimum string length is %d", minLength), + Value: value, + Schema: schema, + SchemaField: "minLength", + Reason: fmt.Sprintf("minimum string length is %d", minLength), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1262,10 +1277,11 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxLength", - Reason: fmt.Sprintf("maximum string length is %d", *maxLength), + Value: value, + Schema: schema, + SchemaField: "maxLength", + Reason: fmt.Sprintf("maximum string length is %d", *maxLength), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1286,10 +1302,11 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } if cp := schema.compiledPattern; cp != nil && !cp.MatchString(value) { err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "pattern", - Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern), + Value: value, + Schema: schema, + SchemaField: "pattern", + Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1318,11 +1335,12 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } if formatStrErr != "" || formatErr != nil { err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "format", - Reason: formatStrErr, - Origin: formatErr, + Value: value, + Schema: schema, + SchemaField: "format", + Reason: formatStrErr, + Origin: formatErr, + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1358,10 +1376,11 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minItems", - Reason: fmt.Sprintf("minimum number of items is %d", v), + Value: value, + Schema: schema, + SchemaField: "minItems", + Reason: fmt.Sprintf("minimum number of items is %d", v), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1375,10 +1394,11 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxItems", - Reason: fmt.Sprintf("maximum number of items is %d", *v), + Value: value, + Schema: schema, + SchemaField: "maxItems", + Reason: fmt.Sprintf("maximum number of items is %d", *v), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1395,10 +1415,11 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "uniqueItems", - Reason: "duplicate items found", + Value: value, + Schema: schema, + SchemaField: "uniqueItems", + Reason: "duplicate items found", + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1486,10 +1507,11 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minProperties", - Reason: fmt.Sprintf("there must be at least %d properties", v), + Value: value, + Schema: schema, + SchemaField: "minProperties", + Reason: fmt.Sprintf("there must be at least %d properties", v), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1503,10 +1525,11 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxProperties", - Reason: fmt.Sprintf("there must be at most %d properties", *v), + Value: value, + Schema: schema, + SchemaField: "maxProperties", + Reason: fmt.Sprintf("there must be at most %d properties", *v), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1574,10 +1597,11 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return errSchema } err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "properties", - Reason: fmt.Sprintf("property %q is unsupported", k), + Value: value, + Schema: schema, + SchemaField: "properties", + Reason: fmt.Sprintf("property %q is unsupported", k), + customizeMessageError: settings.customizeMessageError, } if !settings.multiError { return err @@ -1598,10 +1622,11 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return errSchema } err := markSchemaErrorKey(&SchemaError{ - Value: value, - Schema: schema, - SchemaField: "required", - Reason: fmt.Sprintf("property %q is missing", k), + Value: value, + Schema: schema, + SchemaField: "required", + Reason: fmt.Sprintf("property %q is missing", k), + customizeMessageError: settings.customizeMessageError, }, k) if !settings.multiError { return err @@ -1622,10 +1647,11 @@ func (schema *Schema) expectedType(settings *schemaValidationSettings, typ strin return errSchema } return &SchemaError{ - Value: typ, - Schema: schema, - SchemaField: "type", - Reason: "Field must be set to " + schema.Type + " or not be present", + Value: typ, + Schema: schema, + SchemaField: "type", + Reason: "Field must be set to " + schema.Type + " or not be present", + customizeMessageError: settings.customizeMessageError, } } @@ -1641,12 +1667,13 @@ func (schema *Schema) compilePattern() (err error) { } type SchemaError struct { - Value interface{} - reversePath []string - Schema *Schema - SchemaField string - Reason string - Origin error + Value interface{} + reversePath []string + Schema *Schema + SchemaField string + Reason string + Origin error + customizeMessageError func(err *SchemaError) string } var _ interface{ Unwrap() error } = SchemaError{} @@ -1689,6 +1716,12 @@ func (err *SchemaError) JSONPointer() []string { } func (err *SchemaError) Error() string { + if err.customizeMessageError != nil { + if msg := err.customizeMessageError(err); msg != "" { + return msg + } + } + if err.Origin != nil { return err.Origin.Error() } diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 854ae8480..5a28c8d8d 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -16,6 +16,8 @@ type schemaValidationSettings struct { onceSettingDefaults sync.Once defaultsSet func() + + customizeMessageError func(err *SchemaError) string } // FailFast returns schema validation errors quicker. @@ -50,6 +52,12 @@ func DefaultsSet(f func()) SchemaValidationOption { return func(s *schemaValidationSettings) { s.defaultsSet = f } } +// SetSchemaErrorMessageCustomizer allows to override the schema error message. +// If the passed function returns an empty string, it returns to the previous Error() implementation. +func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaValidationOption { + return func(s *schemaValidationSettings) { s.customizeMessageError = f } +} + func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings { settings := &schemaValidationSettings{} for _, opt := range opts { diff --git a/openapi3/schema_validation_settings_test.go b/openapi3/schema_validation_settings_test.go new file mode 100644 index 000000000..db52d3bdf --- /dev/null +++ b/openapi3/schema_validation_settings_test.go @@ -0,0 +1,36 @@ +package openapi3_test + +import ( + "fmt" + + "github.com/getkin/kin-openapi/openapi3" +) + +func ExampleSetSchemaErrorMessageCustomizer() { + loader := openapi3.NewLoader() + spc := ` +components: + schemas: + Something: + type: object + properties: + field: + title: Some field + type: string +`[1:] + + doc, err := loader.LoadFromData([]byte(spc)) + if err != nil { + panic(err) + } + + opt := openapi3.SetSchemaErrorMessageCustomizer(func(err *openapi3.SchemaError) string { + return fmt.Sprintf(`field "%s" should be string`, err.Schema.Title) + }) + + err = doc.Components.Schemas["Something"].Value.Properties["field"].Value.VisitJSON(123, opt) + + fmt.Println(err.Error()) + + // Output: field "Some field" should be string +} diff --git a/openapi3filter/options.go b/openapi3filter/options.go index 14843dd1b..14c35d5da 100644 --- a/openapi3filter/options.go +++ b/openapi3filter/options.go @@ -1,5 +1,7 @@ package openapi3filter +import "github.com/getkin/kin-openapi/openapi3" + // DefaultOptions do not set an AuthenticationFunc. // A spec with security schemes defined will not pass validation // unless an AuthenticationFunc is defined. @@ -25,4 +27,15 @@ type Options struct { // Indicates whether default values are set in the // request. If true, then they are not set SkipSettingDefaults bool + + customSchemaErrorFunc CustomSchemaErrorFunc +} + +// CustomSchemaErrorFunc allows for custom the schema error message. +type CustomSchemaErrorFunc func(err *openapi3.SchemaError) string + +// WithCustomSchemaErrorFunc sets a function to override the schema error message. +// If the passed function returns an empty string, it returns to the previous Error() implementation. +func (o *Options) WithCustomSchemaErrorFunc(f CustomSchemaErrorFunc) { + o.customSchemaErrorFunc = f } diff --git a/openapi3filter/options_test.go b/openapi3filter/options_test.go new file mode 100644 index 000000000..12737114d --- /dev/null +++ b/openapi3filter/options_test.go @@ -0,0 +1,83 @@ +package openapi3filter_test + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func ExampleOptions_WithCustomSchemaErrorFunc() { + const spec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: 0.0.1 +paths: + /some: + post: + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + field: + title: Some field + type: integer + responses: + '200': + description: Created +` + + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + if err != nil { + panic(err) + } + + err = doc.Validate(loader.Context) + if err != nil { + panic(err) + } + + router, err := gorillamux.NewRouter(doc) + if err != nil { + panic(err) + } + + opts := &openapi3filter.Options{} + + opts.WithCustomSchemaErrorFunc(func(err *openapi3.SchemaError) string { + return fmt.Sprintf(`field "%s" must be an integer`, err.Schema.Title) + }) + + req, err := http.NewRequest(http.MethodPost, "/some", strings.NewReader(`{"field":"not integer"}`)) + if err != nil { + panic(err) + } + + req.Header.Add("Content-Type", "application/json") + + route, pathParams, err := router.FindRoute(req) + if err != nil { + panic(err) + } + + validationInput := &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + Options: opts, + } + err = openapi3filter.ValidateRequest(context.Background(), validationInput) + + fmt.Println(err.Error()) + + // Output: request body has an error: doesn't match the schema: field "Some field" must be an integer +} diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 83bea98ad..4acb9ff1f 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -178,6 +178,9 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param opts = make([]openapi3.SchemaValidationOption, 0, 1) opts = append(opts, openapi3.MultiErrors()) } + if options.customSchemaErrorFunc != nil { + opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc)) + } if err = schema.VisitJSON(value, opts...); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } @@ -264,6 +267,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req if options.MultiError { opts = append(opts, openapi3.MultiErrors()) } + if options.customSchemaErrorFunc != nil { + opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc)) + } // Validate JSON with the schema if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index e90b5d60e..abcbb4e9d 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -66,6 +66,9 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error if options.MultiError { opts = append(opts, openapi3.MultiErrors()) } + if options.customSchemaErrorFunc != nil { + opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc)) + } headers := make([]string, 0, len(response.Headers)) for k := range response.Headers { From 8a660101d5f0d19cc7350f760c6d5c602d5eec0c Mon Sep 17 00:00:00 2001 From: orensolo <46680749+orensolo@users.noreply.github.com> Date: Thu, 24 Nov 2022 19:05:15 +0200 Subject: [PATCH 211/260] openapi3filter: fix crash when given arrays of objects as query parameters (#664) --- openapi3filter/issue625_test.go | 123 +++++++++++++++++++++++++++++ openapi3filter/req_resp_decoder.go | 84 +++++++++++++++++++- 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 openapi3filter/issue625_test.go diff --git a/openapi3filter/issue625_test.go b/openapi3filter/issue625_test.go new file mode 100644 index 000000000..f00940b26 --- /dev/null +++ b/openapi3filter/issue625_test.go @@ -0,0 +1,123 @@ +package openapi3filter_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue625(t *testing.T) { + + anyOfArraySpec := ` +openapi: 3.0.0 +info: + version: 1.0.0 + title: Sample API +paths: + /items: + get: + description: Returns a list of stuff + parameters: + - description: test object + explode: false + in: query + name: test + required: false + schema: + type: array + items: + anyOf: + - type: integer + - type: boolean + responses: + '200': + description: Successful response +`[1:] + + oneOfArraySpec := strings.ReplaceAll(anyOfArraySpec, "anyOf", "oneOf") + + allOfArraySpec := strings.ReplaceAll(strings.ReplaceAll(anyOfArraySpec, "anyOf", "allOf"), + "type: boolean", "type: number") + + tests := []struct { + name string + spec string + req string + errStr string + }{ + { + name: "success anyof object array", + spec: anyOfArraySpec, + req: "/items?test=3,7", + }, + { + name: "failed anyof object array", + spec: anyOfArraySpec, + req: "/items?test=s1,s2", + errStr: `parameter "test" in query has an error: path 0: value s1: an invalid boolean: invalid syntax`, + }, + + { + name: "success allof object array", + spec: allOfArraySpec, + req: `/items?test=1,3`, + }, + { + name: "failed allof object array", + spec: allOfArraySpec, + req: `/items?test=1.2,3.1`, + errStr: `parameter "test" in query has an error: Value must be an integer`, + }, + { + name: "success oneof object array", + spec: oneOfArraySpec, + req: `/items?test=true,3`, + }, + { + name: "faled oneof object array", + spec: oneOfArraySpec, + req: `/items?test="val1","val2"`, + errStr: `parameter "test" in query has an error: item 0: decoding oneOf failed: 0 schemas matched`, + }, + } + + for _, testcase := range tests { + t.Run(testcase.name, func(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + + doc, err := loader.LoadFromData([]byte(testcase.spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + httpReq, err := http.NewRequest(http.MethodGet, testcase.req, nil) + require.NoError(t, err) + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + requestValidationInput := &openapi3filter.RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } + err = openapi3filter.ValidateRequest(ctx, requestValidationInput) + if testcase.errStr == "" { + require.NoError(t, err) + } else { + require.Contains(t, err.Error(), testcase.errStr) + } + }, + ) + } +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index ca8194432..300b2705f 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -526,10 +526,92 @@ func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationM } values = strings.Split(values[0], delim) } - val, err := parseArray(values, schema) + val, err := d.parseArray(values, sm, schema) return val, ok, err } +// parseArray returns an array that contains items from a raw array. +// Every item is parsed as a primitive value. +// The function returns an error when an error happened while parse array's items. +func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMethod, schemaRef *openapi3.SchemaRef) ([]interface{}, error) { + var value []interface{} + + for i, v := range raw { + item, err := d.parseValue(v, schemaRef.Value.Items) + if err != nil { + if v, ok := err.(*ParseError); ok { + return nil, &ParseError{path: []interface{}{i}, Cause: v} + } + return nil, fmt.Errorf("item %d: %w", i, err) + } + + // If the items are nil, then the array is nil. There shouldn't be case where some values are actual primitive + // values and some are nil values. + if item == nil { + return nil, nil + } + value = append(value, item) + } + return value, nil +} + +func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (interface{}, error) { + if len(schema.Value.AllOf) > 0 { + var value interface{} + var err error + for _, sr := range schema.Value.AllOf { + value, err = d.parseValue(v, sr) + if value == nil || err != nil { + break + } + } + return value, err + } + + if len(schema.Value.AnyOf) > 0 { + var value interface{} + var err error + for _, sr := range schema.Value.AnyOf { + value, err = d.parseValue(v, sr) + if err == nil { + return value, nil + } + } + + return nil, err + } + + if len(schema.Value.OneOf) > 0 { + isMatched := 0 + var value interface{} + var err error + for _, sr := range schema.Value.OneOf { + result, err := d.parseValue(v, sr) + if err == nil { + value = result + isMatched++ + } + } + if isMatched == 1 { + return value, nil + } else if isMatched > 1 { + return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched) + } else if isMatched == 0 { + return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched) + } + + return nil, err + } + + if schema.Value.Not != nil { + // TODO(decode not): handle decoding "not" JSON Schema + return nil, errors.New("not implemented: decoding 'not'") + } + + return parsePrimitive(v, schema) + +} + func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { var propsFn func(url.Values) (map[string]string, error) switch sm.Style { From 83dd2ffdbd4b885924a51278dc18657a2d7cd5ee Mon Sep 17 00:00:00 2001 From: Stepan I Date: Mon, 28 Nov 2022 14:45:11 +0500 Subject: [PATCH 212/260] fix: error path is lost (#681) (#682) --- openapi3/schema.go | 14 ++++++++++---- openapi3/schema_formats_test.go | 30 ++++++++++++++++++++++++++++++ openapi3/schema_issue289_test.go | 2 +- openapi3filter/issue625_test.go | 2 +- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 600cb9de2..eb016734d 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1722,11 +1722,8 @@ func (err *SchemaError) Error() string { } } - if err.Origin != nil { - return err.Origin.Error() - } - buf := bytes.NewBuffer(make([]byte, 0, 256)) + if len(err.reversePath) > 0 { buf.WriteString(`Error at "`) reversePath := err.reversePath @@ -1736,6 +1733,13 @@ func (err *SchemaError) Error() string { } buf.WriteString(`": `) } + + if err.Origin != nil { + buf.WriteString(err.Origin.Error()) + + return buf.String() + } + reason := err.Reason if reason == "" { buf.WriteString(`Doesn't match schema "`) @@ -1744,6 +1748,7 @@ func (err *SchemaError) Error() string { } else { buf.WriteString(reason) } + if !SchemaErrorDetailsDisabled { buf.WriteString("\nSchema:\n ") encoder := json.NewEncoder(buf) @@ -1756,6 +1761,7 @@ func (err *SchemaError) Error() string { panic(err) } } + return buf.String() } diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go index eeb7894a7..3899fabdc 100644 --- a/openapi3/schema_formats_test.go +++ b/openapi3/schema_formats_test.go @@ -75,3 +75,33 @@ func TestFormatCallback_WrapError(t *testing.T) { delete(SchemaStringFormats, "foobar") } + +func TestReversePathInMessageSchemaError(t *testing.T) { + DefineIPv4Format() + + SchemaErrorDetailsDisabled = true + + const spc = ` +components: + schemas: + Something: + type: object + properties: + ip: + type: string + format: ipv4 +` + l := NewLoader() + + doc, err := l.LoadFromData([]byte(spc)) + require.NoError(t, err) + + err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]interface{}{ + `ip`: `123.0.0.11111`, + }) + + require.EqualError(t, err, `Error at "/ip": Not an IP address`) + + delete(SchemaStringFormats, "ipv4") + SchemaErrorDetailsDisabled = false +} diff --git a/openapi3/schema_issue289_test.go b/openapi3/schema_issue289_test.go index 6ab6b63d5..56d1d4562 100644 --- a/openapi3/schema_issue289_test.go +++ b/openapi3/schema_issue289_test.go @@ -35,5 +35,5 @@ openapi: "3.0.1" "name": "kin-openapi", "address": "127.0.0.1", }) - require.EqualError(t, err, ErrOneOfConflict.Error()) + require.ErrorIs(t, err, ErrOneOfConflict) } diff --git a/openapi3filter/issue625_test.go b/openapi3filter/issue625_test.go index f00940b26..f0713436a 100644 --- a/openapi3filter/issue625_test.go +++ b/openapi3filter/issue625_test.go @@ -72,7 +72,7 @@ paths: name: "failed allof object array", spec: allOfArraySpec, req: `/items?test=1.2,3.1`, - errStr: `parameter "test" in query has an error: Value must be an integer`, + errStr: `parameter "test" in query has an error: Error at "/0": Value must be an integer`, }, { name: "success oneof object array", From 8165c43f09727c4e97c8f0d1798239d40064af32 Mon Sep 17 00:00:00 2001 From: Eloy Coto Date: Tue, 29 Nov 2022 01:51:04 +0100 Subject: [PATCH 213/260] feat: formatting some error messages (#684) --- openapi3/issue136_test.go | 2 +- openapi3/schema.go | 8 +++---- openapi3filter/issue201_test.go | 2 +- openapi3filter/issue625_test.go | 2 +- openapi3filter/unpack_errors_test.go | 4 ++-- openapi3filter/validation_error_test.go | 28 ++++++++++++------------- routers/gorillamux/example_test.go | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/openapi3/issue136_test.go b/openapi3/issue136_test.go index 36c8b8588..b5e9eebe5 100644 --- a/openapi3/issue136_test.go +++ b/openapi3/issue136_test.go @@ -31,7 +31,7 @@ components: }, { dflt: `1`, - err: "invalid components: invalid schema default: Field must be set to string or not be present", + err: "invalid components: invalid schema default: field must be set to string or not be present", }, } { t.Run(testcase.dflt, func(t *testing.T) { diff --git a/openapi3/schema.go b/openapi3/schema.go index eb016734d..081873edb 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -883,7 +883,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Value: value, Schema: schema, SchemaField: "enum", - Reason: "value is not one of the allowed values", + Reason: fmt.Sprintf("value %q is not one of the allowed values", value), customizeMessageError: settings.customizeMessageError, } } @@ -1085,7 +1085,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "type", - Reason: "Value must be an integer", + Reason: fmt.Sprintf("value \"%g\" must be an integer", value), customizeMessageError: settings.customizeMessageError, } if !settings.multiError { @@ -1305,7 +1305,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "pattern", - Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern), + Reason: fmt.Sprintf(`string %q doesn't match the regular expression "%s"`, value, schema.Pattern), customizeMessageError: settings.customizeMessageError, } if !settings.multiError { @@ -1650,7 +1650,7 @@ func (schema *Schema) expectedType(settings *schemaValidationSettings, typ strin Value: typ, Schema: schema, SchemaField: "type", - Reason: "Field must be set to " + schema.Type + " or not be present", + Reason: fmt.Sprintf("field must be set to %s or not be present", schema.Type), customizeMessageError: settings.customizeMessageError, } } diff --git a/openapi3filter/issue201_test.go b/openapi3filter/issue201_test.go index 8db8620ae..8b2b99d0e 100644 --- a/openapi3filter/issue201_test.go +++ b/openapi3filter/issue201_test.go @@ -94,7 +94,7 @@ paths: }, "invalid required header": { - err: `response header "X-Blup" doesn't match the schema: string doesn't match the regular expression "^blup$"`, + err: `response header "X-Blup" doesn't match the schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`, headers: map[string]string{ "X-Blip": "blip", "x-blop": "blop", diff --git a/openapi3filter/issue625_test.go b/openapi3filter/issue625_test.go index f0713436a..d9e5bae47 100644 --- a/openapi3filter/issue625_test.go +++ b/openapi3filter/issue625_test.go @@ -72,7 +72,7 @@ paths: name: "failed allof object array", spec: allOfArraySpec, req: `/items?test=1.2,3.1`, - errStr: `parameter "test" in query has an error: Error at "/0": Value must be an integer`, + errStr: `parameter "test" in query has an error: Error at "/0": value "1.2" must be an integer`, }, { name: "success oneof object array", diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go index 552e20ef8..0ee48ad39 100644 --- a/openapi3filter/unpack_errors_test.go +++ b/openapi3filter/unpack_errors_test.go @@ -81,7 +81,7 @@ func Example() { // Output: // ===== Start New Error ===== // @body.name: - // Error at "/name": Field must be set to string or not be present + // Error at "/name": field must be set to string or not be present // Schema: // { // "example": "doggie", @@ -93,7 +93,7 @@ func Example() { // // ===== Start New Error ===== // @body.status: - // Error at "/status": value is not one of the allowed values + // Error at "/status": value "invalidStatus" is not one of the allowed values // Schema: // { // "description": "pet status in the store", diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index c1d4630c1..6fee1355d 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -244,11 +244,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "value is not one of the allowed values", + wantErrSchemaReason: "value \"available,sold\" is not one of the allowed values", wantErrSchemaPath: "/0", wantErrSchemaValue: "available,sold", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value is not one of the allowed values", + Title: "value \"available,sold\" is not one of the allowed values", Detail: "value available,sold at /0 must be one of: available, pending, sold; " + // TODO: do we really want to use this heuristic to guess // that they're using the wrong serialization? @@ -262,11 +262,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "value is not one of the allowed values", + wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", wantErrSchemaPath: "/1", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value is not one of the allowed values", + Title: "value \"watdis\" is not one of the allowed values", Detail: "value watdis at /1 must be one of: available, pending, sold", Source: &ValidationErrorSource{Parameter: "status"}}, }, @@ -278,11 +278,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "kind", wantErrParamIn: "query", - wantErrSchemaReason: "value is not one of the allowed values", + wantErrSchemaReason: "value \"fish,with,commas\" is not one of the allowed values", wantErrSchemaPath: "/1", wantErrSchemaValue: "fish,with,commas", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value is not one of the allowed values", + Title: "value \"fish,with,commas\" is not one of the allowed values", Detail: "value fish,with,commas at /1 must be one of: dog, cat, turtle, bird,with,commas", // No 'perhaps you intended' because its the right serialization format Source: &ValidationErrorSource{Parameter: "kind"}}, @@ -304,11 +304,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "x-environment", wantErrParamIn: "header", - wantErrSchemaReason: "value is not one of the allowed values", + wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", wantErrSchemaPath: "/", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value is not one of the allowed values", + Title: "value \"watdis\" is not one of the allowed values", Detail: "value watdis at / must be one of: demo, prod", Source: &ValidationErrorSource{Parameter: "x-environment"}}, }, @@ -323,11 +323,11 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), }, wantErrReason: "doesn't match the schema", - wantErrSchemaReason: "value is not one of the allowed values", + wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", wantErrSchemaValue: "watdis", wantErrSchemaPath: "/status", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "value is not one of the allowed values", + Title: "value \"watdis\" is not one of the allowed values", Detail: "value watdis at /status must be one of: available, pending, sold", Source: &ValidationErrorSource{Pointer: "/status"}}, }, @@ -379,13 +379,13 @@ func getValidationTests(t *testing.T) []*validationTest { bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)), }, wantErrReason: "doesn't match the schema", - wantErrSchemaReason: "Field must be set to array or not be present", + wantErrSchemaReason: "field must be set to array or not be present", wantErrSchemaPath: "/photoUrls", wantErrSchemaValue: "string", // TODO: this shouldn't say "or not be present", but this requires recursively resolving // innerErr.JSONPointer() against e.RequestBody.Content["application/json"].Schema.Value (.Required, .Properties) wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "Field must be set to array or not be present", + Title: "field must be set to array or not be present", Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { @@ -659,7 +659,7 @@ func TestValidationHandler_ServeHTTP(t *testing.T) { body, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) - require.Equal(t, "[422][][] Field must be set to array or not be present [source pointer=/photoUrls]", string(body)) + require.Equal(t, "[422][][] field must be set to array or not be present [source pointer=/photoUrls]", string(body)) }) } @@ -701,6 +701,6 @@ func TestValidationHandler_Middleware(t *testing.T) { body, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) - require.Equal(t, "[422][][] Field must be set to array or not be present [source pointer=/photoUrls]", string(body)) + require.Equal(t, "[422][][] field must be set to array or not be present [source pointer=/photoUrls]", string(body)) }) } diff --git a/routers/gorillamux/example_test.go b/routers/gorillamux/example_test.go index 33ce98e6a..2ca3225a5 100644 --- a/routers/gorillamux/example_test.go +++ b/routers/gorillamux/example_test.go @@ -53,7 +53,7 @@ func Example() { err = openapi3filter.ValidateResponse(ctx, responseValidationInput) fmt.Println(err) // Output: - // response body doesn't match the schema: Field must be set to string or not be present + // response body doesn't match the schema: field must be set to string or not be present // Schema: // { // "type": "string" From df9133b05cfeeb96b7d15786a673c39479fe5acd Mon Sep 17 00:00:00 2001 From: orensolo <46680749+orensolo@users.noreply.github.com> Date: Tue, 29 Nov 2022 18:59:08 +0200 Subject: [PATCH 214/260] fix: query param pattern (#665) --- openapi3filter/issue641_test.go | 109 +++++++++++++++++++++++++++++ openapi3filter/req_resp_decoder.go | 7 ++ 2 files changed, 116 insertions(+) create mode 100644 openapi3filter/issue641_test.go diff --git a/openapi3filter/issue641_test.go b/openapi3filter/issue641_test.go new file mode 100644 index 000000000..3e0bba22e --- /dev/null +++ b/openapi3filter/issue641_test.go @@ -0,0 +1,109 @@ +package openapi3filter_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue641(t *testing.T) { + + anyOfSpec := ` +openapi: 3.0.0 +info: + version: 1.0.0 + title: Sample API +paths: + /items: + get: + description: Returns a list of stuff + parameters: + - description: test object + explode: false + in: query + name: test + required: false + schema: + anyOf: + - pattern: "^[0-9]{1,4}$" + - pattern: "^[0-9]{1,4}$" + type: string + responses: + '200': + description: Successful response +`[1:] + + allOfSpec := strings.ReplaceAll(anyOfSpec, "anyOf", "allOf") + + tests := []struct { + name string + spec string + req string + errStr string + }{ + + { + name: "success anyof pattern", + spec: anyOfSpec, + req: "/items?test=51", + }, + { + name: "failed anyof pattern", + spec: anyOfSpec, + req: "/items?test=999999", + errStr: `parameter "test" in query has an error: Doesn't match schema "anyOf"`, + }, + + { + name: "success allof pattern", + spec: allOfSpec, + req: `/items?test=51`, + }, + { + name: "failed allof pattern", + spec: allOfSpec, + req: `/items?test=999999`, + errStr: `parameter "test" in query has an error: string doesn't match the regular expression`, + }, + } + + for _, testcase := range tests { + t.Run(testcase.name, func(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + + doc, err := loader.LoadFromData([]byte(testcase.spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + httpReq, err := http.NewRequest(http.MethodGet, testcase.req, nil) + require.NoError(t, err) + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + requestValidationInput := &openapi3filter.RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } + err = openapi3filter.ValidateRequest(ctx, requestValidationInput) + if testcase.errStr == "" { + require.NoError(t, err) + } else { + require.Contains(t, err.Error(), testcase.errStr) + } + }, + ) + } +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 300b2705f..59c383d5e 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -334,6 +334,9 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho case *pathParamDecoder: _, found = vDecoder.pathParams[param] case *urlValuesDecoder: + if schema.Value.Pattern != "" { + return dec.DecodePrimitive(param, sm, schema) + } _, found = vDecoder.values[param] case *headerParamDecoder: _, found = vDecoder.header[param] @@ -500,6 +503,10 @@ func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.Serializat // HTTP request does not contain a value of the target query parameter. return nil, ok, nil } + + if schema.Value.Type == "" && schema.Value.Pattern != "" { + return values[0], ok, nil + } val, err := parsePrimitive(values[0], schema) return val, ok, err } From c7936603e48f6f4e428739fcf6e6b6ab7a4c8fdd Mon Sep 17 00:00:00 2001 From: Stepan I Date: Tue, 29 Nov 2022 22:03:39 +0500 Subject: [PATCH 215/260] fix: errors in oneOf not contain path (#676) (#677) --- openapi3/schema.go | 18 +++++-------- openapi3/schema_oneOf_test.go | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 081873edb..d2cd31c5f 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1679,6 +1679,12 @@ type SchemaError struct { var _ interface{ Unwrap() error } = SchemaError{} func markSchemaErrorKey(err error, key string) error { + var me multiErrorForOneOf + + if errors.As(err, &me) { + err = me.Unwrap() + } + if v, ok := err.(*SchemaError); ok { v.reversePath = append(v.reversePath, key) return v @@ -1693,17 +1699,7 @@ func markSchemaErrorKey(err error, key string) error { } func markSchemaErrorIndex(err error, index int) error { - if v, ok := err.(*SchemaError); ok { - v.reversePath = append(v.reversePath, strconv.FormatInt(int64(index), 10)) - return v - } - if v, ok := err.(MultiError); ok { - for _, e := range v { - _ = markSchemaErrorIndex(e, index) - } - return v - } - return err + return markSchemaErrorKey(err, strconv.FormatInt(int64(index), 10)) } func (err *SchemaError) JSONPointer() []string { diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go index 7faf26864..d3e689d51 100644 --- a/openapi3/schema_oneOf_test.go +++ b/openapi3/schema_oneOf_test.go @@ -3,6 +3,7 @@ package openapi3 import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -134,3 +135,50 @@ func TestVisitJSON_OneOf_BadDescriminatorType(t *testing.T) { }) require.EqualError(t, err, "descriminator value is not a string") } + +func TestVisitJSON_OneOf_Path(t *testing.T) { + t.Parallel() + + loader := NewLoader() + spc := ` +components: + schemas: + Something: + type: object + properties: + first: + type: object + properties: + second: + type: object + properties: + third: + oneOf: + - title: First rule + type: string + minLength: 5 + maxLength: 5 + - title: Second rule + type: string + minLength: 10 + maxLength: 10 +`[1:] + + doc, err := loader.LoadFromData([]byte(spc)) + require.NoError(t, err) + + err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]interface{}{ + "first": map[string]interface{}{ + "second": map[string]interface{}{ + "third": "123456789", + }, + }, + }) + + assert.Contains(t, err.Error(), `Error at "/first/second/third"`) + + var sErr *SchemaError + + assert.ErrorAs(t, err, &sErr) + assert.Equal(t, []string{"first", "second", "third"}, sErr.JSONPointer()) +} From 834a7910240206ed96d10de63b8714ac114babe6 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 29 Nov 2022 18:19:02 +0100 Subject: [PATCH 216/260] fix tests after merge train (#686) --- openapi3filter/issue641_test.go | 2 +- openapi3filter/options_test.go | 3 +-- openapi3filter/req_resp_decoder.go | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openapi3filter/issue641_test.go b/openapi3filter/issue641_test.go index 3e0bba22e..9a2964284 100644 --- a/openapi3filter/issue641_test.go +++ b/openapi3filter/issue641_test.go @@ -69,7 +69,7 @@ paths: name: "failed allof pattern", spec: allOfSpec, req: `/items?test=999999`, - errStr: `parameter "test" in query has an error: string doesn't match the regular expression`, + errStr: `parameter "test" in query has an error: string "999999" doesn't match the regular expression "^[0-9]{1,4}$"`, }, } diff --git a/openapi3filter/options_test.go b/openapi3filter/options_test.go index 12737114d..a95b6bb96 100644 --- a/openapi3filter/options_test.go +++ b/openapi3filter/options_test.go @@ -41,8 +41,7 @@ paths: panic(err) } - err = doc.Validate(loader.Context) - if err != nil { + if err = doc.Validate(loader.Context); err != nil { panic(err) } diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 59c383d5e..029a84c2c 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -579,8 +579,7 @@ func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (int var value interface{} var err error for _, sr := range schema.Value.AnyOf { - value, err = d.parseValue(v, sr) - if err == nil { + if value, err = d.parseValue(v, sr); err == nil { return value, nil } } From 7f75486fb2e67e621a4b2d8a649be713a6704501 Mon Sep 17 00:00:00 2001 From: Omar Ramadan Date: Tue, 29 Nov 2022 09:38:44 -0800 Subject: [PATCH 217/260] Internalize recursive external references #618 (#655) --- openapi3/internalize_refs.go | 175 ++++++++++++++++---------------- openapi3/issue618_test.go | 39 +++++++ openapi3/testdata/schema618.yml | 62 +++++++++++ 3 files changed, 191 insertions(+), 85 deletions(-) create mode 100644 openapi3/issue618_test.go create mode 100644 openapi3/testdata/schema618.yml diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index 1733a495e..6ff6b8578 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -45,19 +45,19 @@ func parametersMapNames(s ParametersMap) []string { return out } -func isExternalRef(ref string) bool { - return ref != "" && !strings.HasPrefix(ref, "#/components/") +func isExternalRef(ref string, parentIsExternal bool) bool { + return ref != "" && (!strings.HasPrefix(ref, "#/components/") || parentIsExternal) } -func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver) { - if s == nil || !isExternalRef(s.Ref) { - return +func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver, parentIsExternal bool) bool { + if s == nil || !isExternalRef(s.Ref, parentIsExternal) { + return false } name := refNameResolver(s.Ref) if _, ok := doc.Components.Schemas[name]; ok { s.Ref = "#/components/schemas/" + name - return + return true } if doc.Components.Schemas == nil { @@ -65,16 +65,17 @@ func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver) { } doc.Components.Schemas[name] = s.Value.NewRef() s.Ref = "#/components/schemas/" + name + return true } -func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolver) { - if p == nil || !isExternalRef(p.Ref) { - return +func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolver, parentIsExternal bool) bool { + if p == nil || !isExternalRef(p.Ref, parentIsExternal) { + return false } name := refNameResolver(p.Ref) if _, ok := doc.Components.Parameters[name]; ok { p.Ref = "#/components/parameters/" + name - return + return true } if doc.Components.Parameters == nil { @@ -82,59 +83,62 @@ func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolve } doc.Components.Parameters[name] = &ParameterRef{Value: p.Value} p.Ref = "#/components/parameters/" + name + return true } -func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver) { - if h == nil || !isExternalRef(h.Ref) { - return +func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver, parentIsExternal bool) bool { + if h == nil || !isExternalRef(h.Ref, parentIsExternal) { + return false } name := refNameResolver(h.Ref) if _, ok := doc.Components.Headers[name]; ok { h.Ref = "#/components/headers/" + name - return + return true } if doc.Components.Headers == nil { doc.Components.Headers = make(Headers) } doc.Components.Headers[name] = &HeaderRef{Value: h.Value} h.Ref = "#/components/headers/" + name + return true } -func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameResolver) { - if r == nil || !isExternalRef(r.Ref) { - return +func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameResolver, parentIsExternal bool) bool { + if r == nil || !isExternalRef(r.Ref, parentIsExternal) { + return false } name := refNameResolver(r.Ref) if _, ok := doc.Components.RequestBodies[name]; ok { r.Ref = "#/components/requestBodies/" + name - return + return true } if doc.Components.RequestBodies == nil { doc.Components.RequestBodies = make(RequestBodies) } doc.Components.RequestBodies[name] = &RequestBodyRef{Value: r.Value} r.Ref = "#/components/requestBodies/" + name + return true } -func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver) { - if r == nil || !isExternalRef(r.Ref) { - return +func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver, parentIsExternal bool) bool { + if r == nil || !isExternalRef(r.Ref, parentIsExternal) { + return false } name := refNameResolver(r.Ref) if _, ok := doc.Components.Responses[name]; ok { r.Ref = "#/components/responses/" + name - return + return true } if doc.Components.Responses == nil { doc.Components.Responses = make(Responses) } doc.Components.Responses[name] = &ResponseRef{Value: r.Value} r.Ref = "#/components/responses/" + name - + return true } -func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver RefNameResolver) { - if ss == nil || !isExternalRef(ss.Ref) { +func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver RefNameResolver, parentIsExternal bool) { + if ss == nil || !isExternalRef(ss.Ref, parentIsExternal) { return } name := refNameResolver(ss.Ref) @@ -150,8 +154,8 @@ func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver Ref } -func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver) { - if e == nil || !isExternalRef(e.Ref) { +func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver, parentIsExternal bool) { + if e == nil || !isExternalRef(e.Ref, parentIsExternal) { return } name := refNameResolver(e.Ref) @@ -167,8 +171,8 @@ func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver) { } -func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver) { - if l == nil || !isExternalRef(l.Ref) { +func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver, parentIsExternal bool) { + if l == nil || !isExternalRef(l.Ref, parentIsExternal) { return } name := refNameResolver(l.Ref) @@ -184,9 +188,9 @@ func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver) { } -func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver) { - if c == nil || !isExternalRef(c.Ref) { - return +func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver, parentIsExternal bool) bool { + if c == nil || !isExternalRef(c.Ref, parentIsExternal) { + return false } name := refNameResolver(c.Ref) if _, ok := doc.Components.Callbacks[name]; ok { @@ -197,118 +201,119 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver) } doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value} c.Ref = "#/components/callbacks/" + name + return true } -func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) { +func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsExternal bool) { if s == nil || doc.isVisitedSchema(s) { return } for _, list := range []SchemaRefs{s.AllOf, s.AnyOf, s.OneOf} { for _, s2 := range list { - doc.addSchemaToSpec(s2, refNameResolver) + isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal) if s2 != nil { - doc.derefSchema(s2.Value, refNameResolver) + doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal) } } } for _, s2 := range s.Properties { - doc.addSchemaToSpec(s2, refNameResolver) + isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal) if s2 != nil { - doc.derefSchema(s2.Value, refNameResolver) + doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal) } } for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties, s.Items} { - doc.addSchemaToSpec(ref, refNameResolver) + isExternal := doc.addSchemaToSpec(ref, refNameResolver, parentIsExternal) if ref != nil { - doc.derefSchema(ref.Value, refNameResolver) + doc.derefSchema(ref.Value, refNameResolver, isExternal || parentIsExternal) } } } -func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver) { +func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver, parentIsExternal bool) { for _, h := range hs { - doc.addHeaderToSpec(h, refNameResolver) + isExternal := doc.addHeaderToSpec(h, refNameResolver, parentIsExternal) if doc.isVisitedHeader(h.Value) { continue } - doc.derefParameter(h.Value.Parameter, refNameResolver) + doc.derefParameter(h.Value.Parameter, refNameResolver, parentIsExternal || isExternal) } } -func (doc *T) derefExamples(es Examples, refNameResolver RefNameResolver) { +func (doc *T) derefExamples(es Examples, refNameResolver RefNameResolver, parentIsExternal bool) { for _, e := range es { - doc.addExampleToSpec(e, refNameResolver) + doc.addExampleToSpec(e, refNameResolver, parentIsExternal) } } -func (doc *T) derefContent(c Content, refNameResolver RefNameResolver) { +func (doc *T) derefContent(c Content, refNameResolver RefNameResolver, parentIsExternal bool) { for _, mediatype := range c { - doc.addSchemaToSpec(mediatype.Schema, refNameResolver) + isExternal := doc.addSchemaToSpec(mediatype.Schema, refNameResolver, parentIsExternal) if mediatype.Schema != nil { - doc.derefSchema(mediatype.Schema.Value, refNameResolver) + doc.derefSchema(mediatype.Schema.Value, refNameResolver, isExternal || parentIsExternal) } - doc.derefExamples(mediatype.Examples, refNameResolver) + doc.derefExamples(mediatype.Examples, refNameResolver, parentIsExternal) for _, e := range mediatype.Encoding { - doc.derefHeaders(e.Headers, refNameResolver) + doc.derefHeaders(e.Headers, refNameResolver, parentIsExternal) } } } -func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver) { +func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver, parentIsExternal bool) { for _, l := range ls { - doc.addLinkToSpec(l, refNameResolver) + doc.addLinkToSpec(l, refNameResolver, parentIsExternal) } } -func (doc *T) derefResponses(es Responses, refNameResolver RefNameResolver) { +func (doc *T) derefResponses(es Responses, refNameResolver RefNameResolver, parentIsExternal bool) { for _, e := range es { - doc.addResponseToSpec(e, refNameResolver) + isExternal := doc.addResponseToSpec(e, refNameResolver, parentIsExternal) if e.Value != nil { - doc.derefHeaders(e.Value.Headers, refNameResolver) - doc.derefContent(e.Value.Content, refNameResolver) - doc.derefLinks(e.Value.Links, refNameResolver) + doc.derefHeaders(e.Value.Headers, refNameResolver, isExternal || parentIsExternal) + doc.derefContent(e.Value.Content, refNameResolver, isExternal || parentIsExternal) + doc.derefLinks(e.Value.Links, refNameResolver, isExternal || parentIsExternal) } } } -func (doc *T) derefParameter(p Parameter, refNameResolver RefNameResolver) { - doc.addSchemaToSpec(p.Schema, refNameResolver) - doc.derefContent(p.Content, refNameResolver) +func (doc *T) derefParameter(p Parameter, refNameResolver RefNameResolver, parentIsExternal bool) { + isExternal := doc.addSchemaToSpec(p.Schema, refNameResolver, parentIsExternal) + doc.derefContent(p.Content, refNameResolver, parentIsExternal) if p.Schema != nil { - doc.derefSchema(p.Schema.Value, refNameResolver) + doc.derefSchema(p.Schema.Value, refNameResolver, isExternal || parentIsExternal) } } -func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver) { - doc.derefContent(r.Content, refNameResolver) +func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver, parentIsExternal bool) { + doc.derefContent(r.Content, refNameResolver, parentIsExternal) } -func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver) { +func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver, parentIsExternal bool) { for _, ops := range paths { // inline full operations ops.Ref = "" for _, param := range ops.Parameters { - doc.addParameterToSpec(param, refNameResolver) + doc.addParameterToSpec(param, refNameResolver, parentIsExternal) } for _, op := range ops.Operations() { - doc.addRequestBodyToSpec(op.RequestBody, refNameResolver) + isExternal := doc.addRequestBodyToSpec(op.RequestBody, refNameResolver, parentIsExternal) if op.RequestBody != nil && op.RequestBody.Value != nil { - doc.derefRequestBody(*op.RequestBody.Value, refNameResolver) + doc.derefRequestBody(*op.RequestBody.Value, refNameResolver, parentIsExternal || isExternal) } for _, cb := range op.Callbacks { - doc.addCallbackToSpec(cb, refNameResolver) + isExternal := doc.addCallbackToSpec(cb, refNameResolver, parentIsExternal) if cb.Value != nil { - doc.derefPaths(*cb.Value, refNameResolver) + doc.derefPaths(*cb.Value, refNameResolver, parentIsExternal || isExternal) } } - doc.derefResponses(op.Responses, refNameResolver) + doc.derefResponses(op.Responses, refNameResolver, parentIsExternal) for _, param := range op.Parameters { - doc.addParameterToSpec(param, refNameResolver) + isExternal := doc.addParameterToSpec(param, refNameResolver, parentIsExternal) if param.Value != nil { - doc.derefParameter(*param.Value, refNameResolver) + doc.derefParameter(*param.Value, refNameResolver, parentIsExternal || isExternal) } } } @@ -337,42 +342,42 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri names := schemaNames(doc.Components.Schemas) for _, name := range names { schema := doc.Components.Schemas[name] - doc.addSchemaToSpec(schema, refNameResolver) + isExternal := doc.addSchemaToSpec(schema, refNameResolver, false) if schema != nil { schema.Ref = "" // always dereference the top level - doc.derefSchema(schema.Value, refNameResolver) + doc.derefSchema(schema.Value, refNameResolver, isExternal) } } names = parametersMapNames(doc.Components.Parameters) for _, name := range names { p := doc.Components.Parameters[name] - doc.addParameterToSpec(p, refNameResolver) + isExternal := doc.addParameterToSpec(p, refNameResolver, false) if p != nil && p.Value != nil { p.Ref = "" // always dereference the top level - doc.derefParameter(*p.Value, refNameResolver) + doc.derefParameter(*p.Value, refNameResolver, isExternal) } } - doc.derefHeaders(doc.Components.Headers, refNameResolver) + doc.derefHeaders(doc.Components.Headers, refNameResolver, false) for _, req := range doc.Components.RequestBodies { - doc.addRequestBodyToSpec(req, refNameResolver) + isExternal := doc.addRequestBodyToSpec(req, refNameResolver, false) if req != nil && req.Value != nil { req.Ref = "" // always dereference the top level - doc.derefRequestBody(*req.Value, refNameResolver) + doc.derefRequestBody(*req.Value, refNameResolver, isExternal) } } - doc.derefResponses(doc.Components.Responses, refNameResolver) + doc.derefResponses(doc.Components.Responses, refNameResolver, false) for _, ss := range doc.Components.SecuritySchemes { - doc.addSecuritySchemeToSpec(ss, refNameResolver) + doc.addSecuritySchemeToSpec(ss, refNameResolver, false) } - doc.derefExamples(doc.Components.Examples, refNameResolver) - doc.derefLinks(doc.Components.Links, refNameResolver) + doc.derefExamples(doc.Components.Examples, refNameResolver, false) + doc.derefLinks(doc.Components.Links, refNameResolver, false) for _, cb := range doc.Components.Callbacks { - doc.addCallbackToSpec(cb, refNameResolver) + isExternal := doc.addCallbackToSpec(cb, refNameResolver, false) if cb != nil && cb.Value != nil { cb.Ref = "" // always dereference the top level - doc.derefPaths(*cb.Value, refNameResolver) + doc.derefPaths(*cb.Value, refNameResolver, isExternal) } } - doc.derefPaths(doc.Paths, refNameResolver) + doc.derefPaths(doc.Paths, refNameResolver, false) } diff --git a/openapi3/issue618_test.go b/openapi3/issue618_test.go new file mode 100644 index 000000000..2085ca0ee --- /dev/null +++ b/openapi3/issue618_test.go @@ -0,0 +1,39 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue618(t *testing.T) { + spec := ` +openapi: 3.0.0 +info: + title: foo + version: 0.0.0 +paths: + /foo: + get: + responses: + '200': + description: Some description value text + content: + application/json: + schema: + $ref: ./testdata/schema618.yml#/components/schemas/JournalEntry +`[1:] + + loader := NewLoader() + loader.IsExternalRefsAllowed = true + ctx := loader.Context + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + doc.InternalizeRefs(ctx, nil) + + require.Contains(t, doc.Components.Schemas, "JournalEntry") + require.Contains(t, doc.Components.Schemas, "Record") + require.Contains(t, doc.Components.Schemas, "Account") +} diff --git a/openapi3/testdata/schema618.yml b/openapi3/testdata/schema618.yml new file mode 100644 index 000000000..1ab400075 --- /dev/null +++ b/openapi3/testdata/schema618.yml @@ -0,0 +1,62 @@ +components: + schemas: + Account: + required: + - name + - nature + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + type: + type: string + enum: + - assets + - liabilities + nature: + type: string + enum: + - asset + - liability + Record: + required: + - account + - concept + type: object + properties: + account: + $ref: "#/components/schemas/Account" + concept: + type: string + partial: + type: number + credit: + type: number + debit: + type: number + JournalEntry: + required: + - type + - creationDate + - records + type: object + properties: + id: + type: string + type: + type: string + enum: + - daily + - ingress + - egress + creationDate: + type: string + format: date + records: + type: array + items: + $ref: "#/components/schemas/Record" From b4b41f3f484c7a452e7d1563abdf42c15fc40db8 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sat, 3 Dec 2022 17:21:32 +0100 Subject: [PATCH 218/260] Add variadic options to Validate method (#692) --- .github/workflows/go.yml | 7 +- README.md | 4 + openapi3/callback.go | 4 +- openapi3/components.go | 4 +- openapi3/content.go | 4 +- openapi3/discriminator.go | 4 +- openapi3/encoding.go | 4 +- openapi3/example.go | 4 +- openapi3/example_validation_test.go | 8 +- openapi3/external_docs.go | 4 +- openapi3/header.go | 4 +- openapi3/info.go | 12 ++- openapi3/link.go | 4 +- openapi3/media_type.go | 4 +- openapi3/openapi3.go | 6 +- openapi3/operation.go | 4 +- openapi3/parameter.go | 8 +- openapi3/path_item.go | 4 +- openapi3/paths.go | 4 +- openapi3/refs.go | 27 ++++-- openapi3/request_body.go | 4 +- openapi3/response.go | 8 +- openapi3/schema.go | 3 +- openapi3/schema_test.go | 4 +- openapi3/security_requirements.go | 8 +- openapi3/security_scheme.go | 89 ++++++++++++++---- openapi3/security_scheme_test.go | 139 ++++++++++++---------------- openapi3/server.go | 12 ++- openapi3/tag.go | 8 +- openapi3/validation_options.go | 35 ++++++- openapi3/xml.go | 4 +- 31 files changed, 284 insertions(+), 154 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index aa40035a0..d6950ebbb 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -36,13 +36,13 @@ jobs: - run: echo ${{ steps.go-cache-paths.outputs.go-mod }} - name: Go Build Cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-${{ matrix.go }}-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache (go>=1.15) - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-${{ matrix.go }}-mod-${{ hashFiles('**/go.sum') }} @@ -61,6 +61,7 @@ jobs: - run: go fmt ./... - run: git --no-pager diff --exit-code + - run: go test ./... - if: runner.os == 'Linux' run: go test -count=10 ./... env: @@ -116,7 +117,7 @@ jobs: fi # Ensure impl Validate() - if ! git grep -InE 'func [(].+Schema[)] Validate[(]ctx context.Context[)].+error.+[{]'; then + if ! git grep -InE 'func [(].+'"$ty"'[)] Validate[(]ctx context.Context, opts [.][.][.]ValidationOption[)].+error.+[{]'; then echo "OAI type $ty does not implement Validate()" && exit 1 fi diff --git a/README.md b/README.md index c8431f807..c2226498c 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,10 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.111.0 +* Changed `func (*_) Validate(ctx context.Context) error` to `func (*_) Validate(ctx context.Context, opts ...ValidationOption) error`. +* `openapi3.WithValidationOptions(ctx context.Context, opts *ValidationOptions) context.Context` prototype changed to `openapi3.WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context`. + ### v0.101.0 * `openapi3.SchemaFormatValidationDisabled` has been removed in favour of an option `openapi3.EnableSchemaFormatValidation()` passed to `openapi3.T.Validate`. The default behaviour is also now to not validate formats, as the OpenAPI spec mentions the `format` is an open value. diff --git a/openapi3/callback.go b/openapi3/callback.go index 1e4736946..6246d6d8c 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -30,7 +30,9 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) { type Callback map[string]*PathItem // Validate returns an error if Callback does not comply with the OpenAPI spec. -func (callback Callback) Validate(ctx context.Context) error { +func (callback Callback) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + keys := make([]string, 0, len(callback)) for key := range callback { keys = append(keys, key) diff --git a/openapi3/components.go b/openapi3/components.go index 02ae458f7..16b39f303 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -40,7 +40,9 @@ func (components *Components) UnmarshalJSON(data []byte) error { } // Validate returns an error if Components does not comply with the OpenAPI spec. -func (components *Components) Validate(ctx context.Context) (err error) { +func (components *Components) Validate(ctx context.Context, opts ...ValidationOption) (err error) { + ctx = WithValidationOptions(ctx, opts...) + schemas := make([]string, 0, len(components.Schemas)) for name := range components.Schemas { schemas = append(schemas, name) diff --git a/openapi3/content.go b/openapi3/content.go index 944325041..8abd411da 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -106,7 +106,9 @@ func (content Content) Get(mime string) *MediaType { } // Validate returns an error if Content does not comply with the OpenAPI spec. -func (content Content) Validate(ctx context.Context) error { +func (content Content) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + keys := make([]string, 0, len(content)) for key := range content { keys = append(keys, key) diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 28a2148c1..8eb296024 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -26,6 +26,8 @@ func (discriminator *Discriminator) UnmarshalJSON(data []byte) error { } // Validate returns an error if Discriminator does not comply with the OpenAPI spec. -func (discriminator *Discriminator) Validate(ctx context.Context) error { +func (discriminator *Discriminator) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + return nil } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index bc4985cb7..082d3f2ec 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -66,7 +66,9 @@ func (encoding *Encoding) SerializationMethod() *SerializationMethod { } // Validate returns an error if Encoding does not comply with the OpenAPI spec. -func (encoding *Encoding) Validate(ctx context.Context) error { +func (encoding *Encoding) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if encoding == nil { return nil } diff --git a/openapi3/example.go b/openapi3/example.go index 561f09b97..e75d4d3d5 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -55,7 +55,9 @@ func (example *Example) UnmarshalJSON(data []byte) error { } // Validate returns an error if Example does not comply with the OpenAPI spec. -func (example *Example) Validate(ctx context.Context) error { +func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + if example.Value != nil && example.ExternalValue != "" { return errors.New("value and externalValue are mutually exclusive") } diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index 79288c299..6ce7c0a48 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -221,8 +221,6 @@ func TestExamplesSchemaValidation(t *testing.T) { t.Parallel() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - loader := NewLoader() - spec := bytes.Buffer{} spec.WriteString(` openapi: 3.0.3 @@ -339,13 +337,14 @@ components: `) spec.WriteString(tc.componentExamples) + loader := NewLoader() doc, err := loader.LoadFromData(spec.Bytes()) require.NoError(t, err) if testOption.disableExamplesValidation { err = doc.Validate(loader.Context, DisableExamplesValidation()) } else { - err = doc.Validate(loader.Context) + err = doc.Validate(loader.Context, EnableExamplesValidation()) } if tc.errContains != "" && !testOption.disableExamplesValidation { @@ -436,8 +435,6 @@ func TestExampleObjectValidation(t *testing.T) { t.Parallel() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - loader := NewLoader() - spec := bytes.Buffer{} spec.WriteString(` openapi: 3.0.3 @@ -506,6 +503,7 @@ components: `) spec.WriteString(tc.componentExamples) + loader := NewLoader() doc, err := loader.LoadFromData(spec.Bytes()) require.NoError(t, err) diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index 75ae0d707..65ec2e88f 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -29,7 +29,9 @@ func (e *ExternalDocs) UnmarshalJSON(data []byte) error { } // Validate returns an error if ExternalDocs does not comply with the OpenAPI spec. -func (e *ExternalDocs) Validate(ctx context.Context) error { +func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + if e.URL == "" { return errors.New("url is required") } diff --git a/openapi3/header.go b/openapi3/header.go index c71d3f2a8..aefaa06a3 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -54,7 +54,9 @@ func (header *Header) SerializationMethod() (*SerializationMethod, error) { } // Validate returns an error if Header does not comply with the OpenAPI spec. -func (header *Header) Validate(ctx context.Context) error { +func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if header.Name != "" { return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map") } diff --git a/openapi3/info.go b/openapi3/info.go index fa6593cb4..c67f73ab8 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -31,7 +31,9 @@ func (info *Info) UnmarshalJSON(data []byte) error { } // Validate returns an error if Info does not comply with the OpenAPI spec. -func (info *Info) Validate(ctx context.Context) error { +func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if contact := info.Contact; contact != nil { if err := contact.Validate(ctx); err != nil { return err @@ -76,7 +78,9 @@ func (contact *Contact) UnmarshalJSON(data []byte) error { } // Validate returns an error if Contact does not comply with the OpenAPI spec. -func (contact *Contact) Validate(ctx context.Context) error { +func (contact *Contact) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + return nil } @@ -100,7 +104,9 @@ func (license *License) UnmarshalJSON(data []byte) error { } // Validate returns an error if License does not comply with the OpenAPI spec. -func (license *License) Validate(ctx context.Context) error { +func (license *License) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + if license.Name == "" { return errors.New("value of license name must be a non-empty string") } diff --git a/openapi3/link.go b/openapi3/link.go index 3fb4d78d8..1040a0408 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -51,7 +51,9 @@ func (link *Link) UnmarshalJSON(data []byte) error { } // Validate returns an error if Link does not comply with the OpenAPI spec. -func (link *Link) Validate(ctx context.Context) error { +func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + if link.OperationID == "" && link.OperationRef == "" { return errors.New("missing operationId or operationRef on link") } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 1a9bb51e9..74c11b78c 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -75,7 +75,9 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error { } // Validate returns an error if MediaType does not comply with the OpenAPI spec. -func (mediaType *MediaType) Validate(ctx context.Context) error { +func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if mediaType == nil { return nil } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 510df09a8..714f28030 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -56,11 +56,7 @@ func (doc *T) AddServer(server *Server) { // Validate returns an error if T does not comply with the OpenAPI spec. // Validations Options can be provided to modify the validation behavior. func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { - validationOpts := &ValidationOptions{} - for _, opt := range opts { - opt(validationOpts) - } - ctx = WithValidationOptions(ctx, validationOpts) + ctx = WithValidationOptions(ctx, opts...) if doc.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") diff --git a/openapi3/operation.go b/openapi3/operation.go index 3abc3c4e1..d87704905 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -127,7 +127,9 @@ func (operation *Operation) AddResponse(status int, response *Response) { } // Validate returns an error if Operation does not comply with the OpenAPI spec. -func (operation *Operation) Validate(ctx context.Context) error { +func (operation *Operation) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if v := operation.Parameters; v != nil { if err := v.Validate(ctx); err != nil { return err diff --git a/openapi3/parameter.go b/openapi3/parameter.go index dc82a4980..9124d92a4 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -69,7 +69,9 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter { } // Validate returns an error if Parameters does not comply with the OpenAPI spec. -func (parameters Parameters) Validate(ctx context.Context) error { +func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + dupes := make(map[string]struct{}) for _, parameterRef := range parameters { if v := parameterRef.Value; v != nil { @@ -247,7 +249,9 @@ func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error) } // Validate returns an error if Parameter does not comply with the OpenAPI spec. -func (parameter *Parameter) Validate(ctx context.Context) error { +func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if parameter.Name == "" { return errors.New("parameter name can't be blank") } diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 28ee4b8a8..5cba0a876 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -123,7 +123,9 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { } // Validate returns an error if PathItem does not comply with the OpenAPI spec. -func (pathItem *PathItem) Validate(ctx context.Context) error { +func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + operations := pathItem.Operations() methods := make([]string, 0, len(operations)) diff --git a/openapi3/paths.go b/openapi3/paths.go index e3da7d05b..2af59f2ca 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -12,7 +12,9 @@ import ( type Paths map[string]*PathItem // Validate returns an error if Paths does not comply with the OpenAPI spec. -func (paths Paths) Validate(ctx context.Context) error { +func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + normalizedPaths := make(map[string]string, len(paths)) keys := make([]string, 0, len(paths)) diff --git a/openapi3/refs.go b/openapi3/refs.go index e85f37e03..7311c9d34 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -39,7 +39,8 @@ func (value *CallbackRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if CallbackRef does not comply with the OpenAPI spec. -func (value *CallbackRef) Validate(ctx context.Context) error { +func (value *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } @@ -81,7 +82,8 @@ func (value *ExampleRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if ExampleRef does not comply with the OpenAPI spec. -func (value *ExampleRef) Validate(ctx context.Context) error { +func (value *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } @@ -123,7 +125,8 @@ func (value *HeaderRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if HeaderRef does not comply with the OpenAPI spec. -func (value *HeaderRef) Validate(ctx context.Context) error { +func (value *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } @@ -163,7 +166,8 @@ func (value *LinkRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if LinkRef does not comply with the OpenAPI spec. -func (value *LinkRef) Validate(ctx context.Context) error { +func (value *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } @@ -195,7 +199,8 @@ func (value *ParameterRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if ParameterRef does not comply with the OpenAPI spec. -func (value *ParameterRef) Validate(ctx context.Context) error { +func (value *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } @@ -237,7 +242,8 @@ func (value *ResponseRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if ResponseRef does not comply with the OpenAPI spec. -func (value *ResponseRef) Validate(ctx context.Context) error { +func (value *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } @@ -279,7 +285,8 @@ func (value *RequestBodyRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if RequestBodyRef does not comply with the OpenAPI spec. -func (value *RequestBodyRef) Validate(ctx context.Context) error { +func (value *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } @@ -328,7 +335,8 @@ func (value *SchemaRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if SchemaRef does not comply with the OpenAPI spec. -func (value *SchemaRef) Validate(ctx context.Context) error { +func (value *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } @@ -370,7 +378,8 @@ func (value *SecuritySchemeRef) UnmarshalJSON(data []byte) error { } // Validate returns an error if SecuritySchemeRef does not comply with the OpenAPI spec. -func (value *SecuritySchemeRef) Validate(ctx context.Context) error { +func (value *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) if v := value.Value; v != nil { return v.Validate(ctx) } diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 3e7c0d620..225c3d3c7 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -105,7 +105,9 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { } // Validate returns an error if RequestBody does not comply with the OpenAPI spec. -func (requestBody *RequestBody) Validate(ctx context.Context) error { +func (requestBody *RequestBody) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if requestBody.Content == nil { return errors.New("content of the request body is required") } diff --git a/openapi3/response.go b/openapi3/response.go index 287e2909f..d2f907d12 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -33,7 +33,9 @@ func (responses Responses) Get(status int) *ResponseRef { } // Validate returns an error if Responses does not comply with the OpenAPI spec. -func (responses Responses) Validate(ctx context.Context) error { +func (responses Responses) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if len(responses) == 0 { return errors.New("the responses object MUST contain at least one response code") } @@ -111,7 +113,9 @@ func (response *Response) UnmarshalJSON(data []byte) error { } // Validate returns an error if Response does not comply with the OpenAPI spec. -func (response *Response) Validate(ctx context.Context) error { +func (response *Response) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if response.Description == nil { return errors.New("a short description of the response is required") } diff --git a/openapi3/schema.go b/openapi3/schema.go index d2cd31c5f..9f874d90e 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -602,7 +602,8 @@ func (schema *Schema) IsEmpty() bool { } // Validate returns an error if Schema does not comply with the OpenAPI spec. -func (schema *Schema) Validate(ctx context.Context) error { +func (schema *Schema) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) return schema.validate(ctx, []*Schema{}) } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index abec30477..89971cff0 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1028,9 +1028,7 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) - ctx := WithValidationOptions(context.Background(), &ValidationOptions{ - SchemaFormatValidationEnabled: true, - }) + ctx := WithValidationOptions(context.Background(), EnableSchemaFormatValidation()) err := schema.Validate(ctx) require.Error(t, err) } diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index 592997505..dcdad0c4d 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -16,7 +16,9 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) * } // Validate returns an error if SecurityRequirements does not comply with the OpenAPI spec. -func (srs SecurityRequirements) Validate(ctx context.Context) error { +func (srs SecurityRequirements) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + for _, security := range srs { if err := security.Validate(ctx); err != nil { return err @@ -42,6 +44,8 @@ func (security SecurityRequirement) Authenticate(provider string, scopes ...stri } // Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec. -func (security *SecurityRequirement) Validate(ctx context.Context) error { +func (security *SecurityRequirement) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + return nil } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 3797389bf..2b3235dfc 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/url" "github.com/go-openapi/jsonpointer" @@ -110,7 +111,9 @@ func (ss *SecurityScheme) WithBearerFormat(value string) *SecurityScheme { } // Validate returns an error if SecurityScheme does not comply with the OpenAPI spec. -func (ss *SecurityScheme) Validate(ctx context.Context) error { +func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + hasIn := false hasBearerFormat := false hasFlow := false @@ -204,20 +207,30 @@ func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { } // Validate returns an error if OAuthFlows does not comply with the OpenAPI spec. -func (flows *OAuthFlows) Validate(ctx context.Context) error { +func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if v := flows.Implicit; v != nil { - return v.Validate(ctx, oAuthFlowTypeImplicit) + if err := v.validate(ctx, oAuthFlowTypeImplicit, opts...); err != nil { + return fmt.Errorf("the OAuth flow 'implicit' is invalid: %w", err) + } } if v := flows.Password; v != nil { - return v.Validate(ctx, oAuthFlowTypePassword) + if err := v.validate(ctx, oAuthFlowTypePassword, opts...); err != nil { + return fmt.Errorf("the OAuth flow 'password' is invalid: %w", err) + } } if v := flows.ClientCredentials; v != nil { - return v.Validate(ctx, oAuthFlowTypeClientCredentials) + if err := v.validate(ctx, oAuthFlowTypeClientCredentials, opts...); err != nil { + return fmt.Errorf("the OAuth flow 'clientCredentials' is invalid: %w", err) + } } if v := flows.AuthorizationCode; v != nil { - return v.Validate(ctx, oAuthFlowAuthorizationCode) + if err := v.validate(ctx, oAuthFlowAuthorizationCode, opts...); err != nil { + return fmt.Errorf("the OAuth flow 'authorizationCode' is invalid: %w", err) + } } - return errors.New("no OAuth flow is defined") + return nil } // OAuthFlow is specified by OpenAPI/Swagger standard version 3. @@ -241,20 +254,60 @@ func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flow) } -// Validate returns an error if OAuthFlow does not comply with the OpenAPI spec. -func (flow *OAuthFlow) Validate(ctx context.Context, typ oAuthFlowType) error { - if typ == oAuthFlowAuthorizationCode || typ == oAuthFlowTypeImplicit { - if v := flow.AuthorizationURL; v == "" { - return errors.New("an OAuth flow is missing 'authorizationUrl in authorizationCode or implicit '") +// Validate returns an error if OAuthFlows does not comply with the OpenAPI spec. +func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + + if v := flow.RefreshURL; v != "" { + if _, err := url.Parse(v); err != nil { + return fmt.Errorf("field 'refreshUrl' is invalid: %w", err) } } - if typ != oAuthFlowTypeImplicit { - if v := flow.TokenURL; v == "" { - return errors.New("an OAuth flow is missing 'tokenUrl in not implicit'") + + if v := flow.Scopes; len(v) == 0 { + return errors.New("field 'scopes' is empty or missing") + } + + return nil +} + +func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + + typeIn := func(types ...oAuthFlowType) bool { + for _, ty := range types { + if ty == typ { + return true + } } + return false } - if v := flow.Scopes; v == nil { - return errors.New("an OAuth flow is missing 'scopes'") + + if in := typeIn(oAuthFlowTypeImplicit, oAuthFlowAuthorizationCode); true { + switch { + case flow.AuthorizationURL == "" && in: + return errors.New("field 'authorizationUrl' is empty or missing") + case flow.AuthorizationURL != "" && !in: + return errors.New("field 'authorizationUrl' should not be set") + case flow.AuthorizationURL != "": + if _, err := url.Parse(flow.AuthorizationURL); err != nil { + return fmt.Errorf("field 'authorizationUrl' is invalid: %w", err) + } + } } - return nil + + if in := typeIn(oAuthFlowTypePassword, oAuthFlowTypeClientCredentials, oAuthFlowAuthorizationCode); true { + switch { + case flow.TokenURL == "" && in: + return errors.New("field 'tokenUrl' is empty or missing") + case flow.TokenURL != "" && !in: + return errors.New("field 'tokenUrl' should not be set") + case flow.TokenURL != "": + if _, err := url.Parse(flow.TokenURL); err != nil { + return fmt.Errorf("field 'tokenUrl' is invalid: %w", err) + } + } + } + + return flow.Validate(ctx, opts...) } diff --git a/openapi3/security_scheme_test.go b/openapi3/security_scheme_test.go index cba0b8442..5958c5330 100644 --- a/openapi3/security_scheme_test.go +++ b/openapi3/security_scheme_test.go @@ -15,22 +15,18 @@ type securitySchemeExample struct { func TestSecuritySchemaExample(t *testing.T) { for _, example := range securitySchemeExamples { - t.Run(example.title, testSecuritySchemaExample(t, example)) - } -} - -func testSecuritySchemaExample(t *testing.T, e securitySchemeExample) func(*testing.T) { - return func(t *testing.T) { - var err error - ss := &SecurityScheme{} - err = ss.UnmarshalJSON(e.raw) - require.NoError(t, err) - err = ss.Validate(context.Background()) - if e.valid { + t.Run(example.title, func(t *testing.T) { + ss := &SecurityScheme{} + err := ss.UnmarshalJSON(example.raw) require.NoError(t, err) - } else { - require.Error(t, err) - } + + err = ss.Validate(context.Background()) + if example.valid { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) } } @@ -38,72 +34,65 @@ func testSecuritySchemaExample(t *testing.T, e securitySchemeExample) func(*test var securitySchemeExamples = []securitySchemeExample{ { title: "Basic Authentication Sample", - raw: []byte(` -{ + raw: []byte(`{ "type": "http", "scheme": "basic" -} -`), +}`), valid: true, }, + { title: "Negotiate Authentication Sample", - raw: []byte(` -{ + raw: []byte(`{ "type": "http", "scheme": "negotiate" -} -`), +}`), valid: true, }, + { title: "Unknown http Authentication Sample", - raw: []byte(` -{ + raw: []byte(`{ "type": "http", "scheme": "notvalid" -} -`), +}`), valid: false, }, + { title: "API Key Sample", - raw: []byte(` -{ + raw: []byte(`{ "type": "apiKey", "name": "api_key", "in": "header" -} -`), +}`), valid: true, }, + { title: "apiKey with bearerFormat", - raw: []byte(` -{ + raw: []byte(`{ "type": "apiKey", - "in": "header", - "name": "X-API-KEY", + "in": "header", + "name": "X-API-KEY", "bearerFormat": "Arbitrary text" -} -`), +}`), valid: false, }, + { title: "Bearer Sample with arbitrary format", - raw: []byte(` -{ + raw: []byte(`{ "type": "http", "scheme": "bearer", "bearerFormat": "Arbitrary text" -} -`), +}`), valid: true, }, + { title: "Implicit OAuth2 Sample", - raw: []byte(` -{ + raw: []byte(`{ "type": "oauth2", "flows": { "implicit": { @@ -114,14 +103,13 @@ var securitySchemeExamples = []securitySchemeExample{ } } } -} -`), +}`), valid: true, }, + { title: "OAuth Flow Object Sample", - raw: []byte(` -{ + raw: []byte(`{ "type": "oauth2", "flows": { "implicit": { @@ -140,14 +128,13 @@ var securitySchemeExamples = []securitySchemeExample{ } } } -} -`), +}`), valid: true, }, + { title: "OAuth Flow Object clientCredentials/password", - raw: []byte(` -{ + raw: []byte(`{ "type": "oauth2", "flows": { "clientCredentials": { @@ -163,79 +150,71 @@ var securitySchemeExamples = []securitySchemeExample{ } } } -} -`), +}`), valid: true, }, + { title: "Invalid Basic", - raw: []byte(` -{ + raw: []byte(`{ "type": "https", "scheme": "basic" -} -`), +}`), valid: false, }, + { title: "Apikey Cookie", - raw: []byte(` -{ + raw: []byte(`{ "type": "apiKey", "in": "cookie", "name": "somecookie" -} -`), +}`), valid: true, }, { title: "OAuth Flow Object with no scopes", - raw: []byte(` -{ + raw: []byte(`{ "type": "oauth2", "flows": { "password": { "tokenUrl": "https://example.com/api/oauth/token" } } -} -`), +}`), valid: false, }, + { title: "OAuth Flow Object with empty scopes", - raw: []byte(` -{ + raw: []byte(`{ "type": "oauth2", "flows": { "password": { - "tokenUrl": "https://example.com/api/oauth/token", - "scopes": {} + "tokenUrl": "https://example.com/api/oauth/token", + "scopes": {} } } -} -`), - valid: true, +}`), + valid: false, }, + { title: "OIDC Type With URL", - raw: []byte(` -{ + raw: []byte(`{ "type": "openIdConnect", "openIdConnectUrl": "https://example.com/.well-known/openid-configuration" -} -`), +}`), valid: true, }, + { title: "OIDC Type Without URL", - raw: []byte(` -{ + raw: []byte(`{ "type": "openIdConnect", "openIdConnectUrl": "" -} -`), +}`), valid: false, }, } diff --git a/openapi3/server.go b/openapi3/server.go index 3f989d857..304799634 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -16,7 +16,9 @@ import ( type Servers []*Server // Validate returns an error if Servers does not comply with the OpenAPI spec. -func (servers Servers) Validate(ctx context.Context) error { +func (servers Servers) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + for _, v := range servers { if err := v.Validate(ctx); err != nil { return err @@ -163,7 +165,9 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) { } // Validate returns an error if Server does not comply with the OpenAPI spec. -func (server *Server) Validate(ctx context.Context) (err error) { +func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (err error) { + ctx = WithValidationOptions(ctx, opts...) + if server.URL == "" { return errors.New("value of url must be a non-empty string") } @@ -215,7 +219,9 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { } // Validate returns an error if ServerVariable does not comply with the OpenAPI spec. -func (serverVariable *ServerVariable) Validate(ctx context.Context) error { +func (serverVariable *ServerVariable) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + if serverVariable.Default == "" { data, err := serverVariable.MarshalJSON() if err != nil { diff --git a/openapi3/tag.go b/openapi3/tag.go index b6c24c807..f151e5032 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -20,7 +20,9 @@ func (tags Tags) Get(name string) *Tag { } // Validate returns an error if Tags does not comply with the OpenAPI spec. -func (tags Tags) Validate(ctx context.Context) error { +func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + for _, v := range tags { if err := v.Validate(ctx); err != nil { return err @@ -50,7 +52,9 @@ func (t *Tag) UnmarshalJSON(data []byte) error { } // Validate returns an error if Tag does not comply with the OpenAPI spec. -func (t *Tag) Validate(ctx context.Context) error { +func (t *Tag) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if v := t.ExternalDocs; v != nil { if err := v.Validate(ctx); err != nil { return fmt.Errorf("invalid external docs: %w", err) diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index d8900878a..a74364dae 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -16,12 +16,29 @@ type ValidationOptions struct { type validationOptionsKey struct{} // EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. +// By default, schema format validation is disabled. func EnableSchemaFormatValidation() ValidationOption { return func(options *ValidationOptions) { options.SchemaFormatValidationEnabled = true } } +// DisableSchemaFormatValidation does the opposite of EnableSchemaFormatValidation. +// By default, schema format validation is disabled. +func DisableSchemaFormatValidation() ValidationOption { + return func(options *ValidationOptions) { + options.SchemaFormatValidationEnabled = false + } +} + +// EnableSchemaPatternValidation does the opposite of DisableSchemaPatternValidation. +// By default, schema pattern validation is enabled. +func EnableSchemaPatternValidation() ValidationOption { + return func(options *ValidationOptions) { + options.SchemaPatternValidationDisabled = false + } +} + // DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. func DisableSchemaPatternValidation() ValidationOption { return func(options *ValidationOptions) { @@ -29,7 +46,16 @@ func DisableSchemaPatternValidation() ValidationOption { } } +// EnableExamplesValidation does the opposite of DisableExamplesValidation. +// By default, all schema examples are validated. +func EnableExamplesValidation() ValidationOption { + return func(options *ValidationOptions) { + options.ExamplesValidationDisabled = false + } +} + // DisableExamplesValidation disables all example schema validation. +// By default, all schema examples are validated. func DisableExamplesValidation() ValidationOption { return func(options *ValidationOptions) { options.ExamplesValidationDisabled = true @@ -37,7 +63,14 @@ func DisableExamplesValidation() ValidationOption { } // WithValidationOptions allows adding validation options to a context object that can be used when validationg any OpenAPI type. -func WithValidationOptions(ctx context.Context, options *ValidationOptions) context.Context { +func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context { + if len(opts) == 0 { + return ctx + } + options := &ValidationOptions{} + for _, opt := range opts { + opt(options) + } return context.WithValue(ctx, validationOptionsKey{}, options) } diff --git a/openapi3/xml.go b/openapi3/xml.go index f1ab96b44..4ed3d94eb 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -29,6 +29,8 @@ func (xml *XML) UnmarshalJSON(data []byte) error { } // Validate returns an error if XML does not comply with the OpenAPI spec. -func (xml *XML) Validate(ctx context.Context) error { +func (xml *XML) Validate(ctx context.Context, opts ...ValidationOption) error { + // ctx = WithValidationOptions(ctx, opts...) + return nil // TODO } From ebbf60de70519a93a6de3761a3014bc4d98e4453 Mon Sep 17 00:00:00 2001 From: tomato0111 <119634480+tomato0111@users.noreply.github.com> Date: Tue, 6 Dec 2022 05:14:22 -0800 Subject: [PATCH 219/260] fix: setting defaults for oneOf and anyOf (#690) --- openapi3/schema.go | 17 +-- openapi3filter/validate_set_default_test.go | 116 ++++++++++++++++++++ 2 files changed, 125 insertions(+), 8 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 9f874d90e..67cc7dce0 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -934,10 +934,6 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val matchedOneOfIdx = 0 tempValue = value ) - // make a deep copy to protect origin value from being injected default value that defined in mismatched oneOf schema - if settings.asreq || settings.asrep { - tempValue = deepcopy.Copy(value) - } for idx, item := range v { v := item.Value if v == nil { @@ -948,6 +944,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val continue } + // make a deep copy to protect origin value from being injected default value that defined in mismatched oneOf schema + if settings.asreq || settings.asrep { + tempValue = deepcopy.Copy(value) + } + if err := v.visitJSON(settings, tempValue); err != nil { validationErrors = append(validationErrors, err) continue @@ -990,15 +991,15 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val matchedAnyOfIdx = 0 tempValue = value ) - // make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema - if settings.asreq || settings.asrep { - tempValue = deepcopy.Copy(value) - } for idx, item := range v { v := item.Value if v == nil { return foundUnresolvedRef(item.Ref) } + // make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema + if settings.asreq || settings.asrep { + tempValue = deepcopy.Copy(value) + } if err := v.visitJSON(settings, tempValue); err == nil { ok = true matchedAnyOfIdx = idx diff --git a/openapi3filter/validate_set_default_test.go b/openapi3filter/validate_set_default_test.go index 4550b51b2..731cbbdca 100644 --- a/openapi3filter/validate_set_default_test.go +++ b/openapi3filter/validate_set_default_test.go @@ -317,6 +317,70 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) { } } ] + }, + "contact": { + "oneOf": [ + { + "type": "object", + "required": ["email"], + "properties": { + "email": { + "type": "string" + }, + "allow_image": { + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["phone"], + "properties": { + "phone": { + "type": "string" + }, + "allow_text": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + ] + }, + "contact2": { + "anyOf": [ + { + "type": "object", + "required": ["email"], + "properties": { + "email": { + "type": "string" + }, + "allow_image": { + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["phone"], + "properties": { + "phone": { + "type": "string" + }, + "allow_text": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + ] } } } @@ -358,6 +422,10 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) { FBLink string `json:"fb_link,omitempty"` TWLink string `json:"tw_link,omitempty"` } + type contact struct { + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` + } type body struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` @@ -367,6 +435,8 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) { Filters []filter `json:"filters,omitempty"` SocialNetwork *socialNetwork `json:"social_network,omitempty"` SocialNetwork2 *socialNetwork `json:"social_network_2,omitempty"` + Contact *contact `json:"contact,omitempty"` + Contact2 *contact `json:"contact2,omitempty"` } testCases := []struct { @@ -656,6 +726,52 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) { "platform": "facebook", "fb_link": "www.facebook.com" } +} + `, body) + }, + }, + { + name: "contact(oneOf)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Contact: &contact{ + Phone: "123456", + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "contact": { + "phone": "123456", + "allow_text": false + } +} + `, body) + }, + }, + { + name: "contact(anyOf)", + body: body{ + ID: "bt6kdc3d0cvp6u8u3ft0", + Contact2: &contact{ + Phone: "123456", + }, + }, + bodyAssertion: func(t *testing.T, body string) { + require.JSONEq(t, ` +{ + "id": "bt6kdc3d0cvp6u8u3ft0", + "name": "default", + "code": 123, + "all": false, + "contact2": { + "phone": "123456", + "allow_text": false + } } `, body) }, From 871801135891449a4ba33576e883232fc513069a Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 7 Dec 2022 11:27:57 +0100 Subject: [PATCH 220/260] Try decoding as JSON first then YAML, for speed (#693) Fixes https://github.com/getkin/kin-openapi/issues/680 --- .github/workflows/go.yml | 5 +++++ openapi3/loader.go | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d6950ebbb..809476343 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -97,6 +97,11 @@ jobs: run: | ! grep -IErn '\s$' --exclude-dir={.git,target,pgdata} + - if: runner.os == 'Linux' + name: Ensure use of unmarshal + run: | + [[ "$(git grep -F yaml. -- openapi3/ | grep -v _test.go | wc -l)" = 1 ]] + - if: runner.os == 'Linux' name: Missing specification object link to definition run: | diff --git a/openapi3/loader.go b/openapi3/loader.go index 138431fc0..3979e5d54 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -115,7 +115,7 @@ func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, el if err != nil { return nil, err } - if err := yaml.Unmarshal(data, element); err != nil { + if err := unmarshal(data, element); err != nil { return nil, err } @@ -133,7 +133,7 @@ func (loader *Loader) readURL(location *url.URL) ([]byte, error) { func (loader *Loader) LoadFromData(data []byte) (*T, error) { loader.resetVisitedPathItemRefs() doc := &T{} - if err := yaml.Unmarshal(data, doc); err != nil { + if err := unmarshal(data, doc); err != nil { return nil, err } if err := loader.ResolveRefsIn(doc, nil); err != nil { @@ -162,7 +162,7 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR doc := &T{} loader.visitedDocuments[uri] = doc - if err := yaml.Unmarshal(data, doc); err != nil { + if err := unmarshal(data, doc); err != nil { return nil, err } if err := loader.ResolveRefsIn(doc, location); err != nil { @@ -172,6 +172,14 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR return doc, nil } +func unmarshal(data []byte, v interface{}) error { + // See https://github.com/getkin/kin-openapi/issues/680 + if err := json.Unmarshal(data, v); err != nil { + return yaml.Unmarshal(data, v) + } + return nil +} + // ResolveRefsIn expands references if for instance spec was just unmarshalled func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { if loader.Context == nil { @@ -319,7 +327,7 @@ func (loader *Loader) resolveComponent( if err2 != nil { return nil, err } - if err2 = yaml.Unmarshal(data, &cursor); err2 != nil { + if err2 = unmarshal(data, &cursor); err2 != nil { return nil, err } if cursor, err2 = drill(cursor); err2 != nil || cursor == nil { From f1360474effe1390d6e49ce5ffcfd831d1614260 Mon Sep 17 00:00:00 2001 From: ShouheiNishi <96609867+ShouheiNishi@users.noreply.github.com> Date: Wed, 14 Dec 2022 17:25:06 +0900 Subject: [PATCH 221/260] Use and update GetBody() member of request (#704) --- openapi3filter/validate_request.go | 23 +++++++++++++++++++++-- openapi3filter/validate_request_test.go | 6 ++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 4acb9ff1f..8a747724e 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "io" "io/ioutil" "net/http" "sort" @@ -216,7 +217,19 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req } } // Put the data back into the input - req.Body = ioutil.NopCloser(bytes.NewReader(data)) + req.Body = nil + if req.GetBody != nil { + if req.Body, err = req.GetBody(); err != nil { + req.Body = nil + } + } + if req.Body == nil { + req.ContentLength = int64(len(data)) + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(data)), nil + } + req.Body, _ = req.GetBody() // no error return + } } if len(data) == 0 { @@ -292,8 +305,14 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req } } // Put the data back into the input - req.Body = ioutil.NopCloser(bytes.NewReader(data)) + if req.Body != nil { + req.Body.Close() + } req.ContentLength = int64(len(data)) + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(data)), nil + } + req.Body, _ = req.GetBody() // no error return } return nil diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index 450ee5988..8da550ce0 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -212,6 +212,12 @@ components: assert.Equal(t, contentLen, bodySize, "expect ContentLength %d to equal body size %d", contentLen, bodySize) bodyModified := originalBodySize != bodySize assert.Equal(t, bodyModified, tc.expectedModification, "expect request body modification happened: %t, expected %t", bodyModified, tc.expectedModification) + + validationInput.Request.Body, err = validationInput.Request.GetBody() + assert.NoError(t, err, "unable to re-generate body by GetBody(): %v", err) + body2, err := io.ReadAll(validationInput.Request.Body) + assert.NoError(t, err, "unable to read request body: %v", err) + assert.Equal(t, body, body2, "body by GetBody() is not matched") }) } } From c1219e3e6686ca9d76a8829c3a2faad2f1346a71 Mon Sep 17 00:00:00 2001 From: ShouheiNishi <96609867+ShouheiNishi@users.noreply.github.com> Date: Wed, 14 Dec 2022 17:27:16 +0900 Subject: [PATCH 222/260] Bugfix/issue638 (#700) --- openapi3/issue542_test.go | 3 +- openapi3/issue615_test.go | 10 +-- openapi3/issue638_test.go | 21 ++++++ openapi3/loader.go | 103 ++++++++------------------ openapi3/testdata/issue638/test1.yaml | 15 ++++ openapi3/testdata/issue638/test2.yaml | 13 ++++ 6 files changed, 84 insertions(+), 81 deletions(-) create mode 100644 openapi3/issue638_test.go create mode 100644 openapi3/testdata/issue638/test1.yaml create mode 100644 openapi3/testdata/issue638/test2.yaml diff --git a/openapi3/issue542_test.go b/openapi3/issue542_test.go index 7e0cb88c9..4ba017aed 100644 --- a/openapi3/issue542_test.go +++ b/openapi3/issue542_test.go @@ -10,6 +10,5 @@ func TestIssue542(t *testing.T) { sl := NewLoader() _, err := sl.LoadFromFile("testdata/issue542.yml") - require.Error(t, err) - require.Contains(t, err.Error(), CircularReferenceError) + require.NoError(t, err) } diff --git a/openapi3/issue615_test.go b/openapi3/issue615_test.go index ceb317ab0..e7bd01e92 100644 --- a/openapi3/issue615_test.go +++ b/openapi3/issue615_test.go @@ -9,17 +9,11 @@ import ( ) func TestIssue615(t *testing.T) { - for { + { loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = true _, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml") - if err == nil { - continue - } - // Test currently reproduces the issue 615: failure to load a valid spec - // Upon issue resolution, this check should be changed to require.NoError - require.Error(t, err, openapi3.CircularReferenceError) - break + require.NoError(t, err) } var old int diff --git a/openapi3/issue638_test.go b/openapi3/issue638_test.go new file mode 100644 index 000000000..1db8a6f51 --- /dev/null +++ b/openapi3/issue638_test.go @@ -0,0 +1,21 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue638(t *testing.T) { + for i := 0; i < 50; i++ { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + // This path affects the occurrence of the issue #638. + // ../openapi3/testdata/issue638/test1.yaml : reproduce + // ./testdata/issue638/test1.yaml : not reproduce + // testdata/issue638/test1.yaml : reproduce + doc, err := loader.LoadFromFile("testdata/issue638/test1.yaml") + require.NoError(t, err) + require.Equal(t, "int", doc.Components.Schemas["test1d"].Value.Type) + } +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 3979e5d54..4cc83c0b2 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -287,20 +287,21 @@ func (loader *Loader) resolveComponent( path *url.URL, resolved interface{}, ) ( + componentDoc *T, componentPath *url.URL, err error, ) { - if doc, ref, componentPath, err = loader.resolveRef(doc, ref, path); err != nil { - return nil, err + if componentDoc, ref, componentPath, err = loader.resolveRef(doc, ref, path); err != nil { + return nil, nil, err } parsedURL, err := url.Parse(ref) if err != nil { - return nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) + return nil, nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) } fragment := parsedURL.Fragment if !strings.HasPrefix(fragment, "/") { - return nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) + return nil, nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) } drill := func(cursor interface{}) (interface{}, error) { @@ -318,20 +319,20 @@ func (loader *Loader) resolveComponent( return cursor, nil } var cursor interface{} - if cursor, err = drill(doc); err != nil { + if cursor, err = drill(componentDoc); err != nil { if path == nil { - return nil, err + return nil, nil, err } var err2 error data, err2 := loader.readURL(path) if err2 != nil { - return nil, err + return nil, nil, err } if err2 = unmarshal(data, &cursor); err2 != nil { - return nil, err + return nil, nil, err } if cursor, err2 = drill(cursor); err2 != nil || cursor == nil { - return nil, err + return nil, nil, err } err = nil } @@ -339,7 +340,7 @@ func (loader *Loader) resolveComponent( switch { case reflect.TypeOf(cursor) == reflect.TypeOf(resolved): reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem()) - return componentPath, nil + return componentDoc, componentPath, nil case reflect.TypeOf(cursor) == reflect.TypeOf(map[string]interface{}{}): codec := func(got, expect interface{}) error { @@ -353,12 +354,12 @@ func (loader *Loader) resolveComponent( return nil } if err := codec(cursor, resolved); err != nil { - return nil, fmt.Errorf("bad data in %q", ref) + return nil, nil, fmt.Errorf("bad data in %q", ref) } - return componentPath, nil + return componentDoc, componentPath, nil default: - return nil, fmt.Errorf("bad data in %q", ref) + return nil, nil, fmt.Errorf("bad data in %q", ref) } } @@ -429,18 +430,6 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { } } -func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef string) *url.URL { - if loader.rootDir == "" { - return current - } - - if resolvedRef == "" { - return &url.URL{Path: loader.rootLocation} - } - - return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)} -} - func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { if ref != "" && ref[0] == '#' { return doc, ref, path, nil @@ -492,7 +481,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat component.Value = &header } else { var resolved HeaderRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -500,7 +489,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + return nil } } value := component.Value @@ -540,7 +529,7 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum component.Value = ¶m } else { var resolved ParameterRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -548,7 +537,7 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + return nil } } value := component.Value @@ -597,7 +586,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d component.Value = &requestBody } else { var resolved RequestBodyRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -605,7 +594,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + return nil } } value := component.Value @@ -659,7 +648,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen component.Value = &resp } else { var resolved ResponseRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -667,7 +656,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + return nil } } value := component.Value @@ -742,7 +731,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat visited = append(visited, ref) var resolved SchemaRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -750,11 +739,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } component.Value = resolved.Value - foundPath, rerr := loader.getResolvedRefPath(ref, &resolved, documentPath, componentPath) - if rerr != nil { - return fmt.Errorf("failed to resolve file from reference %q: %w", ref, rerr) - } - documentPath = loader.documentPathForRecursiveRef(documentPath, foundPath) + return nil } if loader.visitedSchema == nil { loader.visitedSchema = make(map[*Schema]struct{}) @@ -805,30 +790,6 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return nil } -func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) (string, error) { - if referencedFilename := strings.Split(ref, "#")[0]; referencedFilename == "" { - if cur != nil { - if loader.rootDir != "" && strings.HasPrefix(cur.Path, loader.rootDir) { - return cur.Path[len(loader.rootDir)+1:], nil - } - - return path.Base(cur.Path), nil - } - return "", nil - } - // ref. to external file - if resolved.Ref != "" { - return resolved.Ref, nil - } - - if loader.rootDir == "" { - return found.Path, nil - } - - // found dest spec. file - return filepath.Rel(loader.rootDir, found.Path) -} - func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedSecurityScheme == nil { @@ -852,7 +813,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme component.Value = &scheme } else { var resolved SecuritySchemeRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -860,7 +821,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme return err } component.Value = resolved.Value - _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + return nil } } return nil @@ -889,7 +850,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP component.Value = &example } else { var resolved ExampleRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -897,7 +858,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP return err } component.Value = resolved.Value - _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + return nil } } return nil @@ -916,7 +877,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen component.Value = &resolved } else { var resolved CallbackRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -924,7 +885,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen return err } component.Value = resolved.Value - documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + return nil } } value := component.Value @@ -1014,7 +975,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u component.Value = &link } else { var resolved LinkRef - componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) + doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } @@ -1022,7 +983,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u return err } component.Value = resolved.Value - _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) + return nil } } return nil diff --git a/openapi3/testdata/issue638/test1.yaml b/openapi3/testdata/issue638/test1.yaml new file mode 100644 index 000000000..f2ab5555c --- /dev/null +++ b/openapi3/testdata/issue638/test1.yaml @@ -0,0 +1,15 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: reference test part 1 + description: reference test part 1 +components: + schemas: + test1a: + $ref: "test2.yaml#/components/schemas/test2a" + test1b: + $ref: "#/components/schemas/test1c" + test1c: + type: int + test1d: + $ref: "test2.yaml#/components/schemas/test2b" diff --git a/openapi3/testdata/issue638/test2.yaml b/openapi3/testdata/issue638/test2.yaml new file mode 100644 index 000000000..d3ca4648b --- /dev/null +++ b/openapi3/testdata/issue638/test2.yaml @@ -0,0 +1,13 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: reference test part 2 + description: reference test part 2 +components: + schemas: + test2a: + type: number + test2b: + $ref: "test1.yaml#/components/schemas/test1b" + test1c: + type: string From b00342138e12bdda28b83d2dd47b77ffa6f8c876 Mon Sep 17 00:00:00 2001 From: ShouheiNishi <96609867+ShouheiNishi@users.noreply.github.com> Date: Wed, 14 Dec 2022 19:15:49 +0900 Subject: [PATCH 223/260] Add json patch support (#702) --- openapi3filter/req_resp_decoder.go | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 029a84c2c..52c65afaa 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -999,6 +999,7 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, func init() { RegisterBodyDecoder("text/plain", plainBodyDecoder) RegisterBodyDecoder("application/json", jsonBodyDecoder) + RegisterBodyDecoder("application/json-patch+json", jsonBodyDecoder) RegisterBodyDecoder("application/x-yaml", yamlBodyDecoder) RegisterBodyDecoder("application/yaml", yamlBodyDecoder) RegisterBodyDecoder("application/problem+json", jsonBodyDecoder) From 7413c27ab9eefcc373d894d98834c62e40bcd423 Mon Sep 17 00:00:00 2001 From: slessard Date: Wed, 14 Dec 2022 02:24:06 -0800 Subject: [PATCH 224/260] openapi3filter: Include schema ref or title in response body validation errors (#699) Co-authored-by: Steve Lessard --- openapi3filter/options_test.go | 2 +- openapi3filter/validate_request.go | 4 +++- openapi3filter/validate_response.go | 28 ++++++++++++++++++++++++- openapi3filter/validation_error_test.go | 12 +++++------ routers/gorillamux/example_test.go | 2 +- routers/legacy/validate_request_test.go | 2 +- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/openapi3filter/options_test.go b/openapi3filter/options_test.go index a95b6bb96..fd19329ff 100644 --- a/openapi3filter/options_test.go +++ b/openapi3filter/options_test.go @@ -78,5 +78,5 @@ paths: fmt.Println(err.Error()) - // Output: request body has an error: doesn't match the schema: field "Some field" must be an integer + // Output: request body has an error: doesn't match schema: field "Some field" must be an integer } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 8a747724e..2424eb9ed 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -286,10 +286,12 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req // Validate JSON with the schema if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { + schemaId := getSchemaIdentifier(contentType.Schema) + schemaId = prependSpaceIfNeeded(schemaId) return &RequestError{ Input: input, RequestBody: requestBody, - Reason: "doesn't match the schema", + Reason: fmt.Sprintf("doesn't match schema%s", schemaId), Err: err, } } diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index abcbb4e9d..27bef82d3 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "sort" + "strings" "github.com/getkin/kin-openapi/openapi3" ) @@ -159,11 +160,36 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error // Validate data with the schema. if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil { + schemaId := getSchemaIdentifier(contentType.Schema) + schemaId = prependSpaceIfNeeded(schemaId) return &ResponseError{ Input: input, - Reason: "response body doesn't match the schema", + Reason: fmt.Sprintf("response body doesn't match schema%s", schemaId), Err: err, } } return nil } + +// getSchemaIdentifier gets something by which a schema could be identified. +// A schema by itself doesn't have a true identity field. This function makes +// a best effort to get a value that can fill that void. +func getSchemaIdentifier(schema *openapi3.SchemaRef) string { + var id string + + if schema != nil { + id = strings.TrimSpace(schema.Ref) + } + if id == "" && schema.Value != nil { + id = strings.TrimSpace(schema.Value.Title) + } + + return id +} + +func prependSpaceIfNeeded(value string) string { + if len(value) > 0 { + value = " " + value + } + return value +} diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 6fee1355d..b84d8bdb6 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -322,7 +322,7 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", wantErrSchemaValue: "watdis", wantErrSchemaPath: "/status", @@ -336,7 +336,7 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama"}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: `property "photoUrls" is missing`, wantErrSchemaValue: map[string]string{"name": "Bahama"}, wantErrSchemaPath: "/photoUrls", @@ -350,7 +350,7 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{}}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: `property "name" is missing`, wantErrSchemaValue: map[string]string{}, wantErrSchemaPath: "/category/name", @@ -364,7 +364,7 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{"tags": [{}]}}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: `property "name" is missing`, wantErrSchemaValue: map[string]string{}, wantErrSchemaPath: "/category/tags/0/name", @@ -378,7 +378,7 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: "field must be set to array or not be present", wantErrSchemaPath: "/photoUrls", wantErrSchemaValue: "string", @@ -393,7 +393,7 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet2", bytes.NewBufferString(`{"name":"Bahama"}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema", wantErrSchemaPath: "/", wantErrSchemaValue: map[string]string{"name": "Bahama"}, wantErrSchemaOriginReason: `property "photoUrls" is missing`, diff --git a/routers/gorillamux/example_test.go b/routers/gorillamux/example_test.go index 2ca3225a5..54058cde2 100644 --- a/routers/gorillamux/example_test.go +++ b/routers/gorillamux/example_test.go @@ -53,7 +53,7 @@ func Example() { err = openapi3filter.ValidateResponse(ctx, responseValidationInput) fmt.Println(err) // Output: - // response body doesn't match the schema: field must be set to string or not be present + // response body doesn't match schema pathref.openapi.yml#/components/schemas/TestSchema: field must be set to string or not be present // Schema: // { // "type": "string" diff --git a/routers/legacy/validate_request_test.go b/routers/legacy/validate_request_test.go index 5b9518c78..9c15ed44a 100644 --- a/routers/legacy/validate_request_test.go +++ b/routers/legacy/validate_request_test.go @@ -107,6 +107,6 @@ func Example() { fmt.Println(err) } // Output: - // request body has an error: doesn't match the schema: input matches more than one oneOf schemas + // request body has an error: doesn't match schema: input matches more than one oneOf schemas } From 6cbc1b03d9536886de4a37a5505ecc7d44ace6d7 Mon Sep 17 00:00:00 2001 From: slessard Date: Fri, 16 Dec 2022 05:58:05 -0800 Subject: [PATCH 225/260] openapi3filter: parse integers with strconv.ParseInt instead of ParseFloat (#711) Co-authored-by: Steve Lessard --- openapi3filter/issue625_test.go | 2 +- openapi3filter/req_resp_decoder.go | 9 ++++++++- openapi3filter/req_resp_decoder_test.go | 25 ++++++++++++++++--------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/openapi3filter/issue625_test.go b/openapi3filter/issue625_test.go index d9e5bae47..5642a7e00 100644 --- a/openapi3filter/issue625_test.go +++ b/openapi3filter/issue625_test.go @@ -72,7 +72,7 @@ paths: name: "failed allof object array", spec: allOfArraySpec, req: `/items?test=1.2,3.1`, - errStr: `parameter "test" in query has an error: Error at "/0": value "1.2" must be an integer`, + errStr: `parameter "test" in query has an error: path 0: value 1.2: an invalid integer: invalid syntax`, }, { name: "success oneof object array", diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 52c65afaa..870651ce6 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -895,7 +895,14 @@ func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) } switch schema.Value.Type { case "integer": - v, err := strconv.ParseFloat(raw, 64) + if schema.Value.Format == "int32" { + v, err := strconv.ParseInt(raw, 0, 32) + if err != nil { + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} + } + return int32(v), nil + } + v, err := strconv.ParseInt(raw, 0, 64) if err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} } diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 8bd62b1c5..709cdc929 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -175,7 +175,7 @@ func TestDecodeParameter(t *testing.T) { name: "integer", param: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema}, path: "/1", - want: float64(1), + want: int64(1), found: true, }, { @@ -456,7 +456,7 @@ func TestDecodeParameter(t *testing.T) { name: "integer", param: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema}, query: "param=1", - want: float64(1), + want: int64(1), found: true, }, { @@ -522,7 +522,7 @@ func TestDecodeParameter(t *testing.T) { name: "anyofSchema integer", param: &openapi3.Parameter{Name: "param", In: "query", Schema: anyofSchema}, query: "param=1", - want: float64(1), + want: int64(1), found: true, }, { @@ -548,7 +548,7 @@ func TestDecodeParameter(t *testing.T) { name: "oneofSchema int", param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema}, query: "param=1122", - want: float64(1122), + want: int64(1122), found: true, }, { @@ -724,7 +724,7 @@ func TestDecodeParameter(t *testing.T) { name: "integer", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema}, header: "X-Param:1", - want: float64(1), + want: int64(1), found: true, }, { @@ -835,6 +835,13 @@ func TestDecodeParameter(t *testing.T) { want: map[string]interface{}{"id": "foo", "name": "bar"}, found: true, }, + { + name: "valid integer prop", + param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema}, + header: "X-Param:88", + found: true, + want: int64(88), + }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", integerSchema)}, @@ -893,7 +900,7 @@ func TestDecodeParameter(t *testing.T) { name: "integer", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema}, cookie: "X-Param:1", - want: float64(1), + want: int64(1), found: true, }, { @@ -1180,7 +1187,7 @@ func TestDecodeBody(t *testing.T) { WithProperty("a", openapi3.NewStringSchema()). WithProperty("b", openapi3.NewIntegerSchema()). WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())), - want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, }, { name: "urlencoded space delimited", @@ -1193,7 +1200,7 @@ func TestDecodeBody(t *testing.T) { encoding: map[string]*openapi3.Encoding{ "c": {Style: openapi3.SerializationSpaceDelimited, Explode: boolPtr(false)}, }, - want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, }, { name: "urlencoded pipe delimited", @@ -1206,7 +1213,7 @@ func TestDecodeBody(t *testing.T) { encoding: map[string]*openapi3.Encoding{ "c": {Style: openapi3.SerializationPipeDelimited, Explode: boolPtr(false)}, }, - want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, }, { name: "multipart", From 6a3b77949209f5c8c8a938d423025be556d5300f Mon Sep 17 00:00:00 2001 From: slessard Date: Fri, 16 Dec 2022 06:07:16 -0800 Subject: [PATCH 226/260] Fix inconsistent processing of server variables in gorillamux router (#705) Co-authored-by: Steve Lessard --- routers/gorillamux/router.go | 60 ++++++----- routers/gorillamux/router_test.go | 163 +++++++++++++++++++++++++++++- 2 files changed, 196 insertions(+), 27 deletions(-) diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 811ba7d16..6977808ed 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -57,8 +57,6 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { muxRouter := mux.NewRouter().UseEncodedPath() r := &Router{} for _, path := range orderedPaths(doc.Paths) { - servers := servers - pathItem := doc.Paths[path] if len(pathItem.Servers) > 0 { if servers, err = makeServers(pathItem.Servers); err != nil { @@ -140,19 +138,13 @@ func makeServers(in openapi3.Servers) ([]srv, error) { if lhs := strings.TrimSuffix(serverURL, server.Variables[sVar].Default); lhs != "" { varsUpdater = func(vars map[string]string) { vars[sVar] = lhs } } - servers = append(servers, srv{ - base: server.Variables[sVar].Default, - server: server, - varsUpdater: varsUpdater, - }) - continue - } + svr, err := newSrv(serverURL, server, varsUpdater) + if err != nil { + return nil, err + } - var schemes []string - if strings.Contains(serverURL, "://") { - scheme0 := strings.Split(serverURL, "://")[0] - schemes = permutePart(scheme0, server) - serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1) + servers = append(servers, svr) + continue } // If a variable represents the port "http://domain.tld:{port}/bla" @@ -172,21 +164,11 @@ func makeServers(in openapi3.Servers) ([]srv, error) { } } - u, err := url.Parse(bEncode(serverURL)) + svr, err := newSrv(serverURL, server, varsUpdater) if err != nil { return nil, err } - path := bDecode(u.EscapedPath()) - if len(path) > 0 && path[len(path)-1] == '/' { - path = path[:len(path)-1] - } - servers = append(servers, srv{ - host: bDecode(u.Host), //u.Hostname()? - base: path, - schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624 - server: server, - varsUpdater: varsUpdater, - }) + servers = append(servers, svr) } if len(servers) == 0 { servers = append(servers, srv{}) @@ -195,6 +177,32 @@ func makeServers(in openapi3.Servers) ([]srv, error) { return servers, nil } +func newSrv(serverURL string, server *openapi3.Server, varsUpdater varsf) (srv, error) { + var schemes []string + if strings.Contains(serverURL, "://") { + scheme0 := strings.Split(serverURL, "://")[0] + schemes = permutePart(scheme0, server) + serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1) + } + + u, err := url.Parse(bEncode(serverURL)) + if err != nil { + return srv{}, err + } + path := bDecode(u.EscapedPath()) + if len(path) > 0 && path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + svr := srv{ + host: bDecode(u.Host), //u.Hostname()? + base: path, + schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624 + server: server, + varsUpdater: varsUpdater, + } + return svr, nil +} + func orderedPaths(paths map[string]*openapi3.PathItem) []string { // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathsObject // When matching URLs, concrete (non-templated) paths would be matched diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go index 104056e18..3e7440063 100644 --- a/routers/gorillamux/router_test.go +++ b/routers/gorillamux/router_test.go @@ -6,6 +6,7 @@ import ( "sort" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/getkin/kin-openapi/openapi3" @@ -249,7 +250,16 @@ func TestServerPath(t *testing.T) { "http://example.com:{port}/path", map[string]string{ "port": "8088", - })}, + }), + newServerWithVariables( + "{server}", + map[string]string{ + "server": "/", + }), + newServerWithVariables( + "/", + nil, + )}, }) require.NoError(t, err) } @@ -325,6 +335,157 @@ func TestRelativeURL(t *testing.T) { require.Equal(t, "/hello", route.Path) } +func Test_makeServers(t *testing.T) { + type testStruct struct { + name string + servers openapi3.Servers + want []srv + wantErr bool + initFn func(tt *testStruct) + } + tests := []testStruct{ + { + name: "server is root path", + servers: openapi3.Servers{ + newServerWithVariables("/", nil), + }, + want: []srv{{ + schemes: nil, + host: "", + base: "", + server: nil, + varsUpdater: nil, + }}, + wantErr: false, + initFn: func(tt *testStruct) { + for i, server := range tt.servers { + tt.want[i].server = server + } + }, + }, + { + name: "server with single variable that evaluates to root path", + servers: openapi3.Servers{ + newServerWithVariables("{server}", map[string]string{"server": "/"}), + }, + want: []srv{{ + schemes: nil, + host: "", + base: "", + server: nil, + varsUpdater: nil, + }}, + wantErr: false, + initFn: func(tt *testStruct) { + for i, server := range tt.servers { + tt.want[i].server = server + } + }, + }, + { + name: "server is http://localhost:28002", + servers: openapi3.Servers{ + newServerWithVariables("http://localhost:28002", nil), + }, + want: []srv{{ + schemes: []string{"http"}, + host: "localhost:28002", + base: "", + server: nil, + varsUpdater: nil, + }}, + wantErr: false, + initFn: func(tt *testStruct) { + for i, server := range tt.servers { + tt.want[i].server = server + } + }, + }, + { + name: "server with single variable that evaluates to http://localhost:28002", + servers: openapi3.Servers{ + newServerWithVariables("{server}", map[string]string{"server": "http://localhost:28002"}), + }, + want: []srv{{ + schemes: []string{"http"}, + host: "localhost:28002", + base: "", + server: nil, + varsUpdater: nil, + }}, + wantErr: false, + initFn: func(tt *testStruct) { + for i, server := range tt.servers { + tt.want[i].server = server + } + }, + }, + { + name: "server with multiple variables that evaluates to http://localhost:28002", + servers: openapi3.Servers{ + newServerWithVariables("{scheme}://{host}:{port}", map[string]string{"scheme": "http", "host": "localhost", "port": "28002"}), + }, + want: []srv{{ + schemes: []string{"http"}, + host: "{host}:28002", + base: "", + server: nil, + varsUpdater: func(vars map[string]string) { vars["port"] = "28002" }, + }}, + wantErr: false, + initFn: func(tt *testStruct) { + for i, server := range tt.servers { + tt.want[i].server = server + } + }, + }, + { + name: "server with unparsable URL fails", + servers: openapi3.Servers{ + newServerWithVariables("exam^ple.com:443", nil), + }, + want: nil, + wantErr: true, + initFn: nil, + }, + { + name: "server with single variable that evaluates to unparsable URL fails", + servers: openapi3.Servers{ + newServerWithVariables("{server}", map[string]string{"server": "exam^ple.com:443"}), + }, + want: nil, + wantErr: true, + initFn: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.initFn != nil { + tt.initFn(&tt) + } + got, err := makeServers(tt.servers) + if (err != nil) != tt.wantErr { + t.Errorf("makeServers() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, len(tt.want), len(got), "expected and actual servers lengths are not equal") + for i := 0; i < len(tt.want); i++ { + // Unfortunately using assert.Equals or reflect.DeepEquals isn't + // an option because function pointers cannot be compared + assert.Equal(t, tt.want[i].schemes, got[i].schemes) + assert.Equal(t, tt.want[i].host, got[i].host) + assert.Equal(t, tt.want[i].host, got[i].host) + assert.Equal(t, tt.want[i].server, got[i].server) + if tt.want[i].varsUpdater == nil { + assert.Nil(t, got[i].varsUpdater, "expected and actual varsUpdater should point to same function") + } else { + assert.NotNil(t, got[i].varsUpdater, "expected and actual varsUpdater should point to same function") + } + } + }) + } +} + func newServerWithVariables(url string, variables map[string]string) *openapi3.Server { var serverVariables = map[string]*openapi3.ServerVariable{} From 35bb627604661b8016d826357aacfdfea4c1d492 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 16 Dec 2022 15:31:42 +0100 Subject: [PATCH 227/260] Fix links to OpenAPI spec after GitHub changes (#714) --- openapi3/callback.go | 2 +- openapi3/components.go | 2 +- openapi3/discriminator.go | 2 +- openapi3/doc.go | 2 +- openapi3/encoding.go | 2 +- openapi3/example.go | 2 +- openapi3/header.go | 2 +- openapi3/info.go | 6 +++--- openapi3/link.go | 2 +- openapi3/media_type.go | 2 +- openapi3/openapi3.go | 2 +- openapi3/parameter.go | 2 +- openapi3/path_item.go | 2 +- openapi3/refs.go | 2 +- openapi3/request_body.go | 2 +- openapi3/response.go | 4 ++-- openapi3/schema.go | 2 +- openapi3/security_requirements.go | 2 +- openapi3/security_scheme.go | 6 +++--- openapi3/security_scheme_test.go | 2 +- openapi3/server.go | 2 +- openapi3/tag.go | 2 +- openapi3/xml.go | 2 +- 23 files changed, 28 insertions(+), 28 deletions(-) diff --git a/openapi3/callback.go b/openapi3/callback.go index 6246d6d8c..62cea72d8 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -26,7 +26,7 @@ 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#callbackObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object type Callback map[string]*PathItem // Validate returns an error if Callback does not comply with the OpenAPI spec. diff --git a/openapi3/components.go b/openapi3/components.go index 16b39f303..8abc3fe9c 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -10,7 +10,7 @@ import ( ) // Components is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object type Components struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 8eb296024..8ab344a84 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -7,7 +7,7 @@ import ( ) // Discriminator is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object type Discriminator struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/doc.go b/openapi3/doc.go index fc2735cb7..41c9965c6 100644 --- a/openapi3/doc.go +++ b/openapi3/doc.go @@ -1,4 +1,4 @@ // Package openapi3 parses and writes OpenAPI 3 specification documents. // -// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md +// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md package openapi3 diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 082d3f2ec..003833e16 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -9,7 +9,7 @@ import ( ) // Encoding is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object type Encoding struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/example.go b/openapi3/example.go index e75d4d3d5..f4cbfc074 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -28,7 +28,7 @@ func (e Examples) JSONLookup(token string) (interface{}, error) { } // Example is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object type Example struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/header.go b/openapi3/header.go index aefaa06a3..454fad51e 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -28,7 +28,7 @@ func (h Headers) JSONLookup(token string) (interface{}, error) { } // Header is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#headerObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#header-object type Header struct { Parameter } diff --git a/openapi3/info.go b/openapi3/info.go index c67f73ab8..72076095e 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -8,7 +8,7 @@ import ( ) // Info is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object type Info struct { ExtensionProps `json:"-" yaml:"-"` @@ -58,7 +58,7 @@ func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error } // Contact is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contactObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object type Contact struct { ExtensionProps `json:"-" yaml:"-"` @@ -85,7 +85,7 @@ func (contact *Contact) Validate(ctx context.Context, opts ...ValidationOption) } // License is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#licenseObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#license-object type License struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/link.go b/openapi3/link.go index 1040a0408..137aef309 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -28,7 +28,7 @@ func (links Links) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*Links)(nil) // Link is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#linkObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object type Link struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 74c11b78c..8e3fef7e6 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -12,7 +12,7 @@ import ( ) // MediaType is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object type MediaType struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 714f28030..d9506a9cd 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -9,7 +9,7 @@ import ( ) // T is the root of an OpenAPI v3 document -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oasObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#openapi-object type T struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 9124d92a4..c55af474d 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -90,7 +90,7 @@ func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOpt } // Parameter is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameterObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object type Parameter struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 5cba0a876..db83a0a5a 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -10,7 +10,7 @@ import ( ) // PathItem is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#path-item-object type PathItem struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/refs.go b/openapi3/refs.go index 7311c9d34..d36d562fe 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -9,7 +9,7 @@ import ( ) // Ref is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#referenceObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object type Ref struct { Ref string `json:"$ref" yaml:"$ref"` } diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 225c3d3c7..baf7f81e8 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -28,7 +28,7 @@ func (r RequestBodies) JSONLookup(token string) (interface{}, error) { } // RequestBody is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#requestBodyObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#request-body-object type RequestBody struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/response.go b/openapi3/response.go index d2f907d12..eaf8e57f8 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -13,7 +13,7 @@ import ( ) // Responses is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responsesObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responses-object type Responses map[string]*ResponseRef var _ jsonpointer.JSONPointable = (*Responses)(nil) @@ -68,7 +68,7 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) { } // Response is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responseObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object type Response struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/schema.go b/openapi3/schema.go index 67cc7dce0..c5af8536e 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -111,7 +111,7 @@ func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { } // Schema is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object type Schema struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index dcdad0c4d..3f5bd9510 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -28,7 +28,7 @@ func (srs SecurityRequirements) Validate(ctx context.Context, opts ...Validation } // SecurityRequirement is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securityRequirementObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object type SecurityRequirement map[string][]string func NewSecurityRequirement() SecurityRequirement { diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 2b3235dfc..83330f24a 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -29,7 +29,7 @@ func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) // SecurityScheme is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securitySchemeObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object type SecurityScheme struct { ExtensionProps `json:"-" yaml:"-"` @@ -177,7 +177,7 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption } // OAuthFlows is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowsObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object type OAuthFlows struct { ExtensionProps `json:"-" yaml:"-"` @@ -234,7 +234,7 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) } // OAuthFlow is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object type OAuthFlow struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/security_scheme_test.go b/openapi3/security_scheme_test.go index 5958c5330..48ea04604 100644 --- a/openapi3/security_scheme_test.go +++ b/openapi3/security_scheme_test.go @@ -30,7 +30,7 @@ func TestSecuritySchemaExample(t *testing.T) { } } -// from https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#fixed-fields-23 +// from https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-23 var securitySchemeExamples = []securitySchemeExample{ { title: "Basic Authentication Sample", diff --git a/openapi3/server.go b/openapi3/server.go index 304799634..587e8e0e1 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -50,7 +50,7 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) } // Server is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object type Server struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/tag.go b/openapi3/tag.go index f151e5032..b5cb7f899 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -32,7 +32,7 @@ func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error { } // Tag is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object type Tag struct { ExtensionProps `json:"-" yaml:"-"` diff --git a/openapi3/xml.go b/openapi3/xml.go index 4ed3d94eb..a55ff410d 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -7,7 +7,7 @@ import ( ) // XML is specified by OpenAPI/Swagger standard version 3. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object type XML struct { ExtensionProps `json:"-" yaml:"-"` From 2975a21ed6b57ea97427c31a297c368fb1ce35de Mon Sep 17 00:00:00 2001 From: Cosmos Nicolaou Date: Fri, 16 Dec 2022 10:58:33 -0800 Subject: [PATCH 228/260] openapi3: patch YAML serialization of dates (#698) Co-authored-by: Pierre Fenoll --- openapi3/issue697_test.go | 15 +++++++++++++++ openapi3/schema.go | 10 +++++++++- openapi3/testdata/issue697.yml | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 openapi3/issue697_test.go create mode 100644 openapi3/testdata/issue697.yml diff --git a/openapi3/issue697_test.go b/openapi3/issue697_test.go new file mode 100644 index 000000000..c7317584a --- /dev/null +++ b/openapi3/issue697_test.go @@ -0,0 +1,15 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue697(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromFile("testdata/issue697.yml") + require.NoError(t, err) + err = doc.Validate(loader.Context) + require.NoError(t, err) +} diff --git a/openapi3/schema.go b/openapi3/schema.go index c5af8536e..41ccaafef 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -12,6 +12,7 @@ import ( "regexp" "sort" "strconv" + "strings" "unicode/utf16" "github.com/go-openapi/jsonpointer" @@ -180,7 +181,14 @@ func (schema *Schema) MarshalJSON() ([]byte, error) { // UnmarshalJSON sets Schema to a copy of data. func (schema *Schema) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, schema) + err := jsoninfo.UnmarshalStrictStruct(data, schema) + if schema.Format == "date" { + // This is a fix for: https://github.com/getkin/kin-openapi/issues/697 + if eg, ok := schema.Example.(string); ok { + schema.Example = strings.TrimSuffix(eg, "T00:00:00Z") + } + } + return err } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable diff --git a/openapi3/testdata/issue697.yml b/openapi3/testdata/issue697.yml new file mode 100644 index 000000000..71a4b2ae2 --- /dev/null +++ b/openapi3/testdata/issue697.yml @@ -0,0 +1,14 @@ +openapi: 3.0.1 +components: + schemas: + API: + properties: + dateExample: + type: string + format: date + example: 2019-09-12 +info: + title: sample + version: version not set +paths: {} + From 25a5fe41669cfc1238b3a999188a57a83cf076cd Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Sat, 17 Dec 2022 18:08:31 +0100 Subject: [PATCH 229/260] Leave allocation capacity guessing to the runtime (#716) --- jsoninfo/marshal.go | 2 +- openapi2/openapi2.go | 4 ++-- openapi2conv/openapi2_conv.go | 2 +- openapi3/content.go | 2 +- openapi3/path_item.go | 2 +- routers/legacy/router.go | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/jsoninfo/marshal.go b/jsoninfo/marshal.go index 6e946d877..f2abf7c00 100644 --- a/jsoninfo/marshal.go +++ b/jsoninfo/marshal.go @@ -23,7 +23,7 @@ type ObjectEncoder struct { func NewObjectEncoder() *ObjectEncoder { return &ObjectEncoder{ - result: make(map[string]json.RawMessage, 8), + result: make(map[string]json.RawMessage), } } diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index dcc5ddb66..430e39851 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -42,7 +42,7 @@ func (doc *T) UnmarshalJSON(data []byte) error { func (doc *T) AddOperation(path string, method string, operation *Operation) { paths := doc.Paths if paths == nil { - paths = make(map[string]*PathItem, 8) + paths = make(map[string]*PathItem) doc.Paths = paths } pathItem := paths[path] @@ -77,7 +77,7 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error { } func (pathItem *PathItem) Operations() map[string]*Operation { - operations := make(map[string]*Operation, 8) + operations := make(map[string]*Operation) if v := pathItem.Delete; v != nil { operations[http.MethodDelete] = v } diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 8c082053f..b81e3c2c4 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -1206,7 +1206,7 @@ func stripNonCustomExtensions(extensions map[string]interface{}) { func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.ExtensionProps) { paths := doc2.Paths if paths == nil { - paths = make(map[string]*openapi2.PathItem, 8) + paths = make(map[string]*openapi2.PathItem) doc2.Paths = paths } pathItem := paths[path] diff --git a/openapi3/content.go b/openapi3/content.go index 8abd411da..81b070eec 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -10,7 +10,7 @@ import ( type Content map[string]*MediaType func NewContent() Content { - return make(map[string]*MediaType, 4) + return make(map[string]*MediaType) } func NewContentWithSchema(schema *Schema, consumes []string) Content { diff --git a/openapi3/path_item.go b/openapi3/path_item.go index db83a0a5a..5323dc163 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -41,7 +41,7 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error { } func (pathItem *PathItem) Operations() map[string]*Operation { - operations := make(map[string]*Operation, 4) + operations := make(map[string]*Operation) if v := pathItem.Connect; v != nil { operations[http.MethodConnect] = v } diff --git a/routers/legacy/router.go b/routers/legacy/router.go index 74e387323..911422b85 100644 --- a/routers/legacy/router.go +++ b/routers/legacy/router.go @@ -124,7 +124,7 @@ func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]s Reason: routers.ErrPathNotFound.Error(), } } - pathParams = make(map[string]string, 8) + pathParams = make(map[string]string) paramNames, err := server.ParameterNames() if err != nil { return nil, nil, err From 3be535f1631d9a5f757f05da1e201baf7342663a Mon Sep 17 00:00:00 2001 From: slessard Date: Sat, 17 Dec 2022 11:26:13 -0800 Subject: [PATCH 230/260] openapi3filter: validate non-string headers (#712) Co-authored-by: Steve Lessard --- openapi3/schema.go | 4 + openapi3filter/issue201_test.go | 8 +- openapi3filter/req_resp_decoder.go | 4 +- openapi3filter/validate_response.go | 62 +++++-- openapi3filter/validate_response_test.go | 215 +++++++++++++++++++++++ 5 files changed, 271 insertions(+), 22 deletions(-) create mode 100644 openapi3filter/validate_response_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 41ccaafef..bbce46c7b 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1050,6 +1050,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return } +// The value is not considered in visitJSONNull because according to the spec +// "null is not supported as a type" unless `nullable` is also set to true +// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#data-types +// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) { if schema.Nullable { return diff --git a/openapi3filter/issue201_test.go b/openapi3filter/issue201_test.go index 8b2b99d0e..7e2eaabe1 100644 --- a/openapi3filter/issue201_test.go +++ b/openapi3filter/issue201_test.go @@ -17,7 +17,7 @@ func TestIssue201(t *testing.T) { loader := openapi3.NewLoader() ctx := loader.Context spec := ` -openapi: '3' +openapi: '3.0.3' info: version: 1.0.0 title: Sample API @@ -37,20 +37,24 @@ paths: description: '' required: true schema: + type: string pattern: '^blip$' x-blop: description: '' schema: + type: string pattern: '^blop$' X-Blap: description: '' required: true schema: + type: string pattern: '^blap$' X-Blup: description: '' required: true schema: + type: string pattern: '^blup$' `[1:] @@ -94,7 +98,7 @@ paths: }, "invalid required header": { - err: `response header "X-Blup" doesn't match the schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`, + err: `response header "X-Blup" doesn't match schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`, headers: map[string]string{ "X-Blip": "blip", "x-blop": "blop", diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 870651ce6..4791b4ad4 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -339,7 +339,7 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho } _, found = vDecoder.values[param] case *headerParamDecoder: - _, found = vDecoder.header[param] + _, found = vDecoder.header[http.CanonicalHeaderKey(param)] case *cookieParamDecoder: _, err := vDecoder.req.Cookie(param) found = err != http.ErrNoCookie @@ -888,7 +888,7 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err // parsePrimitive returns a value that is created by parsing a source string to a primitive type // that is specified by a schema. The function returns nil when the source string is empty. -// The function panics when a schema has a non primitive type. +// The function panics when a schema has a non-primitive type. func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) { if raw == "" { return nil, nil diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index 27bef82d3..c1be31928 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -78,24 +78,10 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error } } sort.Strings(headers) - for _, k := range headers { - s := response.Headers[k] - h := input.Header.Get(k) - if h == "" { - if s.Value.Required { - return &ResponseError{ - Input: input, - Reason: fmt.Sprintf("response header %q missing", k), - } - } - continue - } - if err := s.Value.Schema.Value.VisitJSON(h, opts...); err != nil { - return &ResponseError{ - Input: input, - Reason: fmt.Sprintf("response header %q doesn't match the schema", k), - Err: err, - } + for _, headerName := range headers { + headerRef := response.Headers[headerName] + if err := validateResponseHeader(headerName, headerRef, input, opts); err != nil { + return err } } @@ -171,6 +157,46 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error return nil } +func validateResponseHeader(headerName string, headerRef *openapi3.HeaderRef, input *ResponseValidationInput, opts []openapi3.SchemaValidationOption) error { + var err error + var decodedValue interface{} + var found bool + var sm *openapi3.SerializationMethod + dec := &headerParamDecoder{header: input.Header} + + if sm, err = headerRef.Value.SerializationMethod(); err != nil { + return &ResponseError{ + Input: input, + Reason: fmt.Sprintf("unable to get header %q serialization method", headerName), + Err: err, + } + } + + if decodedValue, found, err = decodeValue(dec, headerName, sm, headerRef.Value.Schema, headerRef.Value.Required); err != nil { + return &ResponseError{ + Input: input, + Reason: fmt.Sprintf("unable to decode header %q value", headerName), + Err: err, + } + } + + if found { + if err = headerRef.Value.Schema.Value.VisitJSON(decodedValue, opts...); err != nil { + return &ResponseError{ + Input: input, + Reason: fmt.Sprintf("response header %q doesn't match schema", headerName), + Err: err, + } + } + } else if headerRef.Value.Required { + return &ResponseError{ + Input: input, + Reason: fmt.Sprintf("response header %q missing", headerName), + } + } + return nil +} + // getSchemaIdentifier gets something by which a schema could be identified. // A schema by itself doesn't have a true identity field. This function makes // a best effort to get a value that can fill that void. diff --git a/openapi3filter/validate_response_test.go b/openapi3filter/validate_response_test.go new file mode 100644 index 000000000..5ce657b0b --- /dev/null +++ b/openapi3filter/validate_response_test.go @@ -0,0 +1,215 @@ +package openapi3filter + +import ( + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +func Test_validateResponseHeader(t *testing.T) { + type args struct { + headerName string + headerRef *openapi3.HeaderRef + } + tests := []struct { + name string + args args + isHeaderPresent bool + headerVals []string + wantErr bool + wantErrMsg string + }{ + { + name: "test required string header with single string value", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewStringSchema(), true), + }, + isHeaderPresent: true, + headerVals: []string{"blab"}, + wantErr: false, + }, + { + name: "test required string header with single, empty string value", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewStringSchema(), true), + }, + isHeaderPresent: true, + headerVals: []string{""}, + wantErr: true, + wantErrMsg: `response header "X-Blab" doesn't match schema: Value is not nullable`, + }, + { + name: "test optional string header with single string value", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewStringSchema(), false), + }, + isHeaderPresent: false, + headerVals: []string{"blab"}, + wantErr: false, + }, + { + name: "test required, but missing string header", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewStringSchema(), true), + }, + isHeaderPresent: false, + headerVals: nil, + wantErr: true, + wantErrMsg: `response header "X-Blab" missing`, + }, + { + name: "test integer header with single integer value", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewIntegerSchema(), true), + }, + isHeaderPresent: true, + headerVals: []string{"88"}, + wantErr: false, + }, + { + name: "test integer header with single string value", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewIntegerSchema(), true), + }, + isHeaderPresent: true, + headerVals: []string{"blab"}, + wantErr: true, + wantErrMsg: `unable to decode header "X-Blab" value: value blab: an invalid integer: invalid syntax`, + }, + { + name: "test int64 header with single int64 value", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewInt64Schema(), true), + }, + isHeaderPresent: true, + headerVals: []string{"88"}, + wantErr: false, + }, + { + name: "test int32 header with single int32 value", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewInt32Schema(), true), + }, + isHeaderPresent: true, + headerVals: []string{"88"}, + wantErr: false, + }, + { + name: "test float64 header with single float64 value", + args: args{ + headerName: "X-Blab", + headerRef: newHeaderRef(openapi3.NewFloat64Schema(), true), + }, + isHeaderPresent: true, + headerVals: []string{"88.87"}, + wantErr: false, + }, + { + name: "test integer header with multiple csv integer values", + args: args{ + headerName: "X-blab", + headerRef: newHeaderRef(newArraySchema(openapi3.NewIntegerSchema()), true), + }, + isHeaderPresent: true, + headerVals: []string{"87,88"}, + wantErr: false, + }, + { + name: "test integer header with multiple integer values", + args: args{ + headerName: "X-blab", + headerRef: newHeaderRef(newArraySchema(openapi3.NewIntegerSchema()), true), + }, + isHeaderPresent: true, + headerVals: []string{"87", "88"}, + wantErr: false, + }, + { + name: "test non-typed, nullable header with single string value", + args: args{ + headerName: "X-blab", + headerRef: newHeaderRef(&openapi3.Schema{Nullable: true}, true), + }, + isHeaderPresent: true, + headerVals: []string{"blab"}, + wantErr: false, + }, + { + name: "test required non-typed, nullable header not present", + args: args{ + headerName: "X-blab", + headerRef: newHeaderRef(&openapi3.Schema{Nullable: true}, true), + }, + isHeaderPresent: false, + headerVals: []string{"blab"}, + wantErr: true, + wantErrMsg: `response header "X-blab" missing`, + }, + { + name: "test non-typed, non-nullable header with single string value", + args: args{ + headerName: "X-blab", + headerRef: newHeaderRef(&openapi3.Schema{Nullable: false}, true), + }, + isHeaderPresent: true, + headerVals: []string{"blab"}, + wantErr: true, + wantErrMsg: `response header "X-blab" doesn't match schema: Value is not nullable`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := newInputDefault() + opts := []openapi3.SchemaValidationOption(nil) + if tt.isHeaderPresent { + input.Header = map[string][]string{http.CanonicalHeaderKey(tt.args.headerName): tt.headerVals} + } + + err := validateResponseHeader(tt.args.headerName, tt.args.headerRef, input, opts) + if tt.wantErr { + require.NotEmpty(t, tt.wantErrMsg, "wanted error message is not populated") + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErrMsg) + } else { + require.NoError(t, err) + } + }) + } +} + +func newInputDefault() *ResponseValidationInput { + return &ResponseValidationInput{ + RequestValidationInput: &RequestValidationInput{ + Request: nil, + PathParams: nil, + Route: nil, + }, + Status: 200, + Header: nil, + Body: io.NopCloser(strings.NewReader(`{}`)), + } +} + +func newHeaderRef(schema *openapi3.Schema, required bool) *openapi3.HeaderRef { + return &openapi3.HeaderRef{Value: &openapi3.Header{Parameter: openapi3.Parameter{Schema: &openapi3.SchemaRef{Value: schema}, Required: required}}} +} + +func newArraySchema(schema *openapi3.Schema) *openapi3.Schema { + arraySchema := openapi3.NewArraySchema() + arraySchema.Items = openapi3.NewSchemaRef("", schema) + + return arraySchema +} From de2455e2bc52e7c2d1cbd7e5ef96f30f1371a5ce Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 19 Dec 2022 15:11:06 +0100 Subject: [PATCH 231/260] openapi3: unexport ValidationOptions fields and add some more (#717) --- README.md | 10 ++++ cmd/validate/main.go | 102 +++++++++++++++++++++++++++++++++ openapi3/media_type.go | 2 +- openapi3/parameter.go | 2 +- openapi3/request_body.go | 2 +- openapi3/response.go | 2 +- openapi3/schema.go | 12 ++-- openapi3/validation_options.go | 35 ++++++++--- 8 files changed, 148 insertions(+), 19 deletions(-) create mode 100644 cmd/validate/main.go diff --git a/README.md b/README.md index c2226498c..3596289db 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ Be sure to check [OpenAPI Initiative](https://github.com/OAI)'s [great tooling l * Generates `*openapi3.Schema` values for Go types. # Some recipes +## Validating an OpenAPI document +```shell +go run github.com/getkin/kin-openapi/cmd/validate@latest [--defaults] [--examples] [--ext] [--patterns] -- +``` + ## Loading OpenAPI document Use `openapi3.Loader`, which resolves all references: ```go @@ -196,6 +201,11 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.112.0 +* `(openapi3.ValidationOptions).ExamplesValidationDisabled` has been unexported. +* `(openapi3.ValidationOptions).SchemaFormatValidationEnabled` has been unexported. +* `(openapi3.ValidationOptions).SchemaPatternValidationDisabled` has been unexported. + ### v0.111.0 * Changed `func (*_) Validate(ctx context.Context) error` to `func (*_) Validate(ctx context.Context, opts ...ValidationOption) error`. * `openapi3.WithValidationOptions(ctx context.Context, opts *ValidationOptions) context.Context` prototype changed to `openapi3.WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context`. diff --git a/cmd/validate/main.go b/cmd/validate/main.go new file mode 100644 index 000000000..9759564fa --- /dev/null +++ b/cmd/validate/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "flag" + "log" + "os" + "strings" + + "github.com/invopop/yaml" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/getkin/kin-openapi/openapi3" +) + +var ( + defaultDefaults = true + defaults = flag.Bool("defaults", defaultDefaults, "when false, disables schemas' default field validation") +) + +var ( + defaultExamples = true + examples = flag.Bool("examples", defaultExamples, "when false, disables all example schema validation") +) + +var ( + defaultExt = false + ext = flag.Bool("ext", defaultExt, "enables visiting other files") +) + +var ( + defaultPatterns = true + patterns = flag.Bool("patterns", defaultPatterns, "when false, allows schema patterns unsupported by the Go regexp engine") +) + +func main() { + flag.Parse() + filename := flag.Arg(0) + if len(flag.Args()) != 1 || filename == "" { + log.Fatalf("Usage: go run github.com/getkin/kin-openapi/cmd/validate@latest [--defaults] [--examples] [--ext] [--patterns] -- \nGot: %+v\n", os.Args) + } + + data, err := os.ReadFile(filename) + if err != nil { + log.Fatal(err) + } + + var vd struct { + OpenAPI string `json:"openapi" yaml:"openapi"` + Swagger string `json:"swagger" yaml:"swagger"` + } + if err := yaml.Unmarshal(data, &vd); err != nil { + log.Fatal(err) + } + + switch { + case vd.OpenAPI == "3" || strings.HasPrefix(vd.OpenAPI, "3."): + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = *ext + + doc, err := loader.LoadFromFile(filename) + if err != nil { + log.Fatal(err) + } + + var opts []openapi3.ValidationOption + if !*defaults { + opts = append(opts, openapi3.DisableSchemaDefaultsValidation()) + } + if !*examples { + opts = append(opts, openapi3.DisableExamplesValidation()) + } + if !*patterns { + opts = append(opts, openapi3.DisableSchemaPatternValidation()) + } + + if err = doc.Validate(loader.Context, opts...); err != nil { + log.Fatal(err) + } + + case vd.Swagger == "2" || strings.HasPrefix(vd.Swagger, "2."): + if *defaults != defaultDefaults { + log.Fatal("Flag --defaults is only for OpenAPIv3") + } + if *examples != defaultExamples { + log.Fatal("Flag --examples is only for OpenAPIv3") + } + if *ext != defaultExt { + log.Fatal("Flag --ext is only for OpenAPIv3") + } + if *patterns != defaultPatterns { + log.Fatal("Flag --patterns is only for OpenAPIv3") + } + + var doc openapi2.T + if err := yaml.Unmarshal(data, &doc); err != nil { + log.Fatal(err) + } + + default: + log.Fatal("Missing or incorrect 'openapi' or 'swagger' field") + } +} diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 8e3fef7e6..090be7657 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -90,7 +90,7 @@ func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOpti return errors.New("example and examples are mutually exclusive") } - if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); vo.examplesValidationDisabled { return nil } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index c55af474d..04e13b203 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -323,7 +323,7 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name) } - if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); vo.examplesValidationDisabled { return nil } if example := parameter.Example; example != nil { diff --git a/openapi3/request_body.go b/openapi3/request_body.go index baf7f81e8..f0d9e1ec2 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -112,7 +112,7 @@ func (requestBody *RequestBody) Validate(ctx context.Context, opts ...Validation return errors.New("content of the request body is required") } - if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled { vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false } diff --git a/openapi3/response.go b/openapi3/response.go index eaf8e57f8..324f77ddc 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -119,7 +119,7 @@ func (response *Response) Validate(ctx context.Context, opts ...ValidationOption if response.Description == nil { return errors.New("a short description of the response is required") } - if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled { vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true } diff --git a/openapi3/schema.go b/openapi3/schema.go index bbce46c7b..f2b4ea2c8 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -678,7 +678,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "float", "double": default: - if validationOpts.SchemaFormatValidationEnabled { + if validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -688,7 +688,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "int32", "int64": default: - if validationOpts.SchemaFormatValidationEnabled { + if validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -710,12 +710,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && validationOpts.SchemaFormatValidationEnabled { + if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } } - if schema.Pattern != "" && !validationOpts.SchemaPatternValidationDisabled { + if schema.Pattern != "" && !validationOpts.schemaPatternValidationDisabled { if err = schema.compilePattern(); err != nil { return err } @@ -771,13 +771,13 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } - if v := schema.Default; v != nil { + if v := schema.Default; v != nil && !validationOpts.schemaDefaultsValidationDisabled { if err := schema.VisitJSON(v); err != nil { return fmt.Errorf("invalid default: %w", err) } } - if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled { + if x := schema.Example; x != nil && !validationOpts.examplesValidationDisabled { if err := validateExampleValue(ctx, x, schema); err != nil { return fmt.Errorf("invalid example: %w", err) } diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index a74364dae..343b6836e 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -7,10 +7,11 @@ type ValidationOption func(options *ValidationOptions) // ValidationOptions provides configuration for validating OpenAPI documents. type ValidationOptions struct { - SchemaFormatValidationEnabled bool - SchemaPatternValidationDisabled bool - ExamplesValidationDisabled bool examplesValidationAsReq, examplesValidationAsRes bool + examplesValidationDisabled bool + schemaDefaultsValidationDisabled bool + schemaFormatValidationEnabled bool + schemaPatternValidationDisabled bool } type validationOptionsKey struct{} @@ -19,7 +20,7 @@ type validationOptionsKey struct{} // By default, schema format validation is disabled. func EnableSchemaFormatValidation() ValidationOption { return func(options *ValidationOptions) { - options.SchemaFormatValidationEnabled = true + options.schemaFormatValidationEnabled = true } } @@ -27,7 +28,7 @@ func EnableSchemaFormatValidation() ValidationOption { // By default, schema format validation is disabled. func DisableSchemaFormatValidation() ValidationOption { return func(options *ValidationOptions) { - options.SchemaFormatValidationEnabled = false + options.schemaFormatValidationEnabled = false } } @@ -35,14 +36,30 @@ func DisableSchemaFormatValidation() ValidationOption { // By default, schema pattern validation is enabled. func EnableSchemaPatternValidation() ValidationOption { return func(options *ValidationOptions) { - options.SchemaPatternValidationDisabled = false + options.schemaPatternValidationDisabled = false } } // DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. func DisableSchemaPatternValidation() ValidationOption { return func(options *ValidationOptions) { - options.SchemaPatternValidationDisabled = true + options.schemaPatternValidationDisabled = true + } +} + +// EnableSchemaDefaultsValidation does the opposite of DisableSchemaDefaultsValidation. +// By default, schema default values are validated against their schema. +func EnableSchemaDefaultsValidation() ValidationOption { + return func(options *ValidationOptions) { + options.schemaDefaultsValidationDisabled = false + } +} + +// DisableSchemaDefaultsValidation disables schemas' default field validation. +// By default, schema default values are validated against their schema. +func DisableSchemaDefaultsValidation() ValidationOption { + return func(options *ValidationOptions) { + options.schemaDefaultsValidationDisabled = true } } @@ -50,7 +67,7 @@ func DisableSchemaPatternValidation() ValidationOption { // By default, all schema examples are validated. func EnableExamplesValidation() ValidationOption { return func(options *ValidationOptions) { - options.ExamplesValidationDisabled = false + options.examplesValidationDisabled = false } } @@ -58,7 +75,7 @@ func EnableExamplesValidation() ValidationOption { // By default, all schema examples are validated. func DisableExamplesValidation() ValidationOption { return func(options *ValidationOptions) { - options.ExamplesValidationDisabled = true + options.examplesValidationDisabled = true } } From 1490eae89ee4a7f5d88be0d3abc76bd469fd7877 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 19 Dec 2022 15:48:40 +0100 Subject: [PATCH 232/260] openapi3: introduce (Paths).InMatchingOrder() paths iterator (#719) --- .github/workflows/go.yml | 2 +- openapi2/openapi2.go | 10 ++++------ openapi2conv/openapi2_conv.go | 10 ++++------ openapi3/internalize_refs.go | 5 +---- openapi3/loader.go | 18 +++++------------- openapi3/openapi3.go | 10 ++++------ openapi3/paths.go | 35 +++++++++++++++++++++++++++++++++-- routers/gorillamux/router.go | 27 +-------------------------- 8 files changed, 53 insertions(+), 64 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 809476343..e1648fcc7 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -105,7 +105,7 @@ jobs: - if: runner.os == 'Linux' name: Missing specification object link to definition run: | - [[ 30 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]] + [[ 31 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]] - if: runner.os == 'Linux' name: Style around ExtensionProps embedding diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 430e39851..4927ade86 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -40,15 +40,13 @@ func (doc *T) UnmarshalJSON(data []byte) error { } func (doc *T) AddOperation(path string, method string, operation *Operation) { - paths := doc.Paths - if paths == nil { - paths = make(map[string]*PathItem) - doc.Paths = paths + if doc.Paths == nil { + doc.Paths = make(map[string]*PathItem) } - pathItem := paths[path] + pathItem := doc.Paths[path] if pathItem == nil { pathItem = &PathItem{} - paths[path] = pathItem + doc.Paths[path] = pathItem } pathItem.SetOperation(method, operation) } diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index b81e3c2c4..989b685d5 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -1204,15 +1204,13 @@ func stripNonCustomExtensions(extensions map[string]interface{}) { } func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.ExtensionProps) { - paths := doc2.Paths - if paths == nil { - paths = make(map[string]*openapi2.PathItem) - doc2.Paths = paths + if doc2.Paths == nil { + doc2.Paths = make(map[string]*openapi2.PathItem) } - pathItem := paths[path] + pathItem := doc2.Paths[path] if pathItem == nil { pathItem = &openapi2.PathItem{} - paths[path] = pathItem + doc2.Paths[path] = pathItem } pathItem.ExtensionProps = extensionProps } diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index 6ff6b8578..dcec43b40 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -193,14 +193,11 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver, return false } name := refNameResolver(c.Ref) - if _, ok := doc.Components.Callbacks[name]; ok { - c.Ref = "#/components/callbacks/" + name - } if doc.Components.Callbacks == nil { doc.Components.Callbacks = make(Callbacks) } - doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value} c.Ref = "#/components/callbacks/" + name + doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value} return true } diff --git a/openapi3/loader.go b/openapi3/loader.go index 4cc83c0b2..ecc2ef256 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -281,12 +281,7 @@ func isSingleRefElement(ref string) bool { return !strings.Contains(ref, "#") } -func (loader *Loader) resolveComponent( - doc *T, - ref string, - path *url.URL, - resolved interface{}, -) ( +func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolved interface{}) ( componentDoc *T, componentPath *url.URL, err error, @@ -928,11 +923,10 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen } id := unescapeRefString(rest) - definitions := doc.Components.Callbacks - if definitions == nil { + if doc.Components.Callbacks == nil { return failedToResolveRefFragmentPart(ref, "callbacks") } - resolved := definitions[id] + resolved := doc.Components.Callbacks[id] if resolved == nil { return failedToResolveRefFragmentPart(ref, id) } @@ -1022,15 +1016,13 @@ func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *Pa } id := unescapeRefString(rest) - definitions := doc.Paths - if definitions == nil { + if doc.Paths == nil { return failedToResolveRefFragmentPart(ref, "paths") } - resolved := definitions[id] + resolved := doc.Paths[id] if resolved == nil { return failedToResolveRefFragmentPart(ref, id) } - *pathItem = *resolved } } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index d9506a9cd..6622ef030 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -36,15 +36,13 @@ func (doc *T) UnmarshalJSON(data []byte) error { } func (doc *T) AddOperation(path string, method string, operation *Operation) { - paths := doc.Paths - if paths == nil { - paths = make(Paths) - doc.Paths = paths + if doc.Paths == nil { + doc.Paths = make(Paths) } - pathItem := paths[path] + pathItem := doc.Paths[path] if pathItem == nil { pathItem = &PathItem{} - paths[path] = pathItem + doc.Paths[path] = pathItem } pathItem.SetOperation(method, operation) } diff --git a/openapi3/paths.go b/openapi3/paths.go index 2af59f2ca..0986b0557 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -29,8 +29,8 @@ func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error } if pathItem == nil { - paths[path] = &PathItem{} - pathItem = paths[path] + pathItem = &PathItem{} + paths[path] = pathItem } normalizedPath, _, varsInPath := normalizeTemplatedPath(path) @@ -109,6 +109,37 @@ func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error return nil } +// 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 { + // NOTE: sorting by number of variables ASC then by descending lexicographical + // order seems to be a good heuristic. + if paths == nil { + return nil + } + + vars := make(map[int][]string) + max := 0 + for path := range paths { + count := strings.Count(path, "}") + vars[count] = append(vars[count], path) + if count > max { + max = count + } + } + + ordered := make([]string, 0, len(paths)) + for c := 0; c <= max; c++ { + if ps, ok := vars[c]; ok { + sort.Sort(sort.Reverse(sort.StringSlice(ps))) + ordered = append(ordered, ps...) + } + } + return ordered +} + // Find returns a path that matches the key. // // The method ignores differences in template variable names (except possible "*" suffix). diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 6977808ed..bbf81cea8 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -56,7 +56,7 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) { muxRouter := mux.NewRouter().UseEncodedPath() r := &Router{} - for _, path := range orderedPaths(doc.Paths) { + for _, path := range doc.Paths.InMatchingOrder() { pathItem := doc.Paths[path] if len(pathItem.Servers) > 0 { if servers, err = makeServers(pathItem.Servers); err != nil { @@ -203,31 +203,6 @@ func newSrv(serverURL string, server *openapi3.Server, varsUpdater varsf) (srv, return svr, nil } -func orderedPaths(paths map[string]*openapi3.PathItem) []string { - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathsObject - // When matching URLs, concrete (non-templated) paths would be matched - // before their templated counterparts. - // NOTE: sorting by number of variables ASC then by descending lexicographical - // order seems to be a good heuristic. - vars := make(map[int][]string) - max := 0 - for path := range paths { - count := strings.Count(path, "}") - vars[count] = append(vars[count], path) - if count > max { - max = count - } - } - ordered := make([]string, 0, len(paths)) - for c := 0; c <= max; c++ { - if ps, ok := vars[c]; ok { - sort.Sort(sort.Reverse(sort.StringSlice(ps))) - ordered = append(ordered, ps...) - } - } - return ordered -} - // Magic strings that temporarily replace "{}" so net/url.Parse() works var blURL, brURL = strings.Repeat("-", 50), strings.Repeat("_", 50) From 1f680b5280f02cba555391057e93bba78e221fd2 Mon Sep 17 00:00:00 2001 From: Greg Ward Date: Mon, 19 Dec 2022 11:18:52 -0500 Subject: [PATCH 233/260] feat: improve error reporting for bad/missing discriminator (#718) --- openapi3/schema.go | 25 ++++++++++++++++++++++--- openapi3/schema_oneOf_test.go | 8 ++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index f2b4ea2c8..20c1fa550 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -922,16 +922,35 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if valuemap, okcheck := value.(map[string]interface{}); okcheck { discriminatorVal, okcheck := valuemap[pn] if !okcheck { - return errors.New("input does not contain the discriminator property") + return &SchemaError{ + Schema: schema, + SchemaField: "discriminator", + Reason: fmt.Sprintf("input does not contain the discriminator property %q", pn), + } } discriminatorValString, okcheck := discriminatorVal.(string) if !okcheck { - return errors.New("descriminator value is not a string") + valStr := "null" + if discriminatorVal != nil { + valStr = fmt.Sprintf("%v", discriminatorVal) + } + + return &SchemaError{ + Value: discriminatorVal, + Schema: schema, + SchemaField: "discriminator", + Reason: fmt.Sprintf("value of discriminator property %q is not a string: %v", pn, valStr), + } } if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck { - return errors.New("input does not contain a valid discriminator value") + return &SchemaError{ + Value: discriminatorVal, + Schema: schema, + SchemaField: "discriminator", + Reason: fmt.Sprintf("discriminator property %q has invalid value: %q", pn, discriminatorVal), + } } } } diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go index d3e689d51..1a8ea8138 100644 --- a/openapi3/schema_oneOf_test.go +++ b/openapi3/schema_oneOf_test.go @@ -86,7 +86,7 @@ func TestVisitJSON_OneOf_MissingDiscriptorProperty(t *testing.T) { err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ "name": "snoopy", }) - require.EqualError(t, err, "input does not contain the discriminator property") + require.ErrorContains(t, err, "input does not contain the discriminator property \"$type\"\n") } func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) { @@ -96,7 +96,7 @@ func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) { "name": "snoopy", "$type": "snake", }) - require.EqualError(t, err, "input does not contain a valid discriminator value") + require.ErrorContains(t, err, "discriminator property \"$type\" has invalid value: \"snake\"") } func TestVisitJSON_OneOf_MissingField(t *testing.T) { @@ -126,14 +126,14 @@ func TestVisitJSON_OneOf_BadDescriminatorType(t *testing.T) { "scratches": true, "$type": 1, }) - require.EqualError(t, err, "descriminator value is not a string") + require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string: 1") err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ "name": "snoopy", "barks": true, "$type": nil, }) - require.EqualError(t, err, "descriminator value is not a string") + require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string: null") } func TestVisitJSON_OneOf_Path(t *testing.T) { From a0b67a057e3bc7db365513de7f4b84d9eb0bc1d6 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 19 Dec 2022 18:16:56 +0100 Subject: [PATCH 234/260] openapi3: continue validation on valid oneOf properties (#721) --- openapi3/schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 20c1fa550..d795fb530 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -634,7 +634,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(ctx, stack); err == nil { + if err = v.validate(ctx, stack); err != nil { return } } From 46e0df8ae84cbb51b7c672a0bb8ebb6995d28c7a Mon Sep 17 00:00:00 2001 From: orensolo <46680749+orensolo@users.noreply.github.com> Date: Tue, 20 Dec 2022 11:29:35 +0200 Subject: [PATCH 235/260] openapi3filter: use option to skip setting defaults on validation (#708) --- openapi3filter/issue707_test.go | 91 ++++++++++++++++++++++++++++++ openapi3filter/validate_request.go | 2 +- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 openapi3filter/issue707_test.go diff --git a/openapi3filter/issue707_test.go b/openapi3filter/issue707_test.go new file mode 100644 index 000000000..c0dbe6462 --- /dev/null +++ b/openapi3filter/issue707_test.go @@ -0,0 +1,91 @@ +package openapi3filter + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue707(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + spec := ` + openapi: 3.0.0 + info: + version: 1.0.0 + title: Sample API + paths: + /items: + get: + description: Returns a list of stuff + parameters: + - description: parameter with a default value + explode: true + in: query + name: param-with-default + schema: + default: 124 + type: integer + style: form + required: false + responses: + '200': + description: Successful response +`[1:] + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + tests := []struct { + name string + options *Options + expectedQuery string + }{ + { + name: "no defaults are added to requests parameters", + options: &Options{ + SkipSettingDefaults: true, + }, + expectedQuery: "", + }, + + { + name: "defaults are added to requests", + expectedQuery: "param-with-default=124", + }, + } + + for _, testcase := range tests { + t.Run(testcase.name, func(t *testing.T) { + httpReq, err := http.NewRequest(http.MethodGet, "/items", strings.NewReader("")) + require.NoError(t, err) + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + requestValidationInput := &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + Options: testcase.options, + } + err = ValidateRequest(ctx, requestValidationInput) + require.NoError(t, err) + + require.NoError(t, err) + require.Equal(t, testcase.expectedQuery, + httpReq.URL.RawQuery, "default value must not be included") + }) + } +} diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 2424eb9ed..a61c57a09 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -137,7 +137,7 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param } // Set default value if needed - if value == nil && schema != nil && schema.Default != nil { + if !options.SkipSettingDefaults && value == nil && schema != nil && schema.Default != nil { value = schema.Default req := input.Request switch parameter.In { From 92d47ad2141d9618ac66847c0618b03a3e44763a Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Fri, 30 Dec 2022 14:33:23 +0100 Subject: [PATCH 236/260] openapi3: remove email string format (#727) --- README.md | 3 +++ openapi3/schema_formats.go | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3596289db..850b11b8a 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,9 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.113.0 +* The string format `email` has been removed by default. To use it please call `openapi3.DefineStringFormat("email", openapi3.FormatOfStringForEmail)`. + ### v0.112.0 * `(openapi3.ValidationOptions).ExamplesValidationDisabled` has been unexported. * `(openapi3.ValidationOptions).SchemaFormatValidationEnabled` has been unexported. diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 51e245411..ecbc0ebfa 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -10,6 +10,10 @@ import ( const ( // FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122 FormatOfStringForUUIDOfRFC4122 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$` + + // FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses. + // Use DefineStringFormat(...) if you need something stricter. + FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$` ) // FormatCallback performs custom checks on exotic formats @@ -79,10 +83,6 @@ func validateIPv6(ip string) error { } func init() { - // This pattern catches only some suspiciously wrong-looking email addresses. - // Use DefineStringFormat(...) if you need something stricter. - DefineStringFormat("email", `^[^@]+@[^@<>",\s]+$`) - // Base64 // The pattern supports base64 and b./ase64url. Padding ('=') is supported. DefineStringFormat("byte", `(^$|^[a-zA-Z0-9+/\-_]*=*$)`) From 19556cfcc22281022f2a8fa4f5c6ed3290f8025f Mon Sep 17 00:00:00 2001 From: Katsumi Kato Date: Tue, 3 Jan 2023 08:50:57 +0900 Subject: [PATCH 237/260] openapi3filter: support for allOf request schema in multipart/form-data (#729) fix https://github.com/getkin/kin-openapi/issues/722 --- openapi3filter/issue722_test.go | 133 +++++++++++++++++++++++++++++ openapi3filter/req_resp_decoder.go | 78 +++++++++++------ 2 files changed, 186 insertions(+), 25 deletions(-) create mode 100644 openapi3filter/issue722_test.go diff --git a/openapi3filter/issue722_test.go b/openapi3filter/issue722_test.go new file mode 100644 index 000000000..2ffa9d143 --- /dev/null +++ b/openapi3filter/issue722_test.go @@ -0,0 +1,133 @@ +package openapi3filter_test + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "strings" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestValidateMultipartFormDataContainingAllOf(t *testing.T) { + const spec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: 0.0.1 +paths: + /test: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + allOf: + - $ref: '#/components/schemas/Category' + - properties: + file: + type: string + format: binary + description: + type: string + responses: + '200': + description: Created + +components: + schemas: + Category: + type: object + properties: + name: + type: string + required: + - name +` + + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + if err != nil { + t.Fatal(err) + } + if err = doc.Validate(loader.Context); err != nil { + t.Fatal(err) + } + + router, err := gorillamux.NewRouter(doc) + if err != nil { + t.Fatal(err) + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + { // Add file data + fw, err := writer.CreateFormFile("file", "hello.txt") + if err != nil { + t.Fatal(err) + } + if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil { + t.Fatal(err) + } + } + + { // Add a single "name" item as part data + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="name"`) + fw, err := writer.CreatePart(h) + if err != nil { + t.Fatal(err) + } + if _, err = io.Copy(fw, strings.NewReader(`foo`)); err != nil { + t.Fatal(err) + } + } + + { // Add a single "discription" item as part data + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="description"`) + fw, err := writer.CreatePart(h) + if err != nil { + t.Fatal(err) + } + if _, err = io.Copy(fw, strings.NewReader(`description note`)); err != nil { + t.Fatal(err) + } + } + + writer.Close() + + req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes())) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + route, pathParams, err := router.FindRoute(req) + if err != nil { + t.Fatal(err) + } + + if err = openapi3filter.ValidateRequestBody( + context.Background(), + &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + }, + route.Operation.RequestBody.Value, + ); err != nil { + t.Error(err) + } +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 4791b4ad4..2cd700cd1 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1120,33 +1120,47 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S enc = encFn(name) } subEncFn := func(string) *openapi3.Encoding { return enc } - // If the property's schema has type "array" it is means that the form contains a few parts with the same name. - // Every such part has a type that is defined by an items schema in the property's schema. + var valueSchema *openapi3.SchemaRef - var exists bool - valueSchema, exists = schema.Value.Properties[name] - if !exists { - anyProperties := schema.Value.AdditionalPropertiesAllowed - if anyProperties != nil { - switch *anyProperties { - case true: - //additionalProperties: true - continue - default: - //additionalProperties: false - return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + if len(schema.Value.AllOf) > 0 { + var exists bool + for _, sr := range schema.Value.AllOf { + valueSchema, exists = sr.Value.Properties[name] + if exists { + break } } - if schema.Value.AdditionalProperties == nil { + if !exists { return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } - valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name] + } else { + // If the property's schema has type "array" it is means that the form contains a few parts with the same name. + // Every such part has a type that is defined by an items schema in the property's schema. + var exists bool + valueSchema, exists = schema.Value.Properties[name] if !exists { - return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + anyProperties := schema.Value.AdditionalPropertiesAllowed + if anyProperties != nil { + switch *anyProperties { + case true: + //additionalProperties: true + continue + default: + //additionalProperties: false + return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + } + } + if schema.Value.AdditionalProperties == nil { + return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + } + valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name] + if !exists { + return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + } + } + if valueSchema.Value.Type == "array" { + valueSchema = valueSchema.Value.Items } - } - if valueSchema.Value.Type == "array" { - valueSchema = valueSchema.Value.Items } var value interface{} @@ -1160,14 +1174,28 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } allTheProperties := make(map[string]*openapi3.SchemaRef) - for k, v := range schema.Value.Properties { - allTheProperties[k] = v - } - if schema.Value.AdditionalProperties != nil { - for k, v := range schema.Value.AdditionalProperties.Value.Properties { + if len(schema.Value.AllOf) > 0 { + for _, sr := range schema.Value.AllOf { + for k, v := range sr.Value.Properties { + allTheProperties[k] = v + } + if sr.Value.AdditionalProperties != nil { + for k, v := range sr.Value.AdditionalProperties.Value.Properties { + allTheProperties[k] = v + } + } + } + } else { + for k, v := range schema.Value.Properties { allTheProperties[k] = v } + if schema.Value.AdditionalProperties != nil { + for k, v := range schema.Value.AdditionalProperties.Value.Properties { + allTheProperties[k] = v + } + } } + // Make an object value from form values. obj := make(map[string]interface{}) for name, prop := range allTheProperties { From 2ed340d5c6b86a83551a2e6fa24cea6fb449a2c1 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 3 Jan 2023 18:00:58 +0100 Subject: [PATCH 238/260] Disallow unexpected fields in validation and drop `jsoninfo` package (#728) Fixes https://github.com/getkin/kin-openapi/issues/513 Fixes https://github.com/getkin/kin-openapi/issues/37 --- .github/workflows/go.yml | 21 +- .github/workflows/shellcheck.yml | 18 + README.md | 3 + go.mod | 1 + go.sum | 13 +- jsoninfo/doc.go | 2 - jsoninfo/marshal.go | 162 ----- jsoninfo/marshal_ref.go | 30 - jsoninfo/marshal_test.go | 190 ------ jsoninfo/strict_struct.go | 6 - jsoninfo/type_info.go | 68 -- jsoninfo/unmarshal.go | 121 ---- jsoninfo/unmarshal_test.go | 156 ----- jsoninfo/unsupported_properties_error.go | 42 -- openapi2/header.go | 15 + openapi2/openapi2.go | 313 +++------ openapi2/operation.go | 91 +++ openapi2/parameter.go | 176 +++++ openapi2/path_item.go | 150 +++++ openapi2/response.go | 60 ++ openapi2/security_scheme.go | 87 +++ openapi2conv/openapi2_conv.go | 272 ++++---- openapi3/components.go | 60 +- openapi3/discriminator.go | 34 +- openapi3/encoding.go | 44 +- openapi3/example.go | 46 +- openapi3/example_validation_test.go | 5 +- openapi3/extension.go | 40 +- openapi3/extension_test.go | 125 ---- openapi3/external_docs.go | 35 +- openapi3/header.go | 54 +- openapi3/info.go | 109 +++- openapi3/internalize_refs.go | 84 +-- openapi3/issue341_test.go | 2 +- openapi3/issue376_test.go | 143 +++-- openapi3/issue513_test.go | 173 +++++ openapi3/link.go | 53 +- ...oad_cicular_ref_with_external_file_test.go | 4 +- openapi3/loader.go | 106 ++- openapi3/loader_paths_test.go | 1 - openapi3/loader_test.go | 11 +- openapi3/media_type.go | 80 ++- openapi3/openapi3.go | 58 +- openapi3/openapi3_test.go | 16 +- openapi3/operation.go | 72 ++- openapi3/parameter.go | 82 ++- openapi3/path_item.go | 82 ++- openapi3/ref.go | 7 + openapi3/refs.go | 603 +++++++++++++----- openapi3/request_body.go | 40 +- openapi3/response.go | 41 +- openapi3/schema.go | 342 ++++++++-- openapi3/schema_test.go | 8 +- openapi3/security_requirements.go | 2 +- openapi3/security_scheme.go | 137 +++- openapi3/server.go | 73 ++- openapi3/tag.go | 37 +- openapi3/validation_options.go | 13 + openapi3/xml.go | 46 +- openapi3filter/issue707_test.go | 39 +- openapi3filter/req_resp_decoder.go | 22 +- openapi3filter/validate_request.go | 9 +- openapi3filter/validation_test.go | 6 +- {jsoninfo => openapi3gen}/field_info.go | 22 +- openapi3gen/openapi3gen.go | 15 +- openapi3gen/openapi3gen_test.go | 2 +- openapi3gen/type_info.go | 54 ++ refs.sh | 125 ++++ routers/gorillamux/router_test.go | 14 +- 69 files changed, 3174 insertions(+), 1999 deletions(-) create mode 100644 .github/workflows/shellcheck.yml delete mode 100644 jsoninfo/doc.go delete mode 100644 jsoninfo/marshal.go delete mode 100644 jsoninfo/marshal_ref.go delete mode 100644 jsoninfo/marshal_test.go delete mode 100644 jsoninfo/strict_struct.go delete mode 100644 jsoninfo/type_info.go delete mode 100644 jsoninfo/unmarshal.go delete mode 100644 jsoninfo/unmarshal_test.go delete mode 100644 jsoninfo/unsupported_properties_error.go create mode 100644 openapi2/header.go create mode 100644 openapi2/operation.go create mode 100644 openapi2/parameter.go create mode 100644 openapi2/path_item.go create mode 100644 openapi2/response.go create mode 100644 openapi2/security_scheme.go delete mode 100644 openapi3/extension_test.go create mode 100644 openapi3/issue513_test.go create mode 100644 openapi3/ref.go rename {jsoninfo => openapi3gen}/field_info.go (78%) create mode 100644 openapi3gen/type_info.go create mode 100755 refs.sh diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e1648fcc7..dab35cb89 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -52,6 +52,11 @@ jobs: - uses: actions/checkout@v2 + - name: Check codegen + run: | + ./refs.sh | tee openapi3/refs.go + git --no-pager diff --exit-code + - run: go mod download && go mod tidy && go mod verify - run: git --no-pager diff --exit-code @@ -102,22 +107,32 @@ jobs: run: | [[ "$(git grep -F yaml. -- openapi3/ | grep -v _test.go | wc -l)" = 1 ]] + - if: runner.os == 'Linux' + name: Ensure non-pointer MarshalJSON + run: | + ! git grep -InE 'func.+[*].+[)].MarshalJSON[(][)]' + - if: runner.os == 'Linux' name: Missing specification object link to definition run: | [[ 31 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]] - if: runner.os == 'Linux' - name: Style around ExtensionProps embedding + name: Missing validation of unknown fields in extensions + run: | + [[ $(git grep -InF 'return validateExtensions' -- openapi3 | wc -l) -eq $(git grep -InE '^\s+Extensions.+`' -- openapi3 | wc -l) ]] + + - if: runner.os == 'Linux' + name: Style around Extensions embedding run: | - ! ag -B2 -A2 'type.[A-Z].+struct..\n.+ExtensionProps\n[^\n]' openapi3/*.go + ! ag -B2 -A2 'type.[A-Z].+struct..\n.+Extensions\n[^\n]' openapi3/*.go - if: runner.os == 'Linux' name: Ensure all exported fields are mentioned in Validate() impls run: | for ty in $TYPES; do # Ensure definition - if ! ag 'type.[A-Z].+struct..\n.+ExtensionProps' openapi3/*.go | grep -F "type $ty struct"; then + if ! ag 'type.[A-Z].+struct..\n.+Extensions' openapi3/*.go | grep -F "type $ty struct"; then echo "OAI type $ty is not defined" && exit 1 fi diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 000000000..e1f8d1242 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,18 @@ +name: ShellCheck + +on: + push: + pull_request: + +jobs: + shellcheck: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Run shellcheck + uses: ludeeus/action-shellcheck@1.1.0 + with: + check_together: 'yes' + severity: error diff --git a/README.md b/README.md index 850b11b8a..27d6699cf 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,9 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ### v0.113.0 * The string format `email` has been removed by default. To use it please call `openapi3.DefineStringFormat("email", openapi3.FormatOfStringForEmail)`. +* Field `openapi3.T.Components` is now a pointer. +* Fields `openapi3.Schema.AdditionalProperties` and `openapi3.Schema.AdditionalPropertiesAllowed` are replaced by `openapi3.Schema.AdditionalProperties.Schema` and `openapi3.Schema.AdditionalProperties.Has` respectively. +* Type `openapi3.ExtensionProps` is now just `map[string]interface{}` and extensions are accessible through the `Extensions` field. ### v0.112.0 * `(openapi3.ValidationOptions).ExamplesValidationDisabled` has been unexported. diff --git a/go.mod b/go.mod index 942b3195c..12a2f1af7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/invopop/yaml v0.1.0 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 + github.com/perimeterx/marshmallow v1.1.4 github.com/stretchr/testify v1.8.1 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 4982bc738..4d05787e4 100644 --- a/go.sum +++ b/go.sum @@ -5,20 +5,27 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= +github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -29,6 +36,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/jsoninfo/doc.go b/jsoninfo/doc.go deleted file mode 100644 index e59ec2c34..000000000 --- a/jsoninfo/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package jsoninfo provides information and functions for marshalling/unmarshalling JSON. -package jsoninfo diff --git a/jsoninfo/marshal.go b/jsoninfo/marshal.go deleted file mode 100644 index f2abf7c00..000000000 --- a/jsoninfo/marshal.go +++ /dev/null @@ -1,162 +0,0 @@ -package jsoninfo - -import ( - "encoding/json" - "fmt" - "reflect" -) - -// MarshalStrictStruct function: -// - Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag. -// - Correctly handles StrictStruct semantics. -func MarshalStrictStruct(value StrictStruct) ([]byte, error) { - encoder := NewObjectEncoder() - if err := value.EncodeWith(encoder, value); err != nil { - return nil, err - } - return encoder.Bytes() -} - -type ObjectEncoder struct { - result map[string]json.RawMessage -} - -func NewObjectEncoder() *ObjectEncoder { - return &ObjectEncoder{ - result: make(map[string]json.RawMessage), - } -} - -// Bytes returns the result of encoding. -func (encoder *ObjectEncoder) Bytes() ([]byte, error) { - return json.Marshal(encoder.result) -} - -// EncodeExtension adds a key/value to the current JSON object. -func (encoder *ObjectEncoder) EncodeExtension(key string, value interface{}) error { - data, err := json.Marshal(value) - if err != nil { - return err - } - encoder.result[key] = data - return nil -} - -// EncodeExtensionMap adds all properties to the result. -func (encoder *ObjectEncoder) EncodeExtensionMap(value map[string]json.RawMessage) error { - if value != nil { - result := encoder.result - for k, v := range value { - result[k] = v - } - } - return nil -} - -func (encoder *ObjectEncoder) EncodeStructFieldsAndExtensions(value interface{}) error { - reflection := reflect.ValueOf(value) - - // Follow "encoding/json" semantics - if reflection.Kind() != reflect.Ptr { - // Panic because this is a clear programming error - panic(fmt.Errorf("value %s is not a pointer", reflection.Type().String())) - } - if reflection.IsNil() { - // Panic because this is a clear programming error - panic(fmt.Errorf("value %s is nil", reflection.Type().String())) - } - - // Take the element - reflection = reflection.Elem() - - // Obtain typeInfo - typeInfo := GetTypeInfo(reflection.Type()) - - // Declare result - result := encoder.result - - // Supported fields -iteration: - for _, field := range typeInfo.Fields { - // Fields without JSON tag are ignored - if !field.HasJSONTag { - continue - } - - // Marshal - fieldValue := reflection.FieldByIndex(field.Index) - if v, ok := fieldValue.Interface().(json.Marshaler); ok { - if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { - if field.JSONOmitEmpty { - continue iteration - } - result[field.JSONName] = []byte("null") - continue - } - fieldData, err := v.MarshalJSON() - if err != nil { - return err - } - result[field.JSONName] = fieldData - continue - } - switch fieldValue.Kind() { - case reflect.Ptr, reflect.Interface: - if fieldValue.IsNil() { - if field.JSONOmitEmpty { - continue iteration - } - result[field.JSONName] = []byte("null") - continue - } - case reflect.Struct: - case reflect.Map: - if field.JSONOmitEmpty && (fieldValue.IsNil() || fieldValue.Len() == 0) { - continue iteration - } - case reflect.Slice: - if field.JSONOmitEmpty && fieldValue.Len() == 0 { - continue iteration - } - case reflect.Bool: - x := fieldValue.Bool() - if field.JSONOmitEmpty && !x { - continue iteration - } - s := "false" - if x { - s = "true" - } - result[field.JSONName] = []byte(s) - continue iteration - case reflect.Int64, reflect.Int, reflect.Int32: - if field.JSONOmitEmpty && fieldValue.Int() == 0 { - continue iteration - } - case reflect.Uint64, reflect.Uint, reflect.Uint32: - if field.JSONOmitEmpty && fieldValue.Uint() == 0 { - continue iteration - } - case reflect.Float64: - if field.JSONOmitEmpty && fieldValue.Float() == 0.0 { - continue iteration - } - case reflect.String: - if field.JSONOmitEmpty && len(fieldValue.String()) == 0 { - continue iteration - } - default: - panic(fmt.Errorf("field %q has unsupported type %s", field.JSONName, field.Type.String())) - } - - // No special treament is needed - // Use plain old "encoding/json".Marshal - fieldData, err := json.Marshal(fieldValue.Addr().Interface()) - if err != nil { - return err - } - result[field.JSONName] = fieldData - } - - return nil -} diff --git a/jsoninfo/marshal_ref.go b/jsoninfo/marshal_ref.go deleted file mode 100644 index 29575e9e9..000000000 --- a/jsoninfo/marshal_ref.go +++ /dev/null @@ -1,30 +0,0 @@ -package jsoninfo - -import ( - "encoding/json" -) - -func MarshalRef(value string, otherwise interface{}) ([]byte, error) { - if value != "" { - return json.Marshal(&refProps{ - Ref: value, - }) - } - return json.Marshal(otherwise) -} - -func UnmarshalRef(data []byte, destRef *string, destOtherwise interface{}) error { - refProps := &refProps{} - if err := json.Unmarshal(data, refProps); err == nil { - ref := refProps.Ref - if ref != "" { - *destRef = ref - return nil - } - } - return json.Unmarshal(data, destOtherwise) -} - -type refProps struct { - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` -} diff --git a/jsoninfo/marshal_test.go b/jsoninfo/marshal_test.go deleted file mode 100644 index 10551542d..000000000 --- a/jsoninfo/marshal_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package jsoninfo_test - -import ( - "encoding/json" - "testing" - "time" - - "github.com/getkin/kin-openapi/jsoninfo" - "github.com/getkin/kin-openapi/openapi3" -) - -type Simple struct { - openapi3.ExtensionProps - Bool bool `json:"bool"` - Int int `json:"int"` - Int64 int64 `json:"int64"` - Float64 float64 `json:"float64"` - Time time.Time `json:"time"` - String string `json:"string"` - Bytes []byte `json:"bytes"` -} - -type SimpleOmitEmpty struct { - openapi3.ExtensionProps - Bool bool `json:"bool,omitempty"` - Int int `json:"int,omitempty"` - Int64 int64 `json:"int64,omitempty"` - Float64 float64 `json:"float64,omitempty"` - Time time.Time `json:"time,omitempty"` - String string `json:"string,omitempty"` - Bytes []byte `json:"bytes,omitempty"` -} - -type SimplePtrOmitEmpty struct { - openapi3.ExtensionProps - Bool *bool `json:"bool,omitempty"` - Int *int `json:"int,omitempty"` - Int64 *int64 `json:"int64,omitempty"` - Float64 *float64 `json:"float64,omitempty"` - Time *time.Time `json:"time,omitempty"` - String *string `json:"string,omitempty"` - Bytes *[]byte `json:"bytes,omitempty"` -} - -type OriginalNameType struct { - openapi3.ExtensionProps - Field string `json:",omitempty"` -} - -type RootType struct { - openapi3.ExtensionProps - EmbeddedType0 - EmbeddedType1 -} - -type EmbeddedType0 struct { - openapi3.ExtensionProps - Field0 string `json:"embedded0,omitempty"` -} - -type EmbeddedType1 struct { - openapi3.ExtensionProps - Field1 string `json:"embedded1,omitempty"` -} - -// Example describes expected outcome of: -// -// 1.Marshal JSON -// 2.Unmarshal value -// 3.Marshal value -type Example struct { - NoMarshal bool - NoUnmarshal bool - Value jsoninfo.StrictStruct - JSON interface{} -} - -var Examples = []Example{ - // Primitives - { - Value: &SimpleOmitEmpty{}, - JSON: Object{ - "time": time.Unix(0, 0), - }, - }, - { - Value: &SimpleOmitEmpty{}, - JSON: Object{ - "bool": true, - "int": 42, - "int64": 42, - "float64": 3.14, - "string": "abc", - "bytes": []byte{1, 2, 3}, - "time": time.Unix(1, 0), - }, - }, - - // Pointers - { - Value: &SimplePtrOmitEmpty{}, - JSON: Object{}, - }, - { - Value: &SimplePtrOmitEmpty{}, - JSON: Object{ - "bool": true, - "int": 42, - "int64": 42, - "float64": 3.14, - "string": "abc", - "bytes": []byte{1, 2, 3}, - "time": time.Unix(1, 0), - }, - }, - - // JSON tag "fieldName" - { - Value: &Simple{}, - JSON: Object{ - "bool": false, - "int": 0, - "int64": 0, - "float64": 0, - "string": "", - "bytes": []byte{}, - "time": time.Unix(0, 0), - }, - }, - - // JSON tag ",omitempty" - { - Value: &OriginalNameType{}, - JSON: Object{ - "Field": "abc", - }, - }, - - // Embedding - { - Value: &RootType{}, - JSON: Object{}, - }, - { - Value: &RootType{}, - JSON: Object{ - "embedded0": "0", - "embedded1": "1", - "x-other": "abc", - }, - }, -} - -type Object map[string]interface{} - -func TestExtensions(t *testing.T) { - for _, example := range Examples { - // Define JSON that will be unmarshalled - expectedData, err := json.Marshal(example.JSON) - if err != nil { - panic(err) - } - expected := string(expectedData) - - // Define value that will marshalled - x := example.Value - - // Unmarshal - if !example.NoUnmarshal { - t.Logf("Unmarshalling %T", x) - if err := jsoninfo.UnmarshalStrictStruct(expectedData, x); err != nil { - t.Fatalf("Error unmarshalling %T: %v", x, err) - } - t.Logf("Marshalling %T", x) - } - - // Marshal - if !example.NoMarshal { - data, err := jsoninfo.MarshalStrictStruct(x) - if err != nil { - t.Fatalf("Error marshalling: %v", err) - } - actually := string(data) - - if actually != expected { - t.Fatalf("Error!\nExpected: %s\nActually: %s", expected, actually) - } - } - } -} diff --git a/jsoninfo/strict_struct.go b/jsoninfo/strict_struct.go deleted file mode 100644 index 6b4d83977..000000000 --- a/jsoninfo/strict_struct.go +++ /dev/null @@ -1,6 +0,0 @@ -package jsoninfo - -type StrictStruct interface { - EncodeWith(encoder *ObjectEncoder, value interface{}) error - DecodeWith(decoder *ObjectDecoder, value interface{}) error -} diff --git a/jsoninfo/type_info.go b/jsoninfo/type_info.go deleted file mode 100644 index 3dbb8d5d6..000000000 --- a/jsoninfo/type_info.go +++ /dev/null @@ -1,68 +0,0 @@ -package jsoninfo - -import ( - "reflect" - "sort" - "sync" -) - -var ( - typeInfos = map[reflect.Type]*TypeInfo{} - typeInfosMutex sync.RWMutex -) - -// TypeInfo contains information about JSON serialization of a type -type TypeInfo struct { - Type reflect.Type - Fields []FieldInfo -} - -func GetTypeInfoForValue(value interface{}) *TypeInfo { - return GetTypeInfo(reflect.TypeOf(value)) -} - -// GetTypeInfo returns TypeInfo for the given type. -func GetTypeInfo(t reflect.Type) *TypeInfo { - for t.Kind() == reflect.Ptr { - t = t.Elem() - } - typeInfosMutex.RLock() - typeInfo, exists := typeInfos[t] - typeInfosMutex.RUnlock() - if exists { - return typeInfo - } - if t.Kind() != reflect.Struct { - typeInfo = &TypeInfo{ - Type: t, - } - } else { - // Allocate - typeInfo = &TypeInfo{ - Type: t, - Fields: make([]FieldInfo, 0, 16), - } - - // Add fields - typeInfo.Fields = AppendFields(nil, nil, t) - - // Sort fields - sort.Sort(sortableFieldInfos(typeInfo.Fields)) - } - - // Publish - typeInfosMutex.Lock() - typeInfos[t] = typeInfo - typeInfosMutex.Unlock() - return typeInfo -} - -// FieldNames returns all field names -func (typeInfo *TypeInfo) FieldNames() []string { - fields := typeInfo.Fields - names := make([]string, 0, len(fields)) - for _, field := range fields { - names = append(names, field.JSONName) - } - return names -} diff --git a/jsoninfo/unmarshal.go b/jsoninfo/unmarshal.go deleted file mode 100644 index 16886ad83..000000000 --- a/jsoninfo/unmarshal.go +++ /dev/null @@ -1,121 +0,0 @@ -package jsoninfo - -import ( - "encoding/json" - "fmt" - "reflect" -) - -// UnmarshalStrictStruct function: -// - Unmarshals struct fields, ignoring UnmarshalJSON(...) and fields without 'json' tag. -// - Correctly handles StrictStruct -func UnmarshalStrictStruct(data []byte, value StrictStruct) error { - decoder, err := NewObjectDecoder(data) - if err != nil { - return err - } - return value.DecodeWith(decoder, value) -} - -type ObjectDecoder struct { - Data []byte - remainingFields map[string]json.RawMessage -} - -func NewObjectDecoder(data []byte) (*ObjectDecoder, error) { - var remainingFields map[string]json.RawMessage - if err := json.Unmarshal(data, &remainingFields); err != nil { - return nil, fmt.Errorf("failed to unmarshal extension properties: %w (%s)", err, data) - } - return &ObjectDecoder{ - Data: data, - remainingFields: remainingFields, - }, nil -} - -// DecodeExtensionMap returns all properties that were not decoded previously. -func (decoder *ObjectDecoder) DecodeExtensionMap() map[string]json.RawMessage { - return decoder.remainingFields -} - -func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) error { - reflection := reflect.ValueOf(value) - if reflection.Kind() != reflect.Ptr { - panic(fmt.Errorf("value %T is not a pointer", value)) - } - if reflection.IsNil() { - panic(fmt.Errorf("value %T is nil", value)) - } - reflection = reflection.Elem() - for (reflection.Kind() == reflect.Interface || reflection.Kind() == reflect.Ptr) && !reflection.IsNil() { - reflection = reflection.Elem() - } - reflectionType := reflection.Type() - if reflectionType.Kind() != reflect.Struct { - panic(fmt.Errorf("value %T is not a struct", value)) - } - typeInfo := GetTypeInfo(reflectionType) - - // Supported fields - fields := typeInfo.Fields - remainingFields := decoder.remainingFields - for fieldIndex, field := range fields { - // Fields without JSON tag are ignored - if !field.HasJSONTag { - continue - } - - // Get data - fieldData, exists := remainingFields[field.JSONName] - if !exists { - continue - } - - // Unmarshal - if field.TypeIsUnmarshaller { - fieldType := field.Type - isPtr := false - if fieldType.Kind() == reflect.Ptr { - fieldType = fieldType.Elem() - isPtr = true - } - fieldValue := reflect.New(fieldType) - if err := fieldValue.Interface().(json.Unmarshaler).UnmarshalJSON(fieldData); err != nil { - if field.MultipleFields { - i := fieldIndex + 1 - if i < len(fields) && fields[i].JSONName == field.JSONName { - continue - } - } - return fmt.Errorf("failed to unmarshal property %q (%s): %w", - field.JSONName, fieldValue.Type().String(), err) - } - if !isPtr { - fieldValue = fieldValue.Elem() - } - reflection.FieldByIndex(field.Index).Set(fieldValue) - - // Remove the field from remaining fields - delete(remainingFields, field.JSONName) - } else { - fieldPtr := reflection.FieldByIndex(field.Index) - if fieldPtr.Kind() != reflect.Ptr || fieldPtr.IsNil() { - fieldPtr = fieldPtr.Addr() - } - if err := json.Unmarshal(fieldData, fieldPtr.Interface()); err != nil { - if field.MultipleFields { - i := fieldIndex + 1 - if i < len(fields) && fields[i].JSONName == field.JSONName { - continue - } - } - return fmt.Errorf("failed to unmarshal property %q (%s): %w", - field.JSONName, fieldPtr.Type().String(), err) - } - - // Remove the field from remaining fields - delete(remainingFields, field.JSONName) - } - } - return nil -} diff --git a/jsoninfo/unmarshal_test.go b/jsoninfo/unmarshal_test.go deleted file mode 100644 index dd25f04b6..000000000 --- a/jsoninfo/unmarshal_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package jsoninfo - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewObjectDecoder(t *testing.T) { - data := []byte(` - { - "field1": 1, - "field2": 2 - } -`) - t.Run("test new object decoder", func(t *testing.T) { - decoder, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, decoder) - require.Equal(t, data, decoder.Data) - require.Equal(t, 2, len(decoder.DecodeExtensionMap())) - }) -} - -type mockStrictStruct struct { - EncodeWithFn func(encoder *ObjectEncoder, value interface{}) error - DecodeWithFn func(decoder *ObjectDecoder, value interface{}) error -} - -func (m *mockStrictStruct) EncodeWith(encoder *ObjectEncoder, value interface{}) error { - return m.EncodeWithFn(encoder, value) -} - -func (m *mockStrictStruct) DecodeWith(decoder *ObjectDecoder, value interface{}) error { - return m.DecodeWithFn(decoder, value) -} - -func TestUnmarshalStrictStruct(t *testing.T) { - data := []byte(` - { - "field1": 1, - "field2": 2 - } - `) - - t.Run("test unmarshal with StrictStruct without err", func(t *testing.T) { - decodeWithFnCalled := 0 - mockStruct := &mockStrictStruct{ - EncodeWithFn: func(encoder *ObjectEncoder, value interface{}) error { - return nil - }, - DecodeWithFn: func(decoder *ObjectDecoder, value interface{}) error { - decodeWithFnCalled++ - return nil - }, - } - err := UnmarshalStrictStruct(data, mockStruct) - require.NoError(t, err) - require.Equal(t, 1, decodeWithFnCalled) - }) - - t.Run("test unmarshal with StrictStruct with err", func(t *testing.T) { - decodeWithFnCalled := 0 - mockStruct := &mockStrictStruct{ - EncodeWithFn: func(encoder *ObjectEncoder, value interface{}) error { - return nil - }, - DecodeWithFn: func(decoder *ObjectDecoder, value interface{}) error { - decodeWithFnCalled++ - return errors.New("unable to decode the value") - }, - } - err := UnmarshalStrictStruct(data, mockStruct) - require.Error(t, err) - require.Equal(t, 1, decodeWithFnCalled) - }) -} - -func TestDecodeStructFieldsAndExtensions(t *testing.T) { - data := []byte(` - { - "field1": "field1", - "field2": "field2" - } -`) - decoder, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, decoder) - - t.Run("value is not pointer", func(t *testing.T) { - var value interface{} - require.Panics(t, func() { - _ = decoder.DecodeStructFieldsAndExtensions(value) - }, "value is not a pointer") - }) - - t.Run("value is nil", func(t *testing.T) { - var value *string = nil - require.Panics(t, func() { - _ = decoder.DecodeStructFieldsAndExtensions(value) - }, "value is nil") - }) - - t.Run("value is not struct", func(t *testing.T) { - var value = "simple string" - require.Panics(t, func() { - _ = decoder.DecodeStructFieldsAndExtensions(&value) - }, "value is not struct") - }) - - t.Run("successfully decoded with all fields", func(t *testing.T) { - d, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, d) - - var value = struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` - }{} - err = d.DecodeStructFieldsAndExtensions(&value) - require.NoError(t, err) - require.Equal(t, "field1", value.Field1) - require.Equal(t, "field2", value.Field2) - require.Equal(t, 0, len(d.DecodeExtensionMap())) - }) - - t.Run("successfully decoded with renaming field", func(t *testing.T) { - d, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, d) - - var value = struct { - Field1 string `json:"field1"` - }{} - err = d.DecodeStructFieldsAndExtensions(&value) - require.NoError(t, err) - require.Equal(t, "field1", value.Field1) - require.Equal(t, 1, len(d.DecodeExtensionMap())) - }) - - t.Run("un-successfully decoded due to data mismatch", func(t *testing.T) { - d, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, d) - - var value = struct { - Field1 int `json:"field1"` - }{} - err = d.DecodeStructFieldsAndExtensions(&value) - require.Error(t, err) - require.EqualError(t, err, `failed to unmarshal property "field1" (*int): json: cannot unmarshal string into Go value of type int`) - require.Equal(t, 0, value.Field1) - require.Equal(t, 2, len(d.DecodeExtensionMap())) - }) -} diff --git a/jsoninfo/unsupported_properties_error.go b/jsoninfo/unsupported_properties_error.go deleted file mode 100644 index f69aafdc3..000000000 --- a/jsoninfo/unsupported_properties_error.go +++ /dev/null @@ -1,42 +0,0 @@ -package jsoninfo - -import ( - "encoding/json" - "fmt" - "sort" -) - -// UnsupportedPropertiesError is a helper for extensions that want to refuse -// unsupported JSON object properties. -// -// It produces a helpful error message. -type UnsupportedPropertiesError struct { - Value interface{} - UnsupportedProperties map[string]json.RawMessage -} - -func NewUnsupportedPropertiesError(v interface{}, m map[string]json.RawMessage) error { - return &UnsupportedPropertiesError{ - Value: v, - UnsupportedProperties: m, - } -} - -func (err *UnsupportedPropertiesError) Error() string { - m := err.UnsupportedProperties - typeInfo := GetTypeInfoForValue(err.Value) - if m == nil || typeInfo == nil { - return fmt.Sprintf("invalid %T", *err) - } - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - supported := typeInfo.FieldNames() - if len(supported) == 0 { - return fmt.Sprintf("type \"%T\" doesn't take any properties. Unsupported properties: %+v", - err.Value, keys) - } - return fmt.Sprintf("unsupported properties: %+v (supported properties are: %+v)", keys, supported) -} diff --git a/openapi2/header.go b/openapi2/header.go new file mode 100644 index 000000000..a51f99dee --- /dev/null +++ b/openapi2/header.go @@ -0,0 +1,15 @@ +package openapi2 + +type Header struct { + Parameter +} + +// MarshalJSON returns the JSON encoding of Header. +func (header Header) MarshalJSON() ([]byte, error) { + return header.Parameter.MarshalJSON() +} + +// UnmarshalJSON sets Header to a copy of data. +func (header *Header) UnmarshalJSON(data []byte) error { + return header.Parameter.UnmarshalJSON(data) +} diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 4927ade86..88835db95 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -1,19 +1,17 @@ package openapi2 import ( - "fmt" - "net/http" - "sort" + "encoding/json" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" ) // T is the root of an OpenAPI v2 document type T struct { - openapi3.ExtensionProps - Swagger string `json:"swagger" yaml:"swagger"` - Info openapi3.Info `json:"info" yaml:"info"` + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Swagger string `json:"swagger" yaml:"swagger"` // required + Info openapi3.Info `json:"info" yaml:"info"` // required ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` @@ -30,253 +28,90 @@ type T struct { } // MarshalJSON returns the JSON encoding of T. -func (doc *T) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(doc) -} - -// UnmarshalJSON sets T to a copy of data. -func (doc *T) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, doc) -} - -func (doc *T) AddOperation(path string, method string, operation *Operation) { - if doc.Paths == nil { - doc.Paths = make(map[string]*PathItem) +func (doc T) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 15+len(doc.Extensions)) + for k, v := range doc.Extensions { + m[k] = v } - pathItem := doc.Paths[path] - if pathItem == nil { - pathItem = &PathItem{} - doc.Paths[path] = pathItem + m["swagger"] = doc.Swagger + m["info"] = doc.Info + if x := doc.ExternalDocs; x != nil { + m["externalDocs"] = x } - pathItem.SetOperation(method, operation) -} - -type PathItem struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` - Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` - Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` - Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` - Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` - Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` - Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` - Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` -} - -// MarshalJSON returns the JSON encoding of PathItem. -func (pathItem *PathItem) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(pathItem) -} - -// UnmarshalJSON sets PathItem to a copy of data. -func (pathItem *PathItem) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, pathItem) -} - -func (pathItem *PathItem) Operations() map[string]*Operation { - operations := make(map[string]*Operation) - if v := pathItem.Delete; v != nil { - operations[http.MethodDelete] = v + if x := doc.Schemes; len(x) != 0 { + m["schemes"] = x } - if v := pathItem.Get; v != nil { - operations[http.MethodGet] = v + if x := doc.Consumes; len(x) != 0 { + m["consumes"] = x } - if v := pathItem.Head; v != nil { - operations[http.MethodHead] = v + if x := doc.Produces; len(x) != 0 { + m["produces"] = x } - if v := pathItem.Options; v != nil { - operations[http.MethodOptions] = v + if x := doc.Host; x != "" { + m["host"] = x } - if v := pathItem.Patch; v != nil { - operations[http.MethodPatch] = v + if x := doc.BasePath; x != "" { + m["basePath"] = x } - if v := pathItem.Post; v != nil { - operations[http.MethodPost] = v + if x := doc.Paths; len(x) != 0 { + m["paths"] = x } - if v := pathItem.Put; v != nil { - operations[http.MethodPut] = v + if x := doc.Definitions; len(x) != 0 { + m["definitions"] = x } - return operations -} - -func (pathItem *PathItem) GetOperation(method string) *Operation { - switch method { - case http.MethodDelete: - return pathItem.Delete - case http.MethodGet: - return pathItem.Get - case http.MethodHead: - return pathItem.Head - case http.MethodOptions: - return pathItem.Options - case http.MethodPatch: - return pathItem.Patch - case http.MethodPost: - return pathItem.Post - case http.MethodPut: - return pathItem.Put - default: - panic(fmt.Errorf("unsupported HTTP method %q", method)) + if x := doc.Parameters; len(x) != 0 { + m["parameters"] = x } -} - -func (pathItem *PathItem) SetOperation(method string, operation *Operation) { - switch method { - case http.MethodDelete: - pathItem.Delete = operation - case http.MethodGet: - pathItem.Get = operation - case http.MethodHead: - pathItem.Head = operation - case http.MethodOptions: - pathItem.Options = operation - case http.MethodPatch: - pathItem.Patch = operation - case http.MethodPost: - pathItem.Post = operation - case http.MethodPut: - pathItem.Put = operation - default: - panic(fmt.Errorf("unsupported HTTP method %q", method)) + if x := doc.Responses; len(x) != 0 { + m["responses"] = x } -} - -type Operation struct { - openapi3.ExtensionProps - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` - OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` - Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Responses map[string]*Response `json:"responses" yaml:"responses"` - Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` - Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` - Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` - Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` -} - -// MarshalJSON returns the JSON encoding of Operation. -func (operation *Operation) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(operation) -} - -// UnmarshalJSON sets Operation to a copy of data. -func (operation *Operation) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, operation) -} - -type Parameters []*Parameter - -var _ sort.Interface = Parameters{} - -func (ps Parameters) Len() int { return len(ps) } -func (ps Parameters) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } -func (ps Parameters) Less(i, j int) bool { - if ps[i].Name != ps[j].Name { - return ps[i].Name < ps[j].Name + if x := doc.SecurityDefinitions; len(x) != 0 { + m["securityDefinitions"] = x } - if ps[i].In != ps[j].In { - return ps[i].In < ps[j].In + if x := doc.Security; len(x) != 0 { + m["security"] = x } - return ps[i].Ref < ps[j].Ref -} - -type Parameter struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Format string `json:"format,omitempty" yaml:"format,omitempty"` - Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` - ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` - ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` - Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` - Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` - Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` - MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` - MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` - MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` - MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` -} - -// MarshalJSON returns the JSON encoding of Parameter. -func (parameter *Parameter) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(parameter) -} - -// UnmarshalJSON sets Parameter to a copy of data. -func (parameter *Parameter) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, parameter) -} - -type Response struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` - Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` -} - -// MarshalJSON returns the JSON encoding of Response. -func (response *Response) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(response) -} - -// UnmarshalJSON sets Response to a copy of data. -func (response *Response) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, response) -} - -type Header struct { - Parameter -} - -// MarshalJSON returns the JSON encoding of Header. -func (header *Header) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(header) -} - -// UnmarshalJSON sets Header to a copy of data. -func (header *Header) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, header) -} - -type SecurityRequirements []map[string][]string - -type SecurityScheme struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Flow string `json:"flow,omitempty" yaml:"flow,omitempty"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` - Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + if x := doc.Tags; len(x) != 0 { + m["tags"] = x + } + return json.Marshal(m) } -// MarshalJSON returns the JSON encoding of SecurityScheme. -func (securityScheme *SecurityScheme) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(securityScheme) +// UnmarshalJSON sets T to a copy of data. +func (doc *T) UnmarshalJSON(data []byte) error { + type TBis T + var x TBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "swagger") + delete(x.Extensions, "info") + delete(x.Extensions, "externalDocs") + delete(x.Extensions, "schemes") + delete(x.Extensions, "consumes") + delete(x.Extensions, "produces") + delete(x.Extensions, "host") + delete(x.Extensions, "basePath") + delete(x.Extensions, "paths") + delete(x.Extensions, "definitions") + delete(x.Extensions, "parameters") + delete(x.Extensions, "responses") + delete(x.Extensions, "securityDefinitions") + delete(x.Extensions, "security") + delete(x.Extensions, "tags") + *doc = T(x) + return nil } -// UnmarshalJSON sets SecurityScheme to a copy of data. -func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, securityScheme) +func (doc *T) AddOperation(path string, method string, operation *Operation) { + if doc.Paths == nil { + doc.Paths = make(map[string]*PathItem) + } + pathItem := doc.Paths[path] + if pathItem == nil { + pathItem = &PathItem{} + doc.Paths[path] = pathItem + } + pathItem.SetOperation(method, operation) } diff --git a/openapi2/operation.go b/openapi2/operation.go new file mode 100644 index 000000000..b29f67de3 --- /dev/null +++ b/openapi2/operation.go @@ -0,0 +1,91 @@ +package openapi2 + +import ( + "encoding/json" + + "github.com/getkin/kin-openapi/openapi3" +) + +type Operation struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` + OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Responses map[string]*Response `json:"responses" yaml:"responses"` + Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` + Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` + Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` + Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` +} + +// MarshalJSON returns the JSON encoding of Operation. +func (operation Operation) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 12+len(operation.Extensions)) + for k, v := range operation.Extensions { + m[k] = v + } + if x := operation.Summary; x != "" { + m["summary"] = x + } + if x := operation.Description; x != "" { + m["description"] = x + } + if x := operation.Deprecated; x { + m["deprecated"] = x + } + if x := operation.ExternalDocs; x != nil { + m["externalDocs"] = x + } + if x := operation.Tags; len(x) != 0 { + m["tags"] = x + } + if x := operation.OperationID; x != "" { + m["operationId"] = x + } + if x := operation.Parameters; len(x) != 0 { + m["parameters"] = x + } + m["responses"] = operation.Responses + if x := operation.Consumes; len(x) != 0 { + m["consumes"] = x + } + if x := operation.Produces; len(x) != 0 { + m["produces"] = x + } + if x := operation.Schemes; len(x) != 0 { + m["schemes"] = x + } + if x := operation.Security; x != nil { + m["security"] = x + } + return json.Marshal(m) +} + +// UnmarshalJSON sets Operation to a copy of data. +func (operation *Operation) UnmarshalJSON(data []byte) error { + type OperationBis Operation + var x OperationBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "summary") + delete(x.Extensions, "description") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "externalDocs") + delete(x.Extensions, "tags") + delete(x.Extensions, "operationId") + delete(x.Extensions, "parameters") + delete(x.Extensions, "responses") + delete(x.Extensions, "consumes") + delete(x.Extensions, "produces") + delete(x.Extensions, "schemes") + delete(x.Extensions, "security") + *operation = Operation(x) + return nil +} diff --git a/openapi2/parameter.go b/openapi2/parameter.go new file mode 100644 index 000000000..d2c71c64f --- /dev/null +++ b/openapi2/parameter.go @@ -0,0 +1,176 @@ +package openapi2 + +import ( + "encoding/json" + "sort" + + "github.com/getkin/kin-openapi/openapi3" +) + +type Parameters []*Parameter + +var _ sort.Interface = Parameters{} + +func (ps Parameters) Len() int { return len(ps) } +func (ps Parameters) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } +func (ps Parameters) Less(i, j int) bool { + if ps[i].Name != ps[j].Name { + return ps[i].Name < ps[j].Name + } + if ps[i].In != ps[j].In { + return ps[i].In < ps[j].In + } + return ps[i].Ref < ps[j].Ref +} + +type Parameter struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + + In string `json:"in,omitempty" yaml:"in,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` +} + +// MarshalJSON returns the JSON encoding of Parameter. +func (parameter Parameter) MarshalJSON() ([]byte, error) { + if ref := parameter.Ref; ref != "" { + return json.Marshal(openapi3.Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 24+len(parameter.Extensions)) + for k, v := range parameter.Extensions { + m[k] = v + } + + if x := parameter.In; x != "" { + m["in"] = x + } + if x := parameter.Name; x != "" { + m["name"] = x + } + if x := parameter.Description; x != "" { + m["description"] = x + } + if x := parameter.CollectionFormat; x != "" { + m["collectionFormat"] = x + } + if x := parameter.Type; x != "" { + m["type"] = x + } + if x := parameter.Format; x != "" { + m["format"] = x + } + if x := parameter.Pattern; x != "" { + m["pattern"] = x + } + if x := parameter.AllowEmptyValue; x { + m["allowEmptyValue"] = x + } + if x := parameter.Required; x { + m["required"] = x + } + if x := parameter.UniqueItems; x { + m["uniqueItems"] = x + } + if x := parameter.ExclusiveMin; x { + m["exclusiveMinimum"] = x + } + if x := parameter.ExclusiveMax; x { + m["exclusiveMaximum"] = x + } + if x := parameter.Schema; x != nil { + m["schema"] = x + } + if x := parameter.Items; x != nil { + m["items"] = x + } + if x := parameter.Enum; x != nil { + m["enum"] = x + } + if x := parameter.MultipleOf; x != nil { + m["multipleOf"] = x + } + if x := parameter.Minimum; x != nil { + m["minimum"] = x + } + if x := parameter.Maximum; x != nil { + m["maximum"] = x + } + if x := parameter.MaxLength; x != nil { + m["maxLength"] = x + } + if x := parameter.MaxItems; x != nil { + m["maxItems"] = x + } + if x := parameter.MinLength; x != 0 { + m["minLength"] = x + } + if x := parameter.MinItems; x != 0 { + m["minItems"] = x + } + if x := parameter.Default; x != nil { + m["default"] = x + } + + return json.Marshal(m) +} + +// UnmarshalJSON sets Parameter to a copy of data. +func (parameter *Parameter) UnmarshalJSON(data []byte) error { + type ParameterBis Parameter + var x ParameterBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + + delete(x.Extensions, "in") + delete(x.Extensions, "name") + delete(x.Extensions, "description") + delete(x.Extensions, "collectionFormat") + delete(x.Extensions, "type") + delete(x.Extensions, "format") + delete(x.Extensions, "pattern") + delete(x.Extensions, "allowEmptyValue") + delete(x.Extensions, "required") + delete(x.Extensions, "uniqueItems") + delete(x.Extensions, "exclusiveMinimum") + delete(x.Extensions, "exclusiveMaximum") + delete(x.Extensions, "schema") + delete(x.Extensions, "items") + delete(x.Extensions, "enum") + delete(x.Extensions, "multipleOf") + delete(x.Extensions, "minimum") + delete(x.Extensions, "maximum") + delete(x.Extensions, "maxLength") + delete(x.Extensions, "maxItems") + delete(x.Extensions, "minLength") + delete(x.Extensions, "minItems") + delete(x.Extensions, "default") + + *parameter = Parameter(x) + return nil +} diff --git a/openapi2/path_item.go b/openapi2/path_item.go new file mode 100644 index 000000000..95c060e7b --- /dev/null +++ b/openapi2/path_item.go @@ -0,0 +1,150 @@ +package openapi2 + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" +) + +type PathItem struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + + Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` + Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` + Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` + Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` + Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` + Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` + Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` + Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` +} + +// MarshalJSON returns the JSON encoding of PathItem. +func (pathItem PathItem) MarshalJSON() ([]byte, error) { + if ref := pathItem.Ref; ref != "" { + return json.Marshal(openapi3.Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 8+len(pathItem.Extensions)) + for k, v := range pathItem.Extensions { + m[k] = v + } + if x := pathItem.Delete; x != nil { + m["delete"] = x + } + if x := pathItem.Get; x != nil { + m["get"] = x + } + if x := pathItem.Head; x != nil { + m["head"] = x + } + if x := pathItem.Options; x != nil { + m["options"] = x + } + if x := pathItem.Patch; x != nil { + m["patch"] = x + } + if x := pathItem.Post; x != nil { + m["post"] = x + } + if x := pathItem.Put; x != nil { + m["put"] = x + } + if x := pathItem.Parameters; len(x) != 0 { + m["parameters"] = x + } + return json.Marshal(m) +} + +// UnmarshalJSON sets PathItem to a copy of data. +func (pathItem *PathItem) UnmarshalJSON(data []byte) error { + type PathItemBis PathItem + var x PathItemBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + delete(x.Extensions, "delete") + delete(x.Extensions, "get") + delete(x.Extensions, "head") + delete(x.Extensions, "options") + delete(x.Extensions, "patch") + delete(x.Extensions, "post") + delete(x.Extensions, "put") + delete(x.Extensions, "parameters") + *pathItem = PathItem(x) + return nil +} + +func (pathItem *PathItem) Operations() map[string]*Operation { + operations := make(map[string]*Operation) + if v := pathItem.Delete; v != nil { + operations[http.MethodDelete] = v + } + if v := pathItem.Get; v != nil { + operations[http.MethodGet] = v + } + if v := pathItem.Head; v != nil { + operations[http.MethodHead] = v + } + if v := pathItem.Options; v != nil { + operations[http.MethodOptions] = v + } + if v := pathItem.Patch; v != nil { + operations[http.MethodPatch] = v + } + if v := pathItem.Post; v != nil { + operations[http.MethodPost] = v + } + if v := pathItem.Put; v != nil { + operations[http.MethodPut] = v + } + return operations +} + +func (pathItem *PathItem) GetOperation(method string) *Operation { + switch method { + case http.MethodDelete: + return pathItem.Delete + case http.MethodGet: + return pathItem.Get + case http.MethodHead: + return pathItem.Head + case http.MethodOptions: + return pathItem.Options + case http.MethodPatch: + return pathItem.Patch + case http.MethodPost: + return pathItem.Post + case http.MethodPut: + return pathItem.Put + default: + panic(fmt.Errorf("unsupported HTTP method %q", method)) + } +} + +func (pathItem *PathItem) SetOperation(method string, operation *Operation) { + switch method { + case http.MethodDelete: + pathItem.Delete = operation + case http.MethodGet: + pathItem.Get = operation + case http.MethodHead: + pathItem.Head = operation + case http.MethodOptions: + pathItem.Options = operation + case http.MethodPatch: + pathItem.Patch = operation + case http.MethodPost: + pathItem.Post = operation + case http.MethodPut: + pathItem.Put = operation + default: + panic(fmt.Errorf("unsupported HTTP method %q", method)) + } +} diff --git a/openapi2/response.go b/openapi2/response.go new file mode 100644 index 000000000..bd18f882d --- /dev/null +++ b/openapi2/response.go @@ -0,0 +1,60 @@ +package openapi2 + +import ( + "encoding/json" + + "github.com/getkin/kin-openapi/openapi3" +) + +type Response struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` +} + +// MarshalJSON returns the JSON encoding of Response. +func (response Response) MarshalJSON() ([]byte, error) { + if ref := response.Ref; ref != "" { + return json.Marshal(openapi3.Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 4+len(response.Extensions)) + for k, v := range response.Extensions { + m[k] = v + } + if x := response.Description; x != "" { + m["description"] = x + } + if x := response.Schema; x != nil { + m["schema"] = x + } + if x := response.Headers; len(x) != 0 { + m["headers"] = x + } + if x := response.Examples; len(x) != 0 { + m["examples"] = x + } + return json.Marshal(m) +} + +// UnmarshalJSON sets Response to a copy of data. +func (response *Response) UnmarshalJSON(data []byte) error { + type ResponseBis Response + var x ResponseBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + delete(x.Extensions, "description") + delete(x.Extensions, "schema") + delete(x.Extensions, "headers") + delete(x.Extensions, "examples") + *response = Response(x) + return nil +} diff --git a/openapi2/security_scheme.go b/openapi2/security_scheme.go new file mode 100644 index 000000000..5a8c278bd --- /dev/null +++ b/openapi2/security_scheme.go @@ -0,0 +1,87 @@ +package openapi2 + +import ( + "encoding/json" + + "github.com/getkin/kin-openapi/openapi3" +) + +type SecurityRequirements []map[string][]string + +type SecurityScheme struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Flow string `json:"flow,omitempty" yaml:"flow,omitempty"` + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` + Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` +} + +// MarshalJSON returns the JSON encoding of SecurityScheme. +func (securityScheme SecurityScheme) MarshalJSON() ([]byte, error) { + if ref := securityScheme.Ref; ref != "" { + return json.Marshal(openapi3.Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 10+len(securityScheme.Extensions)) + for k, v := range securityScheme.Extensions { + m[k] = v + } + if x := securityScheme.Description; x != "" { + m["description"] = x + } + if x := securityScheme.Type; x != "" { + m["type"] = x + } + if x := securityScheme.In; x != "" { + m["in"] = x + } + if x := securityScheme.Name; x != "" { + m["name"] = x + } + if x := securityScheme.Flow; x != "" { + m["flow"] = x + } + if x := securityScheme.AuthorizationURL; x != "" { + m["authorizationUrl"] = x + } + if x := securityScheme.TokenURL; x != "" { + m["tokenUrl"] = x + } + if x := securityScheme.Scopes; len(x) != 0 { + m["scopes"] = x + } + if x := securityScheme.Tags; len(x) != 0 { + m["tags"] = x + } + return json.Marshal(m) +} + +// UnmarshalJSON sets SecurityScheme to a copy of data. +func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error { + type SecuritySchemeBis SecurityScheme + var x SecuritySchemeBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + delete(x.Extensions, "description") + delete(x.Extensions, "type") + delete(x.Extensions, "in") + delete(x.Extensions, "name") + delete(x.Extensions, "flow") + delete(x.Extensions, "authorizationUrl") + delete(x.Extensions, "tokenUrl") + delete(x.Extensions, "scopes") + delete(x.Extensions, "tags") + *securityScheme = SecurityScheme(x) + return nil +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 989b685d5..c80e67201 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -1,7 +1,6 @@ package openapi2conv import ( - "encoding/json" "errors" "fmt" "net/url" @@ -14,15 +13,13 @@ import ( // ToV3 converts an OpenAPIv2 spec to an OpenAPIv3 spec func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { - stripNonCustomExtensions(doc2.Extensions) - doc3 := &openapi3.T{ - OpenAPI: "3.0.3", - Info: &doc2.Info, - Components: openapi3.Components{}, - Tags: doc2.Tags, - ExtensionProps: doc2.ExtensionProps, - ExternalDocs: doc2.ExternalDocs, + OpenAPI: "3.0.3", + Info: &doc2.Info, + Components: &openapi3.Components{}, + Tags: doc2.Tags, + Extensions: stripNonExtensions(doc2.Extensions), + ExternalDocs: doc2.ExternalDocs, } if host := doc2.Host; host != "" { @@ -53,7 +50,7 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { doc3.Components.Parameters = make(map[string]*openapi3.ParameterRef) doc3.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) for k, parameter := range parameters { - v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&doc3.Components, parameter, doc2.Consumes) + v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(doc3.Components, parameter, doc2.Consumes) switch { case err != nil: return nil, err @@ -72,7 +69,7 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { if paths := doc2.Paths; len(paths) != 0 { doc3Paths := make(map[string]*openapi3.PathItem, len(paths)) for path, pathItem := range paths { - r, err := ToV3PathItem(doc2, &doc3.Components, pathItem, doc2.Consumes) + r, err := ToV3PathItem(doc2, doc3.Components, pathItem, doc2.Consumes) if err != nil { return nil, err } @@ -119,9 +116,8 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { } func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) { - stripNonCustomExtensions(pathItem.Extensions) doc3 := &openapi3.PathItem{ - ExtensionProps: pathItem.ExtensionProps, + Extensions: stripNonExtensions(pathItem.Extensions), } for method, operation := range pathItem.Operations() { doc3Operation, err := ToV3Operation(doc2, components, pathItem, operation, consumes) @@ -150,14 +146,13 @@ func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem * if operation == nil { return nil, nil } - stripNonCustomExtensions(operation.Extensions) doc3 := &openapi3.Operation{ - OperationID: operation.OperationID, - Summary: operation.Summary, - Description: operation.Description, - Deprecated: operation.Deprecated, - Tags: operation.Tags, - ExtensionProps: operation.ExtensionProps, + OperationID: operation.OperationID, + Summary: operation.Summary, + Description: operation.Description, + Deprecated: operation.Deprecated, + Tags: operation.Tags, + Extensions: stripNonExtensions(operation.Extensions), } if v := operation.Security; v != nil { doc3Security := ToV3SecurityRequirements(*v) @@ -230,24 +225,22 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete } return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil, nil } - stripNonCustomExtensions(parameter.Extensions) switch parameter.In { case "body": result := &openapi3.RequestBody{ - Description: parameter.Description, - Required: parameter.Required, - ExtensionProps: parameter.ExtensionProps, + Description: parameter.Description, + Required: parameter.Required, + Extensions: stripNonExtensions(parameter.Extensions), } if parameter.Name != "" { if result.Extensions == nil { - result.Extensions = make(map[string]interface{}) + result.Extensions = make(map[string]interface{}, 1) } result.Extensions["x-originalParamName"] = parameter.Name } if schemaRef := parameter.Schema; schemaRef != nil { - // Assuming JSON result.WithSchemaRef(ToV3SchemaRef(schemaRef), consumes) } return nil, &openapi3.RequestBodyRef{Value: result}, nil, nil @@ -257,39 +250,37 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete if typ == "file" { format, typ = "binary", "string" } - if parameter.ExtensionProps.Extensions == nil { - parameter.ExtensionProps.Extensions = make(map[string]interface{}) + if parameter.Extensions == nil { + parameter.Extensions = make(map[string]interface{}, 1) } - parameter.ExtensionProps.Extensions["x-formData-name"] = parameter.Name + parameter.Extensions["x-formData-name"] = parameter.Name var required []string if parameter.Required { required = []string{parameter.Name} } - schemaRef := &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Description: parameter.Description, - Type: typ, - ExtensionProps: parameter.ExtensionProps, - Format: format, - Enum: parameter.Enum, - Min: parameter.Minimum, - Max: parameter.Maximum, - ExclusiveMin: parameter.ExclusiveMin, - ExclusiveMax: parameter.ExclusiveMax, - MinLength: parameter.MinLength, - MaxLength: parameter.MaxLength, - Default: parameter.Default, - Items: parameter.Items, - MinItems: parameter.MinItems, - MaxItems: parameter.MaxItems, - Pattern: parameter.Pattern, - AllowEmptyValue: parameter.AllowEmptyValue, - UniqueItems: parameter.UniqueItems, - MultipleOf: parameter.MultipleOf, - Required: required, - }, - } - schemaRefMap := make(map[string]*openapi3.SchemaRef) + schemaRef := &openapi3.SchemaRef{Value: &openapi3.Schema{ + Description: parameter.Description, + Type: typ, + Extensions: stripNonExtensions(parameter.Extensions), + Format: format, + Enum: parameter.Enum, + Min: parameter.Minimum, + Max: parameter.Maximum, + ExclusiveMin: parameter.ExclusiveMin, + ExclusiveMax: parameter.ExclusiveMax, + MinLength: parameter.MinLength, + MaxLength: parameter.MaxLength, + Default: parameter.Default, + Items: parameter.Items, + MinItems: parameter.MinItems, + MaxItems: parameter.MaxItems, + Pattern: parameter.Pattern, + AllowEmptyValue: parameter.AllowEmptyValue, + UniqueItems: parameter.UniqueItems, + MultipleOf: parameter.MultipleOf, + Required: required, + }} + schemaRefMap := make(map[string]*openapi3.SchemaRef, 1) schemaRefMap[parameter.Name] = schemaRef return nil, nil, schemaRefMap, nil @@ -304,11 +295,11 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete schemaRefRef = schemaRef.Ref } result := &openapi3.Parameter{ - In: parameter.In, - Name: parameter.Name, - Description: parameter.Description, - Required: required, - ExtensionProps: parameter.ExtensionProps, + In: parameter.In, + Name: parameter.Name, + Description: parameter.Description, + Required: required, + Extensions: stripNonExtensions(parameter.Extensions), Schema: ToV3SchemaRef(&openapi3.SchemaRef{Value: &openapi3.Schema{ Type: parameter.Type, Format: parameter.Format, @@ -417,10 +408,9 @@ func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.Res if ref := response.Ref; ref != "" { return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil } - stripNonCustomExtensions(response.Extensions) result := &openapi3.Response{ - Description: &response.Description, - ExtensionProps: response.ExtensionProps, + Description: &response.Description, + Extensions: stripNonExtensions(response.Extensions), } // Default to "application/json" if "produces" is not specified. @@ -479,19 +469,14 @@ func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { for k, v := range schema.Value.Properties { schema.Value.Properties[k] = ToV3SchemaRef(v) } - if v := schema.Value.AdditionalProperties; v != nil { - schema.Value.AdditionalProperties = ToV3SchemaRef(v) + if v := schema.Value.AdditionalProperties.Schema; v != nil { + schema.Value.AdditionalProperties.Schema = ToV3SchemaRef(v) } for i, v := range schema.Value.AllOf { schema.Value.AllOf[i] = ToV3SchemaRef(v) } if val, ok := schema.Value.Extensions["x-nullable"]; ok { - var nullable bool - - if err := json.Unmarshal(val.(json.RawMessage), &nullable); err == nil { - schema.Value.Nullable = nullable - } - + schema.Value.Nullable, _ = val.(bool) delete(schema.Value.Extensions, "x-nullable") } @@ -539,10 +524,9 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu if securityScheme == nil { return nil, nil } - stripNonCustomExtensions(securityScheme.Extensions) result := &openapi3.SecurityScheme{ - Description: securityScheme.Description, - ExtensionProps: securityScheme.ExtensionProps, + Description: securityScheme.Description, + Extensions: stripNonExtensions(securityScheme.Extensions), } switch securityScheme.Type { case "basic": @@ -586,21 +570,20 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu // FromV3 converts an OpenAPIv3 spec to an OpenAPIv2 spec func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { - doc2Responses, err := FromV3Responses(doc3.Components.Responses, &doc3.Components) + doc2Responses, err := FromV3Responses(doc3.Components.Responses, doc3.Components) if err != nil { return nil, err } - stripNonCustomExtensions(doc3.Extensions) - schemas, parameters := FromV3Schemas(doc3.Components.Schemas, &doc3.Components) + schemas, parameters := FromV3Schemas(doc3.Components.Schemas, doc3.Components) doc2 := &openapi2.T{ - Swagger: "2.0", - Info: *doc3.Info, - Definitions: schemas, - Parameters: parameters, - Responses: doc2Responses, - Tags: doc3.Tags, - ExtensionProps: doc3.ExtensionProps, - ExternalDocs: doc3.ExternalDocs, + Swagger: "2.0", + Info: *doc3.Info, + Definitions: schemas, + Parameters: parameters, + Responses: doc2Responses, + Tags: doc3.Tags, + Extensions: stripNonExtensions(doc3.Extensions), + ExternalDocs: doc3.ExternalDocs, } isHTTPS := false @@ -633,8 +616,7 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { continue } doc2.AddOperation(path, "GET", nil) - stripNonCustomExtensions(pathItem.Extensions) - addPathExtensions(doc2, path, pathItem.ExtensionProps) + addPathExtensions(doc2, path, stripNonExtensions(pathItem.Extensions)) for method, operation := range pathItem.Operations() { if operation == nil { continue @@ -647,7 +629,7 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { } params := openapi2.Parameters{} for _, param := range pathItem.Parameters { - p, err := FromV3Parameter(param, &doc3.Components) + p, err := FromV3Parameter(param, doc3.Components) if err != nil { return nil, err } @@ -658,13 +640,13 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { } for name, param := range doc3.Components.Parameters { - if doc2.Parameters[name], err = FromV3Parameter(param, &doc3.Components); err != nil { + if doc2.Parameters[name], err = FromV3Parameter(param, doc3.Components); err != nil { return nil, err } } for name, requestBodyRef := range doc3.Components.RequestBodies { - bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, &doc3.Components) + bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, doc3.Components) if err != nil { return nil, err } @@ -733,7 +715,7 @@ func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, c paramName := name if originalName, ok := requestBodyRef.Value.Extensions["x-originalParamName"]; ok { - json.Unmarshal(originalName.(json.RawMessage), ¶mName) + paramName = originalName.(string) } var r *openapi2.Parameter @@ -786,11 +768,11 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components required := false value, _ := schema.Value.Extensions["x-formData-name"] - var originalName string - json.Unmarshal(value.(json.RawMessage), &originalName) + originalName, _ := value.(string) for _, prop := range schema.Value.Required { if originalName == prop { required = true + break } } return nil, &openapi2.Parameter{ @@ -812,7 +794,7 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components AllowEmptyValue: schema.Value.AllowEmptyValue, UniqueItems: schema.Value.UniqueItems, MultipleOf: schema.Value.MultipleOf, - ExtensionProps: schema.Value.ExtensionProps, + Extensions: stripNonExtensions(schema.Value.Extensions), Required: required, } } @@ -828,8 +810,8 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components for _, key := range keys { schema.Value.Properties[key], _ = FromV3SchemaRef(schema.Value.Properties[key], components) } - if v := schema.Value.AdditionalProperties; v != nil { - schema.Value.AdditionalProperties, _ = FromV3SchemaRef(v, components) + if v := schema.Value.AdditionalProperties.Schema; v != nil { + schema.Value.AdditionalProperties.Schema, _ = FromV3SchemaRef(v, components) } for i, v := range schema.Value.AllOf { schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) @@ -854,9 +836,8 @@ func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) open } func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) { - stripNonCustomExtensions(pathItem.Extensions) result := &openapi2.PathItem{ - ExtensionProps: pathItem.ExtensionProps, + Extensions: stripNonExtensions(pathItem.Extensions), } for method, operation := range pathItem.Operations() { r, err := FromV3Operation(doc3, operation) @@ -866,7 +847,7 @@ func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.Pa result.SetOperation(method, r) } for _, parameter := range pathItem.Parameters { - p, err := FromV3Parameter(parameter, &doc3.Components) + p, err := FromV3Parameter(parameter, doc3.Components) if err != nil { return nil, err } @@ -910,23 +891,23 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter } } parameter := &openapi2.Parameter{ - Name: propName, - Description: val.Description, - Type: typ, - In: "formData", - ExtensionProps: val.ExtensionProps, - Enum: val.Enum, - ExclusiveMin: val.ExclusiveMin, - ExclusiveMax: val.ExclusiveMax, - MinLength: val.MinLength, - MaxLength: val.MaxLength, - Default: val.Default, - Items: val.Items, - MinItems: val.MinItems, - MaxItems: val.MaxItems, - Maximum: val.Max, - Minimum: val.Min, - Pattern: val.Pattern, + Name: propName, + Description: val.Description, + Type: typ, + In: "formData", + Extensions: stripNonExtensions(val.Extensions), + Enum: val.Enum, + ExclusiveMin: val.ExclusiveMin, + ExclusiveMax: val.ExclusiveMax, + MinLength: val.MinLength, + MaxLength: val.MaxLength, + Default: val.Default, + Items: val.Items, + MinItems: val.MinItems, + MaxItems: val.MaxItems, + Maximum: val.Max, + Minimum: val.Min, + Pattern: val.Pattern, // CollectionFormat: val.CollectionFormat, // Format: val.Format, AllowEmptyValue: val.AllowEmptyValue, @@ -943,21 +924,20 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 if operation == nil { return nil, nil } - stripNonCustomExtensions(operation.Extensions) result := &openapi2.Operation{ - OperationID: operation.OperationID, - Summary: operation.Summary, - Description: operation.Description, - Deprecated: operation.Deprecated, - Tags: operation.Tags, - ExtensionProps: operation.ExtensionProps, + OperationID: operation.OperationID, + Summary: operation.Summary, + Description: operation.Description, + Deprecated: operation.Deprecated, + Tags: operation.Tags, + Extensions: stripNonExtensions(operation.Extensions), } if v := operation.Security; v != nil { resultSecurity := FromV3SecurityRequirements(*v) result.Security = &resultSecurity } for _, parameter := range operation.Parameters { - r, err := FromV3Parameter(parameter, &doc3.Components) + r, err := FromV3Parameter(parameter, doc3.Components) if err != nil { return nil, err } @@ -970,7 +950,7 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 return nil, errors.New("could not find a name for request body") } - bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, &doc3.Components) + bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, doc3.Components) if err != nil { return nil, err } @@ -991,7 +971,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, doc3.Components) if err != nil { return nil, err } @@ -1003,13 +983,12 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, mediaType *openapi3.MediaType, components *openapi3.Components) (*openapi2.Parameter, error) { requestBody := requestBodyRef.Value - stripNonCustomExtensions(requestBody.Extensions) result := &openapi2.Parameter{ - In: "body", - Name: name, - Description: requestBody.Description, - Required: requestBody.Required, - ExtensionProps: requestBody.ExtensionProps, + In: "body", + Name: name, + Description: requestBody.Description, + Required: requestBody.Required, + Extensions: stripNonExtensions(requestBody.Extensions), } if mediaType != nil { @@ -1026,13 +1005,12 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components if parameter == nil { return nil, nil } - stripNonCustomExtensions(parameter.Extensions) result := &openapi2.Parameter{ - Description: parameter.Description, - In: parameter.In, - Name: parameter.Name, - Required: parameter.Required, - ExtensionProps: parameter.ExtensionProps, + Description: parameter.Description, + In: parameter.In, + Name: parameter.Name, + Required: parameter.Required, + Extensions: stripNonExtensions(parameter.Extensions), } if schemaRef := parameter.Schema; schemaRef != nil { schemaRef, _ = FromV3SchemaRef(schemaRef, components) @@ -1088,10 +1066,9 @@ func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) if desc := response.Description; desc != nil { description = *desc } - stripNonCustomExtensions(response.Extensions) result := &openapi2.Response{ - Description: description, - ExtensionProps: response.ExtensionProps, + Description: description, + Extensions: stripNonExtensions(response.Extensions), } if content := response.Content; content != nil { if ct := content["application/json"]; ct != nil { @@ -1127,11 +1104,10 @@ func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*o if securityScheme == nil { return nil, nil } - stripNonCustomExtensions(securityScheme.Extensions) result := &openapi2.SecurityScheme{ - Ref: FromV3Ref(ref.Ref), - Description: securityScheme.Description, - ExtensionProps: securityScheme.ExtensionProps, + Ref: FromV3Ref(ref.Ref), + Description: securityScheme.Description, + Extensions: stripNonExtensions(securityScheme.Extensions), } switch securityScheme.Type { case "http": @@ -1195,15 +1171,17 @@ var attemptedBodyParameterNames = []string{ "requestBody", } -func stripNonCustomExtensions(extensions map[string]interface{}) { +// stripNonExtensions removes invalid extensions: those not prefixed by "x-" and returns them +func stripNonExtensions(extensions map[string]interface{}) map[string]interface{} { for extName := range extensions { if !strings.HasPrefix(extName, "x-") { delete(extensions, extName) } } + return extensions } -func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.ExtensionProps) { +func addPathExtensions(doc2 *openapi2.T, path string, extensions map[string]interface{}) { if doc2.Paths == nil { doc2.Paths = make(map[string]*openapi2.PathItem) } @@ -1212,5 +1190,5 @@ func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.Ex pathItem = &openapi2.PathItem{} doc2.Paths[path] = pathItem } - pathItem.ExtensionProps = extensionProps + pathItem.Extensions = extensions } diff --git a/openapi3/components.go b/openapi3/components.go index 8abc3fe9c..0981e8bfe 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -2,17 +2,16 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "regexp" "sort" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Components is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object type Components struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -30,13 +29,60 @@ func NewComponents() Components { } // MarshalJSON returns the JSON encoding of Components. -func (components *Components) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(components) +func (components Components) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 9+len(components.Extensions)) + for k, v := range components.Extensions { + m[k] = v + } + if x := components.Schemas; len(x) != 0 { + m["schemas"] = x + } + if x := components.Parameters; len(x) != 0 { + m["parameters"] = x + } + if x := components.Headers; len(x) != 0 { + m["headers"] = x + } + if x := components.RequestBodies; len(x) != 0 { + m["requestBodies"] = x + } + if x := components.Responses; len(x) != 0 { + m["responses"] = x + } + if x := components.SecuritySchemes; len(x) != 0 { + m["securitySchemes"] = x + } + if x := components.Examples; len(x) != 0 { + m["examples"] = x + } + if x := components.Links; len(x) != 0 { + m["links"] = x + } + if x := components.Callbacks; len(x) != 0 { + m["callbacks"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Components to a copy of data. func (components *Components) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, components) + type ComponentsBis Components + var x ComponentsBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "schemas") + delete(x.Extensions, "parameters") + delete(x.Extensions, "headers") + delete(x.Extensions, "requestBodies") + delete(x.Extensions, "responses") + delete(x.Extensions, "securitySchemes") + delete(x.Extensions, "examples") + delete(x.Extensions, "links") + delete(x.Extensions, "callbacks") + *components = Components(x) + return nil } // Validate returns an error if Components does not comply with the OpenAPI spec. @@ -178,7 +224,7 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp } } - return + return validateExtensions(ctx, components.Extensions) } const identifierPattern = `^[a-zA-Z0-9._-]+$` diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 8ab344a84..8b6b813f2 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -2,32 +2,48 @@ package openapi3 import ( "context" - - "github.com/getkin/kin-openapi/jsoninfo" + "encoding/json" ) // Discriminator is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object type Discriminator struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` - PropertyName string `json:"propertyName" yaml:"propertyName"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` } // MarshalJSON returns the JSON encoding of Discriminator. -func (discriminator *Discriminator) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(discriminator) +func (discriminator Discriminator) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 2+len(discriminator.Extensions)) + for k, v := range discriminator.Extensions { + m[k] = v + } + m["propertyName"] = discriminator.PropertyName + if x := discriminator.Mapping; len(x) != 0 { + m["mapping"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Discriminator to a copy of data. func (discriminator *Discriminator) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, discriminator) + type DiscriminatorBis Discriminator + var x DiscriminatorBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "propertyName") + delete(x.Extensions, "mapping") + *discriminator = Discriminator(x) + return nil } // Validate returns an error if Discriminator does not comply with the OpenAPI spec. func (discriminator *Discriminator) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) - return nil + return validateExtensions(ctx, discriminator.Extensions) } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 003833e16..dc2e54438 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -2,16 +2,15 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "sort" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Encoding is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object type Encoding struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -41,13 +40,44 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding { } // MarshalJSON returns the JSON encoding of Encoding. -func (encoding *Encoding) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(encoding) +func (encoding Encoding) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 5+len(encoding.Extensions)) + for k, v := range encoding.Extensions { + m[k] = v + } + if x := encoding.ContentType; x != "" { + m["contentType"] = x + } + if x := encoding.Headers; len(x) != 0 { + m["headers"] = x + } + if x := encoding.Style; x != "" { + m["style"] = x + } + if x := encoding.Explode; x != nil { + m["explode"] = x + } + if x := encoding.AllowReserved; x { + m["allowReserved"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Encoding to a copy of data. func (encoding *Encoding) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, encoding) + type EncodingBis Encoding + var x EncodingBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "contentType") + delete(x.Extensions, "headers") + delete(x.Extensions, "style") + delete(x.Extensions, "explode") + delete(x.Extensions, "allowReserved") + *encoding = Encoding(x) + return nil } // SerializationMethod returns a serialization method of request body. @@ -102,5 +132,5 @@ func (encoding *Encoding) Validate(ctx context.Context, opts ...ValidationOption return fmt.Errorf("serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode) } - return nil + return validateExtensions(ctx, encoding.Extensions) } diff --git a/openapi3/example.go b/openapi3/example.go index f4cbfc074..04338beee 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -2,12 +2,11 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type Examples map[string]*ExampleRef @@ -30,7 +29,7 @@ func (e Examples) JSONLookup(token string) (interface{}, error) { // Example is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object type Example struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -39,24 +38,49 @@ type Example struct { } func NewExample(value interface{}) *Example { - return &Example{ - Value: value, - } + return &Example{Value: value} } // MarshalJSON returns the JSON encoding of Example. -func (example *Example) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(example) +func (example Example) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(example.Extensions)) + for k, v := range example.Extensions { + m[k] = v + } + if x := example.Summary; x != "" { + m["summary"] = x + } + if x := example.Description; x != "" { + m["description"] = x + } + if x := example.Value; x != nil { + m["value"] = x + } + if x := example.ExternalValue; x != "" { + m["externalValue"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Example to a copy of data. func (example *Example) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, example) + type ExampleBis Example + var x ExampleBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "summary") + delete(x.Extensions, "description") + delete(x.Extensions, "value") + delete(x.Extensions, "externalValue") + *example = Example(x) + return nil } // Validate returns an error if Example does not comply with the OpenAPI spec. func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if example.Value != nil && example.ExternalValue != "" { return errors.New("value and externalValue are mutually exclusive") @@ -65,5 +89,5 @@ func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) return errors.New("no value or externalValue field") } - return nil + return validateExtensions(ctx, example.Extensions) } diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index 6ce7c0a48..de8954828 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -241,6 +241,7 @@ paths: spec.WriteString(tc.parametersExample) spec.WriteString(` requestBody: + required: true content: application/json: schema: @@ -249,7 +250,6 @@ paths: spec.WriteString(tc.mediaTypeRequestExample) spec.WriteString(` description: Created user object - required: true responses: '204': description: "success" @@ -262,11 +262,12 @@ paths: /readWriteOnly: post: requestBody: + required: true content: application/json: schema: $ref: "#/components/schemas/ReadWriteOnlyData" - required: true`) +`) spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample) spec.WriteString(` responses: diff --git a/openapi3/extension.go b/openapi3/extension.go index f6b7ef9bb..c29959091 100644 --- a/openapi3/extension.go +++ b/openapi3/extension.go @@ -1,38 +1,24 @@ package openapi3 import ( - "github.com/getkin/kin-openapi/jsoninfo" + "context" + "fmt" + "sort" + "strings" ) -// ExtensionProps provides support for OpenAPI extensions. -// It reads/writes all properties that begin with "x-". -type ExtensionProps struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` -} - -// Assert that the type implements the interface -var _ jsoninfo.StrictStruct = &ExtensionProps{} - -// EncodeWith will be invoked by package "jsoninfo" -func (props *ExtensionProps) EncodeWith(encoder *jsoninfo.ObjectEncoder, value interface{}) error { - for k, v := range props.Extensions { - if err := encoder.EncodeExtension(k, v); err != nil { - return err +func validateExtensions(ctx context.Context, extensions map[string]interface{}) error { // FIXME: newtype + Validate(...) + var unknowns []string + for k := range extensions { + if !strings.HasPrefix(k, "x-") { + unknowns = append(unknowns, k) } } - return encoder.EncodeStructFieldsAndExtensions(value) -} -// DecodeWith will be invoked by package "jsoninfo" -func (props *ExtensionProps) DecodeWith(decoder *jsoninfo.ObjectDecoder, value interface{}) error { - if err := decoder.DecodeStructFieldsAndExtensions(value); err != nil { - return err - } - source := decoder.DecodeExtensionMap() - result := make(map[string]interface{}, len(source)) - for k, v := range source { - result[k] = v + if len(unknowns) != 0 { + sort.Strings(unknowns) + return fmt.Errorf("extra sibling fields: %+v", unknowns) } - props.Extensions = result + return nil } diff --git a/openapi3/extension_test.go b/openapi3/extension_test.go deleted file mode 100644 index a99537892..000000000 --- a/openapi3/extension_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package openapi3 - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/getkin/kin-openapi/jsoninfo" -) - -func ExampleExtensionProps_DecodeWith() { - loader := NewLoader() - loader.IsExternalRefsAllowed = true - spec, err := loader.LoadFromFile("testdata/testref.openapi.json") - if err != nil { - panic(err) - } - - dec, err := jsoninfo.NewObjectDecoder(spec.Info.Extensions["x-my-extension"].(json.RawMessage)) - if err != nil { - panic(err) - } - var value struct { - Key int `json:"k"` - } - if err = spec.Info.DecodeWith(dec, &value); err != nil { - panic(err) - } - fmt.Println(value.Key) - // Output: 42 -} - -func TestExtensionProps_EncodeWith(t *testing.T) { - t.Run("successfully encoded", func(t *testing.T) { - encoder := jsoninfo.NewObjectEncoder() - var extensionProps = ExtensionProps{ - Extensions: map[string]interface{}{ - "field1": "value1", - }, - } - - var value = struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` - }{} - - err := extensionProps.EncodeWith(encoder, &value) - require.NoError(t, err) - }) -} - -func TestExtensionProps_DecodeWith(t *testing.T) { - data := []byte(` - { - "field1": "value1", - "field2": "value2" - } -`) - t.Run("successfully decode all the fields", func(t *testing.T) { - decoder, err := jsoninfo.NewObjectDecoder(data) - require.NoError(t, err) - var extensionProps = &ExtensionProps{ - Extensions: map[string]interface{}{ - "field1": "value1", - "field2": "value1", - }, - } - - var value = struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` - }{} - - err = extensionProps.DecodeWith(decoder, &value) - require.NoError(t, err) - require.Equal(t, 0, len(extensionProps.Extensions)) - require.Equal(t, "value1", value.Field1) - require.Equal(t, "value2", value.Field2) - }) - - t.Run("successfully decode some of the fields", func(t *testing.T) { - decoder, err := jsoninfo.NewObjectDecoder(data) - require.NoError(t, err) - var extensionProps = &ExtensionProps{ - Extensions: map[string]interface{}{ - "field1": "value1", - "field2": "value2", - }, - } - - var value = &struct { - Field1 string `json:"field1"` - }{} - - err = extensionProps.DecodeWith(decoder, value) - require.NoError(t, err) - require.Equal(t, 1, len(extensionProps.Extensions)) - require.Equal(t, "value1", value.Field1) - }) - - t.Run("successfully decode none of the fields", func(t *testing.T) { - decoder, err := jsoninfo.NewObjectDecoder(data) - require.NoError(t, err) - - var extensionProps = &ExtensionProps{ - Extensions: map[string]interface{}{ - "field1": "value1", - "field2": "value2", - }, - } - - var value = struct { - Field3 string `json:"field3"` - Field4 string `json:"field4"` - }{} - - err = extensionProps.DecodeWith(decoder, &value) - require.NoError(t, err) - require.Equal(t, 2, len(extensionProps.Extensions)) - require.Empty(t, value.Field3) - require.Empty(t, value.Field4) - }) -} diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index 65ec2e88f..276a36cce 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -2,35 +2,53 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "net/url" - - "github.com/getkin/kin-openapi/jsoninfo" ) // ExternalDocs is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object type ExternalDocs struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` } // MarshalJSON returns the JSON encoding of ExternalDocs. -func (e *ExternalDocs) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(e) +func (e ExternalDocs) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 2+len(e.Extensions)) + for k, v := range e.Extensions { + m[k] = v + } + if x := e.Description; x != "" { + m["description"] = x + } + if x := e.URL; x != "" { + m["url"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets ExternalDocs to a copy of data. func (e *ExternalDocs) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, e) + type ExternalDocsBis ExternalDocs + var x ExternalDocsBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "description") + delete(x.Extensions, "url") + *e = ExternalDocs(x) + return nil } // Validate returns an error if ExternalDocs does not comply with the OpenAPI spec. func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if e.URL == "" { return errors.New("url is required") @@ -38,5 +56,6 @@ func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) e if _, err := url.Parse(e.URL); err != nil { return fmt.Errorf("url is incorrect: %w", err) } - return nil + + return validateExtensions(ctx, e.Extensions) } diff --git a/openapi3/header.go b/openapi3/header.go index 454fad51e..8bce69f2e 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -6,8 +6,6 @@ import ( "fmt" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type Headers map[string]*HeaderRef @@ -35,9 +33,19 @@ type Header struct { var _ jsonpointer.JSONPointable = (*Header)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable +func (header Header) JSONLookup(token string) (interface{}, error) { + return header.Parameter.JSONLookup(token) +} + +// MarshalJSON returns the JSON encoding of Header. +func (header Header) MarshalJSON() ([]byte, error) { + return header.Parameter.MarshalJSON() +} + // UnmarshalJSON sets Header to a copy of data. func (header *Header) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, header) + return header.Parameter.UnmarshalJSON(data) } // SerializationMethod returns a header's serialization method. @@ -93,43 +101,3 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er } return nil } - -// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (header Header) JSONLookup(token string) (interface{}, error) { - switch token { - case "schema": - if header.Schema != nil { - if header.Schema.Ref != "" { - return &Ref{Ref: header.Schema.Ref}, nil - } - return header.Schema.Value, nil - } - case "name": - return header.Name, nil - case "in": - return header.In, nil - case "description": - return header.Description, nil - case "style": - return header.Style, nil - case "explode": - return header.Explode, nil - case "allowEmptyValue": - return header.AllowEmptyValue, nil - case "allowReserved": - return header.AllowReserved, nil - case "deprecated": - return header.Deprecated, nil - case "required": - return header.Required, nil - case "example": - return header.Example, nil - case "examples": - return header.Examples, nil - case "content": - return header.Content, nil - } - - v, _, err := jsonpointer.GetForToken(header.ExtensionProps, token) - return v, err -} diff --git a/openapi3/info.go b/openapi3/info.go index 72076095e..381047fca 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -2,15 +2,14 @@ package openapi3 import ( "context" + "encoding/json" "errors" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Info is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object type Info struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -21,13 +20,44 @@ type Info struct { } // MarshalJSON returns the JSON encoding of Info. -func (info *Info) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(info) +func (info Info) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 6+len(info.Extensions)) + for k, v := range info.Extensions { + m[k] = v + } + m["title"] = info.Title + if x := info.Description; x != "" { + m["description"] = x + } + if x := info.TermsOfService; x != "" { + m["termsOfService"] = x + } + if x := info.Contact; x != nil { + m["contact"] = x + } + if x := info.License; x != nil { + m["license"] = x + } + m["version"] = info.Version + return json.Marshal(m) } // UnmarshalJSON sets Info to a copy of data. func (info *Info) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, info) + type InfoBis Info + var x InfoBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "title") + delete(x.Extensions, "description") + delete(x.Extensions, "termsOfService") + delete(x.Extensions, "contact") + delete(x.Extensions, "license") + delete(x.Extensions, "version") + *info = Info(x) + return nil } // Validate returns an error if Info does not comply with the OpenAPI spec. @@ -54,13 +84,13 @@ func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error return errors.New("value of title must be a non-empty string") } - return nil + return validateExtensions(ctx, info.Extensions) } // Contact is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object type Contact struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -68,47 +98,88 @@ type Contact struct { } // MarshalJSON returns the JSON encoding of Contact. -func (contact *Contact) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(contact) +func (contact Contact) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 3+len(contact.Extensions)) + for k, v := range contact.Extensions { + m[k] = v + } + if x := contact.Name; x != "" { + m["name"] = x + } + if x := contact.URL; x != "" { + m["url"] = x + } + if x := contact.Email; x != "" { + m["email"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Contact to a copy of data. func (contact *Contact) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, contact) + type ContactBis Contact + var x ContactBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "name") + delete(x.Extensions, "url") + delete(x.Extensions, "email") + *contact = Contact(x) + return nil } // Validate returns an error if Contact does not comply with the OpenAPI spec. func (contact *Contact) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) - return nil + return validateExtensions(ctx, contact.Extensions) } // License is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#license-object type License struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` } // MarshalJSON returns the JSON encoding of License. -func (license *License) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(license) +func (license License) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 2+len(license.Extensions)) + for k, v := range license.Extensions { + m[k] = v + } + m["name"] = license.Name + if x := license.URL; x != "" { + m["url"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets License to a copy of data. func (license *License) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, license) + type LicenseBis License + var x LicenseBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "name") + delete(x.Extensions, "url") + *license = License(x) + return nil } // Validate returns an error if License does not comply with the OpenAPI spec. func (license *License) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if license.Name == "" { return errors.New("value of license name must be a non-empty string") } - return nil + + return validateExtensions(ctx, license.Extensions) } diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index dcec43b40..acb83cd0c 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -55,11 +55,16 @@ func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver, par } name := refNameResolver(s.Ref) - if _, ok := doc.Components.Schemas[name]; ok { - s.Ref = "#/components/schemas/" + name - return true + if doc.Components != nil { + if _, ok := doc.Components.Schemas[name]; ok { + s.Ref = "#/components/schemas/" + name + return true + } } + if doc.Components == nil { + doc.Components = &Components{} + } if doc.Components.Schemas == nil { doc.Components.Schemas = make(Schemas) } @@ -220,7 +225,7 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsEx doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal) } } - for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties, s.Items} { + for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties.Schema, s.Items} { isExternal := doc.addSchemaToSpec(ref, refNameResolver, parentIsExternal) if ref != nil { doc.derefSchema(ref.Value, refNameResolver, isExternal || parentIsExternal) @@ -335,44 +340,45 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri refNameResolver = DefaultRefNameResolver } - // Handle components section - names := schemaNames(doc.Components.Schemas) - for _, name := range names { - schema := doc.Components.Schemas[name] - isExternal := doc.addSchemaToSpec(schema, refNameResolver, false) - if schema != nil { - schema.Ref = "" // always dereference the top level - doc.derefSchema(schema.Value, refNameResolver, isExternal) + if components := doc.Components; components != nil { + names := schemaNames(components.Schemas) + for _, name := range names { + schema := components.Schemas[name] + isExternal := doc.addSchemaToSpec(schema, refNameResolver, false) + if schema != nil { + schema.Ref = "" // always dereference the top level + doc.derefSchema(schema.Value, refNameResolver, isExternal) + } } - } - names = parametersMapNames(doc.Components.Parameters) - for _, name := range names { - p := doc.Components.Parameters[name] - isExternal := doc.addParameterToSpec(p, refNameResolver, false) - if p != nil && p.Value != nil { - p.Ref = "" // always dereference the top level - doc.derefParameter(*p.Value, refNameResolver, isExternal) + names = parametersMapNames(components.Parameters) + for _, name := range names { + p := components.Parameters[name] + isExternal := doc.addParameterToSpec(p, refNameResolver, false) + if p != nil && p.Value != nil { + p.Ref = "" // always dereference the top level + doc.derefParameter(*p.Value, refNameResolver, isExternal) + } } - } - doc.derefHeaders(doc.Components.Headers, refNameResolver, false) - for _, req := range doc.Components.RequestBodies { - isExternal := doc.addRequestBodyToSpec(req, refNameResolver, false) - if req != nil && req.Value != nil { - req.Ref = "" // always dereference the top level - doc.derefRequestBody(*req.Value, refNameResolver, isExternal) + doc.derefHeaders(components.Headers, refNameResolver, false) + for _, req := range components.RequestBodies { + isExternal := doc.addRequestBodyToSpec(req, refNameResolver, false) + if req != nil && req.Value != nil { + req.Ref = "" // always dereference the top level + doc.derefRequestBody(*req.Value, refNameResolver, isExternal) + } } - } - doc.derefResponses(doc.Components.Responses, refNameResolver, false) - for _, ss := range doc.Components.SecuritySchemes { - doc.addSecuritySchemeToSpec(ss, refNameResolver, false) - } - doc.derefExamples(doc.Components.Examples, refNameResolver, false) - doc.derefLinks(doc.Components.Links, refNameResolver, false) - for _, cb := range doc.Components.Callbacks { - 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) + doc.derefResponses(components.Responses, refNameResolver, false) + for _, ss := range components.SecuritySchemes { + doc.addSecuritySchemeToSpec(ss, refNameResolver, false) + } + doc.derefExamples(components.Examples, refNameResolver, false) + doc.derefLinks(components.Links, refNameResolver, false) + for _, cb := range components.Callbacks { + 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) + } } } diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go index 93364d0e8..15ea9d48c 100644 --- a/openapi3/issue341_test.go +++ b/openapi3/issue341_test.go @@ -20,7 +20,7 @@ func TestIssue341(t *testing.T) { bs, err := doc.MarshalJSON() require.NoError(t, err) - require.Equal(t, []byte(`{"components":{},"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`), bs) + require.JSONEq(t, `{"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`, string(bs)) require.Equal(t, "string", doc.Paths["/testpath"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) } diff --git a/openapi3/issue376_test.go b/openapi3/issue376_test.go index 22aa7fb40..825f1d1ac 100644 --- a/openapi3/issue376_test.go +++ b/openapi3/issue376_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "context" "fmt" "testing" @@ -42,15 +43,42 @@ info: require.Equal(t, "string", doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type) } +func TestExclusiveValuesOfValuesAdditionalProperties(t *testing.T) { + schema := &Schema{ + AdditionalProperties: AdditionalProperties{ + Has: BoolPtr(false), + Schema: NewSchemaRef("", &Schema{}), + }, + } + err := schema.Validate(context.Background()) + require.ErrorContains(t, err, ` to both `) + + schema = &Schema{ + AdditionalProperties: AdditionalProperties{ + Has: BoolPtr(false), + }, + } + err = schema.Validate(context.Background()) + require.NoError(t, err) + + schema = &Schema{ + AdditionalProperties: AdditionalProperties{ + Schema: NewSchemaRef("", &Schema{}), + }, + } + err = schema.Validate(context.Background()) + require.NoError(t, err) +} + func TestMultijsonTagSerialization(t *testing.T) { - spec := []byte(` + specYAML := []byte(` openapi: 3.0.0 components: schemas: unset: type: number - #empty-object: - # TODO additionalProperties: {} + empty-object: + additionalProperties: {} object: additionalProperties: {type: string} boolean: @@ -61,42 +89,77 @@ info: version: 1.2.3.4 `) - loader := NewLoader() - - doc, err := loader.LoadFromData(spec) - require.NoError(t, err) - - err = doc.Validate(loader.Context) - require.NoError(t, err) - - for propName, propSchema := range doc.Components.Schemas { - ap := propSchema.Value.AdditionalProperties - apa := propSchema.Value.AdditionalPropertiesAllowed - - encoded, err := propSchema.MarshalJSON() - require.NoError(t, err) - require.Equal(t, string(encoded), map[string]string{ - "unset": `{"type":"number"}`, - // TODO: "empty-object":`{"additionalProperties":{}}`, - "object": `{"additionalProperties":{"type":"string"}}`, - "boolean": `{"additionalProperties":false}`, - }[propName]) - - if propName == "unset" { - require.True(t, ap == nil && apa == nil) - continue - } - - apStr := "" - if ap != nil { - apStr = fmt.Sprintf("{Ref:%s Value.Type:%v}", (*ap).Ref, (*ap).Value.Type) - } - apaStr := "" - if apa != nil { - apaStr = fmt.Sprintf("%v", *apa) - } - - require.Truef(t, (ap != nil && apa == nil) || (ap == nil && apa != nil), - "%s: isnil(%s) xor isnil(%s)", propName, apaStr, apStr) + specJSON := []byte(`{ + "openapi": "3.0.0", + "components": { + "schemas": { + "unset": { + "type": "number" + }, + "empty-object": { + "additionalProperties": { + } + }, + "object": { + "additionalProperties": { + "type": "string" + } + }, + "boolean": { + "additionalProperties": false + } + } + }, + "paths": { + }, + "info": { + "title": "An API", + "version": "1.2.3.4" + } +}`) + + for i, spec := range [][]byte{specJSON, specYAML} { + t.Run(fmt.Sprintf("spec%02d", i), func(t *testing.T) { + loader := NewLoader() + + doc, err := loader.LoadFromData(spec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + for propName, propSchema := range doc.Components.Schemas { + t.Run(propName, func(t *testing.T) { + ap := propSchema.Value.AdditionalProperties.Schema + apa := propSchema.Value.AdditionalProperties.Has + + apStr := "" + if ap != nil { + apStr = fmt.Sprintf("{Ref:%s Value.Type:%v}", (*ap).Ref, (*ap).Value.Type) + } + apaStr := "" + if apa != nil { + apaStr = fmt.Sprintf("%v", *apa) + } + + encoded, err := propSchema.MarshalJSON() + require.NoError(t, err) + require.Equal(t, map[string]string{ + "unset": `{"type":"number"}`, + "empty-object": `{"additionalProperties":{}}`, + "object": `{"additionalProperties":{"type":"string"}}`, + "boolean": `{"additionalProperties":false}`, + }[propName], string(encoded)) + + if propName == "unset" { + require.True(t, ap == nil && apa == nil) + return + } + + require.Truef(t, (ap != nil && apa == nil) || (ap == nil && apa != nil), + "%s: isnil(%s) xor isnil(%s)", propName, apaStr, apStr) + }) + } + }) } } diff --git a/openapi3/issue513_test.go b/openapi3/issue513_test.go new file mode 100644 index 000000000..332b9226e --- /dev/null +++ b/openapi3/issue513_test.go @@ -0,0 +1,173 @@ +package openapi3 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue513OKWithExtension(t *testing.T) { + spec := ` +openapi: "3.0.3" +info: + title: 'My app' + version: 1.0.0 + description: 'An API' + +paths: + /v1/operation: + delete: + summary: Delete something + responses: + 200: + description: Success + default: + description: '* **400** - Bad Request' + x-my-extension: {val: ue} + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Error: + type: object + description: An error response body. + properties: + message: + description: A detailed message describing the error. + type: string +`[1:] + sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(sl.Context) + require.NoError(t, err) + data, err := json.Marshal(doc) + require.NoError(t, err) + require.Contains(t, string(data), `x-my-extension`) +} + +func TestIssue513KOHasExtraFieldSchema(t *testing.T) { + spec := ` +openapi: "3.0.3" +info: + title: 'My app' + version: 1.0.0 + description: 'An API' + +paths: + /v1/operation: + delete: + summary: Delete something + responses: + 200: + description: Success + default: + description: '* **400** - Bad Request' + x-my-extension: {val: ue} + # Notice here schema is invalid. It should instead be: + # content: + # application/json: + # schema: + # $ref: '#/components/schemas/Error' + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Error: + type: object + description: An error response body. + properties: + message: + description: A detailed message describing the error. + type: string +`[1:] + sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + require.Contains(t, doc.Paths["/v1/operation"].Delete.Responses["default"].Value.Extensions, `x-my-extension`) + err = doc.Validate(sl.Context) + require.ErrorContains(t, err, `extra sibling fields: [schema]`) +} + +func TestIssue513KOMixesRefAlongWithOtherFields(t *testing.T) { + spec := ` +openapi: "3.0.3" +info: + title: 'My app' + version: 1.0.0 + description: 'An API' + +paths: + /v1/operation: + delete: + summary: Delete something + responses: + 200: + description: A sibling field that the spec says is ignored + $ref: '#/components/responses/SomeResponseBody' +components: + responses: + SomeResponseBody: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Error: + type: object + description: An error response body. + properties: + message: + description: A detailed message describing the error. + type: string +`[1:] + sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(sl.Context) + require.ErrorContains(t, err, `extra sibling fields: [description]`) +} + +func TestIssue513KOMixesRefAlongWithOtherFieldsAllowed(t *testing.T) { + spec := ` +openapi: "3.0.3" +info: + title: 'My app' + version: 1.0.0 + description: 'An API' + +paths: + /v1/operation: + delete: + summary: Delete something + responses: + 200: + description: A sibling field that the spec says is ignored + $ref: '#/components/responses/SomeResponseBody' +components: + responses: + SomeResponseBody: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Error: + type: object + description: An error response body. + properties: + message: + description: A detailed message describing the error. + type: string +`[1:] + sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(sl.Context, AllowExtraSiblingFields("description")) + require.NoError(t, err) +} diff --git a/openapi3/link.go b/openapi3/link.go index 137aef309..08dfa8d67 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -2,12 +2,11 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type Links map[string]*LinkRef @@ -30,7 +29,7 @@ var _ jsonpointer.JSONPointable = (*Links)(nil) // Link is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object type Link struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` @@ -41,18 +40,55 @@ type Link struct { } // MarshalJSON returns the JSON encoding of Link. -func (link *Link) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(link) +func (link Link) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 6+len(link.Extensions)) + for k, v := range link.Extensions { + m[k] = v + } + + if x := link.OperationRef; x != "" { + m["operationRef"] = x + } + if x := link.OperationID; x != "" { + m["operationId"] = x + } + if x := link.Description; x != "" { + m["description"] = x + } + if x := link.Parameters; len(x) != 0 { + m["parameters"] = x + } + if x := link.Server; x != nil { + m["server"] = x + } + if x := link.RequestBody; x != nil { + m["requestBody"] = x + } + + return json.Marshal(m) } // UnmarshalJSON sets Link to a copy of data. func (link *Link) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, link) + type LinkBis Link + var x LinkBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "operationRef") + delete(x.Extensions, "operationId") + delete(x.Extensions, "description") + delete(x.Extensions, "parameters") + delete(x.Extensions, "server") + delete(x.Extensions, "requestBody") + *link = Link(x) + return nil } // Validate returns an error if Link does not comply with the OpenAPI spec. func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if link.OperationID == "" && link.OperationRef == "" { return errors.New("missing operationId or operationRef on link") @@ -60,5 +96,6 @@ func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error if link.OperationID != "" && link.OperationRef != "" { return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", link.OperationID, link.OperationRef) } - return nil + + return validateExtensions(ctx, link.Extensions) } diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go index 978ef8f38..9bcaaf77f 100644 --- a/openapi3/load_cicular_ref_with_external_file_test.go +++ b/openapi3/load_cicular_ref_with_external_file_test.go @@ -53,8 +53,8 @@ func TestLoadCircularRefFromFile(t *testing.T) { Title: "Recursive cyclic refs example", Version: "1.0", }, - Components: openapi3.Components{ - Schemas: map[string]*openapi3.SchemaRef{ + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{ "Foo": foo, "Bar": bar, }, diff --git a/openapi3/loader.go b/openapi3/loader.go index ecc2ef256..72ab8c46a 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -175,6 +175,7 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR func unmarshal(data []byte, v interface{}) error { // See https://github.com/getkin/kin-openapi/issues/680 if err := json.Unmarshal(data, v); err != nil { + // UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys return yaml.Unmarshal(data, v) } return nil @@ -190,54 +191,54 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { loader.resetVisitedPathItemRefs() } - // Visit all components - components := doc.Components - for _, component := range components.Headers { - if err = loader.resolveHeaderRef(doc, component, location); err != nil { - return + if components := doc.Components; components != nil { + for _, component := range components.Headers { + if err = loader.resolveHeaderRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.Parameters { - if err = loader.resolveParameterRef(doc, component, location); err != nil { - return + for _, component := range components.Parameters { + if err = loader.resolveParameterRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.RequestBodies { - if err = loader.resolveRequestBodyRef(doc, component, location); err != nil { - return + for _, component := range components.RequestBodies { + if err = loader.resolveRequestBodyRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.Responses { - if err = loader.resolveResponseRef(doc, component, location); err != nil { - return + for _, component := range components.Responses { + if err = loader.resolveResponseRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.Schemas { - if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil { - return + for _, component := range components.Schemas { + if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil { + return + } } - } - for _, component := range components.SecuritySchemes { - if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil { - return + for _, component := range components.SecuritySchemes { + if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil { + return + } } - } - examples := make([]string, 0, len(components.Examples)) - for name := range components.Examples { - examples = append(examples, name) - } - sort.Strings(examples) - for _, name := range examples { - component := components.Examples[name] - if err = loader.resolveExampleRef(doc, component, location); err != nil { - return + examples := make([]string, 0, len(components.Examples)) + for name := range components.Examples { + examples = append(examples, name) + } + sort.Strings(examples) + for _, name := range examples { + component := components.Examples[name] + if err = loader.resolveExampleRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.Callbacks { - if err = loader.resolveCallbackRef(doc, component, location); err != nil { - return + for _, component := range components.Callbacks { + if err = loader.resolveCallbackRef(doc, component, location); err != nil { + return + } } } @@ -361,10 +362,10 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv 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.AdditionalPropertiesAllowed; ap != nil { + if ap := s.Value.AdditionalProperties.Has; ap != nil { return *ap, nil } - return s.Value.AdditionalProperties, nil + return s.Value.AdditionalProperties.Schema, nil } switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { @@ -390,14 +391,7 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { hasFields := false for i := 0; i < val.NumField(); i++ { hasFields = true - field := val.Type().Field(i) - tagValue := field.Tag.Get("yaml") - yamlKey := strings.Split(tagValue, ",")[0] - if yamlKey == "-" { - tagValue := field.Tag.Get("multijson") - yamlKey = strings.Split(tagValue, ",")[0] - } - if yamlKey == fieldName { + if fieldName == strings.Split(val.Type().Field(i).Tag.Get("yaml"), ",")[0] { return val.Field(i).Interface(), nil } } @@ -407,14 +401,10 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { return drillIntoField(val.FieldByName("Value").Interface(), fieldName) } if hasFields { - if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "ExtensionProps" { - extensions := val.Field(0).Interface().(ExtensionProps).Extensions + if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "Extensions" { + extensions := val.Field(0).Interface().(map[string]interface{}) if enc, ok := extensions[fieldName]; ok { - var dec interface{} - if err := json.Unmarshal(enc.(json.RawMessage), &dec); err != nil { - return nil, err - } - return dec, nil + return enc, nil } } } @@ -757,7 +747,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } } - if v := value.AdditionalProperties; v != nil { + if v := value.AdditionalProperties.Schema; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } @@ -923,7 +913,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen } id := unescapeRefString(rest) - if doc.Components.Callbacks == nil { + if doc.Components == nil || doc.Components.Callbacks == nil { return failedToResolveRefFragmentPart(ref, "callbacks") } resolved := doc.Components.Callbacks[id] diff --git a/openapi3/loader_paths_test.go b/openapi3/loader_paths_test.go index 584f00e85..f7edc7374 100644 --- a/openapi3/loader_paths_test.go +++ b/openapi3/loader_paths_test.go @@ -13,7 +13,6 @@ openapi: "3.0" info: version: "1.0" title: sample -basePath: /adc/v1 paths: PATH: get: diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index e792767fd..3515586a0 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -78,7 +78,7 @@ func ExampleLoader() { } func TestResolveSchemaRef(t *testing.T) { - source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1",description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`) + source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`) loader := NewLoader() doc, err := loader.LoadFromData(source) require.NoError(t, err) @@ -90,15 +90,6 @@ func TestResolveSchemaRef(t *testing.T) { require.NotNil(t, refAVisited.Value) } -func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) { - source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{"/foo":{"post":{"requestBody":{"content":{"application/json":{"schema":null}}}}}}}`) - loader := NewLoader() - doc, err := loader.LoadFromData(source) - require.NoError(t, err) - err = doc.Validate(loader.Context) - require.EqualError(t, err, `invalid paths: invalid path /foo: invalid operation POST: found unresolved ref: ""`) -} - func TestResolveResponseExampleRef(t *testing.T) { source := []byte(` openapi: 3.0.1 diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 090be7657..2a9b4721c 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -2,19 +2,18 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "sort" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) // MediaType is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object type MediaType struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` @@ -65,13 +64,40 @@ func (mediaType *MediaType) WithEncoding(name string, enc *Encoding) *MediaType } // MarshalJSON returns the JSON encoding of MediaType. -func (mediaType *MediaType) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(mediaType) +func (mediaType MediaType) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(mediaType.Extensions)) + for k, v := range mediaType.Extensions { + m[k] = v + } + if x := mediaType.Schema; x != nil { + m["schema"] = x + } + if x := mediaType.Example; x != nil { + m["example"] = x + } + if x := mediaType.Examples; len(x) != 0 { + m["examples"] = x + } + if x := mediaType.Encoding; len(x) != 0 { + m["encoding"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets MediaType to a copy of data. func (mediaType *MediaType) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, mediaType) + type MediaTypeBis MediaType + var x MediaTypeBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "schema") + delete(x.Extensions, "example") + delete(x.Extensions, "examples") + delete(x.Extensions, "encoding") + *mediaType = MediaType(x) + return nil } // Validate returns an error if MediaType does not comply with the OpenAPI spec. @@ -90,35 +116,33 @@ func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOpti return errors.New("example and examples are mutually exclusive") } - if vo := getValidationOptions(ctx); vo.examplesValidationDisabled { - return nil - } - - if example := mediaType.Example; example != nil { - if err := validateExampleValue(ctx, example, schema.Value); err != nil { - return fmt.Errorf("invalid example: %w", err) + if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled { + if example := mediaType.Example; example != nil { + if err := validateExampleValue(ctx, example, schema.Value); err != nil { + return fmt.Errorf("invalid example: %w", err) + } } - } - if examples := mediaType.Examples; examples != nil { - names := make([]string, 0, len(examples)) - for name := range examples { - names = append(names, name) - } - sort.Strings(names) - for _, k := range names { - v := examples[k] - if err := v.Validate(ctx); err != nil { - return fmt.Errorf("example %s: %w", k, err) + if examples := mediaType.Examples; examples != nil { + names := make([]string, 0, len(examples)) + for name := range examples { + names = append(names, name) } - if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil { - return fmt.Errorf("example %s: %w", k, err) + sort.Strings(names) + for _, k := range names { + v := examples[k] + if err := v.Validate(ctx); err != nil { + return fmt.Errorf("example %s: %w", k, err) + } + if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil { + return fmt.Errorf("example %s: %w", k, err) + } } } } } - return nil + return validateExtensions(ctx, mediaType.Extensions) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable @@ -138,6 +162,6 @@ func (mediaType MediaType) JSONLookup(token string) (interface{}, error) { case "encoding": return mediaType.Encoding, nil } - v, _, err := jsonpointer.GetForToken(mediaType.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(mediaType.Extensions, token) return v, err } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 6622ef030..8b8f71bb7 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -2,19 +2,18 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" - - "github.com/getkin/kin-openapi/jsoninfo" ) // T is the root of an OpenAPI v3 document // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#openapi-object type T struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` OpenAPI string `json:"openapi" yaml:"openapi"` // Required - Components Components `json:"components,omitempty" yaml:"components,omitempty"` + Components *Components `json:"components,omitempty" yaml:"components,omitempty"` Info *Info `json:"info" yaml:"info"` // Required Paths Paths `json:"paths" yaml:"paths"` // Required Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` @@ -26,13 +25,50 @@ type T struct { } // MarshalJSON returns the JSON encoding of T. -func (doc *T) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(doc) +func (doc T) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(doc.Extensions)) + for k, v := range doc.Extensions { + m[k] = v + } + m["openapi"] = doc.OpenAPI + if x := doc.Components; x != nil { + m["components"] = x + } + m["info"] = doc.Info + m["paths"] = doc.Paths + if x := doc.Security; len(x) != 0 { + m["security"] = x + } + if x := doc.Servers; len(x) != 0 { + m["servers"] = x + } + if x := doc.Tags; len(x) != 0 { + m["tags"] = x + } + if x := doc.ExternalDocs; x != nil { + m["externalDocs"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets T to a copy of data. func (doc *T) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, doc) + type TBis T + var x TBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "openapi") + delete(x.Extensions, "components") + delete(x.Extensions, "info") + delete(x.Extensions, "paths") + delete(x.Extensions, "security") + delete(x.Extensions, "servers") + delete(x.Extensions, "tags") + delete(x.Extensions, "externalDocs") + *doc = T(x) + return nil } func (doc *T) AddOperation(path string, method string, operation *Operation) { @@ -64,8 +100,10 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { // NOTE: only mention info/components/paths/... key in this func's errors. wrap = func(e error) error { return fmt.Errorf("invalid components: %w", e) } - if err := doc.Components.Validate(ctx); err != nil { - return wrap(err) + if v := doc.Components; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) + } } wrap = func(e error) error { return fmt.Errorf("invalid info: %w", e) } @@ -114,5 +152,5 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { } } - return nil + return validateExtensions(ctx, doc.Extensions) } diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 7736310cc..e01af82ba 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -299,28 +299,28 @@ func spec() *T { }, }, }, - Components: Components{ - Parameters: map[string]*ParameterRef{ + Components: &Components{ + Parameters: ParametersMap{ "someParameter": { Value: parameter, }, }, - RequestBodies: map[string]*RequestBodyRef{ + RequestBodies: RequestBodies{ "someRequestBody": { Value: requestBody, }, }, - Responses: map[string]*ResponseRef{ + Responses: Responses{ "someResponse": { Value: response, }, }, - Schemas: map[string]*SchemaRef{ + Schemas: Schemas{ "someSchema": { Value: schema, }, }, - Headers: map[string]*HeaderRef{ + Headers: Headers{ "someHeader": { Ref: "#/components/headers/otherHeader", }, @@ -328,7 +328,7 @@ func spec() *T { Value: &Header{Parameter{Schema: &SchemaRef{Value: NewStringSchema()}}}, }, }, - Examples: map[string]*ExampleRef{ + Examples: Examples{ "someExample": { Ref: "#/components/examples/otherExample", }, @@ -336,7 +336,7 @@ func spec() *T { Value: NewExample(example), }, }, - SecuritySchemes: map[string]*SecuritySchemeRef{ + SecuritySchemes: SecuritySchemes{ "someSecurityScheme": { Ref: "#/components/securitySchemes/otherSecurityScheme", }, diff --git a/openapi3/operation.go b/openapi3/operation.go index d87704905..645c0805f 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -2,19 +2,18 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "strconv" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object type Operation struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -58,13 +57,70 @@ func NewOperation() *Operation { } // MarshalJSON returns the JSON encoding of Operation. -func (operation *Operation) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(operation) +func (operation Operation) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 12+len(operation.Extensions)) + for k, v := range operation.Extensions { + m[k] = v + } + if x := operation.Tags; len(x) != 0 { + m["tags"] = x + } + if x := operation.Summary; x != "" { + m["summary"] = x + } + if x := operation.Description; x != "" { + m["description"] = x + } + if x := operation.OperationID; x != "" { + m["operationId"] = x + } + if x := operation.Parameters; len(x) != 0 { + m["parameters"] = x + } + if x := operation.RequestBody; x != nil { + m["requestBody"] = x + } + m["responses"] = operation.Responses + if x := operation.Callbacks; len(x) != 0 { + m["callbacks"] = x + } + if x := operation.Deprecated; x { + m["deprecated"] = x + } + if x := operation.Security; x != nil { + m["security"] = x + } + if x := operation.Servers; x != nil { + m["servers"] = x + } + if x := operation.ExternalDocs; x != nil { + m["externalDocs"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Operation to a copy of data. func (operation *Operation) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, operation) + type OperationBis Operation + var x OperationBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "tags") + delete(x.Extensions, "summary") + delete(x.Extensions, "description") + delete(x.Extensions, "operationId") + delete(x.Extensions, "parameters") + delete(x.Extensions, "requestBody") + delete(x.Extensions, "responses") + delete(x.Extensions, "callbacks") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "security") + delete(x.Extensions, "servers") + delete(x.Extensions, "externalDocs") + *operation = Operation(x) + return nil } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable @@ -101,7 +157,7 @@ func (operation Operation) JSONLookup(token string) (interface{}, error) { return operation.ExternalDocs, nil } - v, _, err := jsonpointer.GetForToken(operation.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(operation.Extensions, token) return v, err } @@ -156,5 +212,5 @@ func (operation *Operation) Validate(ctx context.Context, opts ...ValidationOpti } } - return nil + return validateExtensions(ctx, operation.Extensions) } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 04e13b203..ec1893e9a 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -2,14 +2,13 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "sort" "strconv" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type ParametersMap map[string]*ParameterRef @@ -92,7 +91,7 @@ func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOpt // Parameter is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object type Parameter struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` @@ -169,13 +168,80 @@ func (parameter *Parameter) WithSchema(value *Schema) *Parameter { } // MarshalJSON returns the JSON encoding of Parameter. -func (parameter *Parameter) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(parameter) +func (parameter Parameter) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 13+len(parameter.Extensions)) + for k, v := range parameter.Extensions { + m[k] = v + } + + if x := parameter.Name; x != "" { + m["name"] = x + } + if x := parameter.In; x != "" { + m["in"] = x + } + if x := parameter.Description; x != "" { + m["description"] = x + } + if x := parameter.Style; x != "" { + m["style"] = x + } + if x := parameter.Explode; x != nil { + m["explode"] = x + } + if x := parameter.AllowEmptyValue; x { + m["allowEmptyValue"] = x + } + if x := parameter.AllowReserved; x { + m["allowReserved"] = x + } + if x := parameter.Deprecated; x { + m["deprecated"] = x + } + if x := parameter.Required; x { + m["required"] = x + } + if x := parameter.Schema; x != nil { + m["schema"] = x + } + if x := parameter.Example; x != nil { + m["example"] = x + } + if x := parameter.Examples; len(x) != 0 { + m["examples"] = x + } + if x := parameter.Content; len(x) != 0 { + m["content"] = x + } + + return json.Marshal(m) } // UnmarshalJSON sets Parameter to a copy of data. func (parameter *Parameter) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, parameter) + type ParameterBis Parameter + var x ParameterBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, "name") + delete(x.Extensions, "in") + delete(x.Extensions, "description") + delete(x.Extensions, "style") + delete(x.Extensions, "explode") + delete(x.Extensions, "allowEmptyValue") + delete(x.Extensions, "allowReserved") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "required") + delete(x.Extensions, "schema") + delete(x.Extensions, "example") + delete(x.Extensions, "examples") + delete(x.Extensions, "content") + + *parameter = Parameter(x) + return nil } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable @@ -214,7 +280,7 @@ func (parameter Parameter) JSONLookup(token string) (interface{}, error) { return parameter.Content, nil } - v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(parameter.Extensions, token) return v, err } @@ -348,5 +414,5 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti } } - return nil + return validateExtensions(ctx, parameter.Extensions) } diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 5323dc163..fab75d93c 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -2,17 +2,16 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "net/http" "sort" - - "github.com/getkin/kin-openapi/jsoninfo" ) // PathItem is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#path-item-object type PathItem struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -31,13 +30,81 @@ type PathItem struct { } // MarshalJSON returns the JSON encoding of PathItem. -func (pathItem *PathItem) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(pathItem) +func (pathItem PathItem) MarshalJSON() ([]byte, error) { + if ref := pathItem.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 13+len(pathItem.Extensions)) + for k, v := range pathItem.Extensions { + m[k] = v + } + if x := pathItem.Summary; x != "" { + m["summary"] = x + } + if x := pathItem.Description; x != "" { + m["description"] = x + } + if x := pathItem.Connect; x != nil { + m["connect"] = x + } + if x := pathItem.Delete; x != nil { + m["delete"] = x + } + if x := pathItem.Get; x != nil { + m["get"] = x + } + if x := pathItem.Head; x != nil { + m["head"] = x + } + if x := pathItem.Options; x != nil { + m["options"] = x + } + if x := pathItem.Patch; x != nil { + m["patch"] = x + } + if x := pathItem.Post; x != nil { + m["post"] = x + } + if x := pathItem.Put; x != nil { + m["put"] = x + } + if x := pathItem.Trace; x != nil { + m["trace"] = x + } + if x := pathItem.Servers; len(x) != 0 { + m["servers"] = x + } + if x := pathItem.Parameters; len(x) != 0 { + m["parameters"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets PathItem to a copy of data. func (pathItem *PathItem) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, pathItem) + type PathItemBis PathItem + var x PathItemBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + delete(x.Extensions, "summary") + delete(x.Extensions, "description") + delete(x.Extensions, "connect") + delete(x.Extensions, "delete") + delete(x.Extensions, "get") + delete(x.Extensions, "head") + delete(x.Extensions, "options") + delete(x.Extensions, "patch") + delete(x.Extensions, "post") + delete(x.Extensions, "put") + delete(x.Extensions, "trace") + delete(x.Extensions, "servers") + delete(x.Extensions, "parameters") + *pathItem = PathItem(x) + return nil } func (pathItem *PathItem) Operations() map[string]*Operation { @@ -139,5 +206,6 @@ func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption return fmt.Errorf("invalid operation %s: %v", method, err) } } - return nil + + return validateExtensions(ctx, pathItem.Extensions) } diff --git a/openapi3/ref.go b/openapi3/ref.go new file mode 100644 index 000000000..a937de4a5 --- /dev/null +++ b/openapi3/ref.go @@ -0,0 +1,7 @@ +package openapi3 + +// Ref is specified by OpenAPI/Swagger 3.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object +type Ref struct { + Ref string `json:"$ref" yaml:"$ref"` +} diff --git a/openapi3/refs.go b/openapi3/refs.go index d36d562fe..cc9b41a45 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -2,58 +2,88 @@ package openapi3 import ( "context" + "encoding/json" + "fmt" + "sort" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" + "github.com/perimeterx/marshmallow" ) -// Ref is specified by OpenAPI/Swagger 3.0 standard. -// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object -type Ref struct { - Ref string `json:"$ref" yaml:"$ref"` -} - // CallbackRef represents either a Callback or a $ref to a Callback. // When serializing and both fields are set, Ref is preferred over Value. type CallbackRef struct { Ref string Value *Callback + extra []string } var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) // MarshalYAML returns the YAML encoding of CallbackRef. -func (value *CallbackRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x CallbackRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of CallbackRef. -func (value *CallbackRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x CallbackRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return json.Marshal(x.Value) } // UnmarshalJSON sets CallbackRef to a copy of data. -func (value *CallbackRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *CallbackRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if CallbackRef does not comply with the OpenAPI spec. -func (value *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value CallbackRef) JSONLookup(token string) (interface{}, error) { +func (x *CallbackRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -62,41 +92,75 @@ func (value CallbackRef) JSONLookup(token string) (interface{}, error) { type ExampleRef struct { Ref string Value *Example + extra []string } var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) // MarshalYAML returns the YAML encoding of ExampleRef. -func (value *ExampleRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x ExampleRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of ExampleRef. -func (value *ExampleRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x ExampleRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets ExampleRef to a copy of data. -func (value *ExampleRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *ExampleRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if ExampleRef does not comply with the OpenAPI spec. -func (value *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value ExampleRef) JSONLookup(token string) (interface{}, error) { +func (x *ExampleRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -105,41 +169,75 @@ func (value ExampleRef) JSONLookup(token string) (interface{}, error) { type HeaderRef struct { Ref string Value *Header + extra []string } var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) // MarshalYAML returns the YAML encoding of HeaderRef. -func (value *HeaderRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x HeaderRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of HeaderRef. -func (value *HeaderRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x HeaderRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets HeaderRef to a copy of data. -func (value *HeaderRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *HeaderRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if HeaderRef does not comply with the OpenAPI spec. -func (value *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value HeaderRef) JSONLookup(token string) (interface{}, error) { +func (x *HeaderRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -148,30 +246,76 @@ func (value HeaderRef) JSONLookup(token string) (interface{}, error) { type LinkRef struct { Ref string Value *Link + extra []string } +var _ jsonpointer.JSONPointable = (*LinkRef)(nil) + // MarshalYAML returns the YAML encoding of LinkRef. -func (value *LinkRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x LinkRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of LinkRef. -func (value *LinkRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x LinkRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets LinkRef to a copy of data. -func (value *LinkRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *LinkRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if LinkRef does not comply with the OpenAPI spec. -func (value *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) +} + +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable +func (x *LinkRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return x.Ref, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) + return ptr, err } // ParameterRef represents either a Parameter or a $ref to a Parameter. @@ -179,127 +323,229 @@ func (value *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) er type ParameterRef struct { Ref string Value *Parameter + extra []string } var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) // MarshalYAML returns the YAML encoding of ParameterRef. -func (value *ParameterRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x ParameterRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of ParameterRef. -func (value *ParameterRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x ParameterRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets ParameterRef to a copy of data. -func (value *ParameterRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *ParameterRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if ParameterRef does not comply with the OpenAPI spec. -func (value *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value ParameterRef) JSONLookup(token string) (interface{}, error) { +func (x *ParameterRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } -// ResponseRef represents either a Response or a $ref to a Response. +// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody. // When serializing and both fields are set, Ref is preferred over Value. -type ResponseRef struct { +type RequestBodyRef struct { Ref string - Value *Response + Value *RequestBody + extra []string } -var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) +var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) -// MarshalYAML returns the YAML encoding of ResponseRef. -func (value *ResponseRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +// MarshalYAML returns the YAML encoding of RequestBodyRef. +func (x RequestBodyRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } -// MarshalJSON returns the JSON encoding of ResponseRef. -func (value *ResponseRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +// MarshalJSON returns the JSON encoding of RequestBodyRef. +func (x RequestBodyRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } -// UnmarshalJSON sets ResponseRef to a copy of data. -func (value *ResponseRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +// UnmarshalJSON sets RequestBodyRef to a copy of data. +func (x *RequestBodyRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } -// Validate returns an error if ResponseRef does not comply with the OpenAPI spec. -func (value *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) error { +// Validate returns an error if RequestBodyRef does not comply with the OpenAPI spec. +func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value ResponseRef) JSONLookup(token string) (interface{}, error) { +func (x *RequestBodyRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } -// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody. +// ResponseRef represents either a Response or a $ref to a Response. // When serializing and both fields are set, Ref is preferred over Value. -type RequestBodyRef struct { +type ResponseRef struct { Ref string - Value *RequestBody + Value *Response + extra []string } -var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) +var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) -// MarshalYAML returns the YAML encoding of RequestBodyRef. -func (value *RequestBodyRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +// MarshalYAML returns the YAML encoding of ResponseRef. +func (x ResponseRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } -// MarshalJSON returns the JSON encoding of RequestBodyRef. -func (value *RequestBodyRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +// MarshalJSON returns the JSON encoding of ResponseRef. +func (x ResponseRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } -// UnmarshalJSON sets RequestBodyRef to a copy of data. -func (value *RequestBodyRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +// UnmarshalJSON sets ResponseRef to a copy of data. +func (x *ResponseRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } -// Validate returns an error if RequestBodyRef does not comply with the OpenAPI spec. -func (value *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) error { +// Validate returns an error if ResponseRef does not comply with the OpenAPI spec. +func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { +func (x *ResponseRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -308,48 +554,75 @@ func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { type SchemaRef struct { Ref string Value *Schema + extra []string } var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) -func NewSchemaRef(ref string, value *Schema) *SchemaRef { - return &SchemaRef{ - Ref: ref, - Value: value, - } -} - // MarshalYAML returns the YAML encoding of SchemaRef. -func (value *SchemaRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x SchemaRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of SchemaRef. -func (value *SchemaRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x SchemaRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets SchemaRef to a copy of data. -func (value *SchemaRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *SchemaRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if SchemaRef does not comply with the OpenAPI spec. -func (value *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value SchemaRef) JSONLookup(token string) (interface{}, error) { +func (x *SchemaRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -358,48 +631,74 @@ func (value SchemaRef) JSONLookup(token string) (interface{}, error) { type SecuritySchemeRef struct { Ref string Value *SecurityScheme + extra []string } var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) // MarshalYAML returns the YAML encoding of SecuritySchemeRef. -func (value *SecuritySchemeRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x SecuritySchemeRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of SecuritySchemeRef. -func (value *SecuritySchemeRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x SecuritySchemeRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets SecuritySchemeRef to a copy of data. -func (value *SecuritySchemeRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if SecuritySchemeRef does not comply with the OpenAPI spec. -func (value *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + extras = append(extras, ex) + } + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { return v.Validate(ctx) } - return foundUnresolvedRef(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { +func (x *SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } - -// marshalRefYAML returns the YAML encoding of ref values. -func marshalRefYAML(value string, otherwise interface{}) (interface{}, error) { - if value != "" { - return &Ref{Ref: value}, nil - } - return otherwise, nil -} diff --git a/openapi3/request_body.go b/openapi3/request_body.go index f0d9e1ec2..de8919f41 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -2,12 +2,11 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type RequestBodies map[string]*RequestBodyRef @@ -30,7 +29,7 @@ func (r RequestBodies) JSONLookup(token string) (interface{}, error) { // RequestBody is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#request-body-object type RequestBody struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -95,13 +94,36 @@ func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType { } // MarshalJSON returns the JSON encoding of RequestBody. -func (requestBody *RequestBody) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(requestBody) +func (requestBody RequestBody) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 3+len(requestBody.Extensions)) + for k, v := range requestBody.Extensions { + m[k] = v + } + if x := requestBody.Description; x != "" { + m["description"] = requestBody.Description + } + if x := requestBody.Required; x { + m["required"] = x + } + if x := requestBody.Content; true { + m["content"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets RequestBody to a copy of data. func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, requestBody) + type RequestBodyBis RequestBody + var x RequestBodyBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "description") + delete(x.Extensions, "required") + delete(x.Extensions, "content") + *requestBody = RequestBody(x) + return nil } // Validate returns an error if RequestBody does not comply with the OpenAPI spec. @@ -116,5 +138,9 @@ func (requestBody *RequestBody) Validate(ctx context.Context, opts ...Validation vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false } - return requestBody.Content.Validate(ctx) + if err := requestBody.Content.Validate(ctx); err != nil { + return err + } + + return validateExtensions(ctx, requestBody.Extensions) } diff --git a/openapi3/response.go b/openapi3/response.go index 324f77ddc..b85c9145c 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -2,14 +2,13 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "sort" "strconv" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Responses is specified by OpenAPI/Swagger 3.0 standard. @@ -70,7 +69,7 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) { // Response is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object type Response struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -103,13 +102,40 @@ func (response *Response) WithJSONSchemaRef(schema *SchemaRef) *Response { } // MarshalJSON returns the JSON encoding of Response. -func (response *Response) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(response) +func (response Response) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(response.Extensions)) + for k, v := range response.Extensions { + m[k] = v + } + if x := response.Description; x != nil { + m["description"] = x + } + if x := response.Headers; len(x) != 0 { + m["headers"] = x + } + if x := response.Content; len(x) != 0 { + m["content"] = x + } + if x := response.Links; len(x) != 0 { + m["links"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Response to a copy of data. func (response *Response) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, response) + type ResponseBis Response + var x ResponseBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "description") + delete(x.Extensions, "headers") + delete(x.Extensions, "content") + delete(x.Extensions, "links") + *response = Response(x) + return nil } // Validate returns an error if Response does not comply with the OpenAPI spec. @@ -152,5 +178,6 @@ func (response *Response) Validate(ctx context.Context, opts ...ValidationOption return err } } - return nil + + return validateExtensions(ctx, response.Extensions) } diff --git a/openapi3/schema.go b/openapi3/schema.go index d795fb530..9d21c00e6 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -17,8 +17,6 @@ import ( "github.com/go-openapi/jsonpointer" "github.com/mohae/deepcopy" - - "github.com/getkin/kin-openapi/jsoninfo" ) const ( @@ -71,6 +69,14 @@ func Uint64Ptr(value uint64) *uint64 { return &value } +// NewSchemaRef simply builds a SchemaRef +func NewSchemaRef(ref string, value *Schema) *SchemaRef { + return &SchemaRef{ + Ref: ref, + Value: value, + } +} + type Schemas map[string]*SchemaRef var _ jsonpointer.JSONPointable = (*Schemas)(nil) @@ -114,7 +120,7 @@ func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { // Schema is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object type Schema struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` @@ -159,13 +165,57 @@ type Schema struct { Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` // Object - Required []string `json:"required,omitempty" yaml:"required,omitempty"` - Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` - MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` - MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // In this order... - AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // ...for multijson - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` + MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` +} + +type AdditionalProperties struct { + Has *bool + Schema *SchemaRef +} + +// MarshalJSON returns the JSON encoding of AdditionalProperties. +func (addProps AdditionalProperties) MarshalJSON() ([]byte, error) { + if x := addProps.Has; x != nil { + if *x { + return []byte("true"), nil + } + return []byte("false"), nil + } + if x := addProps.Schema; x != nil { + return json.Marshal(x) + } + return nil, nil +} + +// UnmarshalJSON sets AdditionalProperties to a copy of data. +func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error { + var x interface{} + if err := json.Unmarshal(data, &x); err != nil { + return err + } + switch y := x.(type) { + case nil: + case bool: + addProps.Has = &y + case map[string]interface{}: + if len(y) == 0 { + addProps.Schema = &SchemaRef{Value: &Schema{}} + } else { + buf := new(bytes.Buffer) + json.NewEncoder(buf).Encode(y) + if err := json.NewDecoder(buf).Decode(&addProps.Schema); err != nil { + return err + } + } + default: + return errors.New("cannot unmarshal additionalProperties: value must be either a schema object or a boolean") + } + return nil } var _ jsonpointer.JSONPointable = (*Schema)(nil) @@ -175,31 +225,217 @@ func NewSchema() *Schema { } // MarshalJSON returns the JSON encoding of Schema. -func (schema *Schema) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(schema) +func (schema Schema) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 36+len(schema.Extensions)) + for k, v := range schema.Extensions { + m[k] = v + } + + if x := schema.OneOf; len(x) != 0 { + m["oneOf"] = x + } + if x := schema.AnyOf; len(x) != 0 { + m["anyOf"] = x + } + if x := schema.AllOf; len(x) != 0 { + m["allOf"] = x + } + if x := schema.Not; x != nil { + m["not"] = x + } + if x := schema.Type; len(x) != 0 { + m["type"] = x + } + if x := schema.Title; len(x) != 0 { + m["title"] = x + } + if x := schema.Format; len(x) != 0 { + m["format"] = x + } + if x := schema.Description; len(x) != 0 { + m["description"] = x + } + if x := schema.Enum; len(x) != 0 { + m["enum"] = x + } + if x := schema.Default; x != nil { + m["default"] = x + } + if x := schema.Example; x != nil { + m["example"] = x + } + if x := schema.ExternalDocs; x != nil { + m["externalDocs"] = x + } + + // Array-related + if x := schema.UniqueItems; x { + m["uniqueItems"] = x + } + // Number-related + if x := schema.ExclusiveMin; x { + m["exclusiveMinimum"] = x + } + if x := schema.ExclusiveMax; x { + m["exclusiveMaximum"] = x + } + // Properties + if x := schema.Nullable; x { + m["nullable"] = x + } + if x := schema.ReadOnly; x { + m["readOnly"] = x + } + if x := schema.WriteOnly; x { + m["writeOnly"] = x + } + if x := schema.AllowEmptyValue; x { + m["allowEmptyValue"] = x + } + if x := schema.Deprecated; x { + m["deprecated"] = x + } + if x := schema.XML; x != nil { + m["xml"] = x + } + + // Number + if x := schema.Min; x != nil { + m["minimum"] = x + } + if x := schema.Max; x != nil { + m["maximum"] = x + } + if x := schema.MultipleOf; x != nil { + m["multipleOf"] = x + } + + // String + if x := schema.MinLength; x != 0 { + m["minLength"] = x + } + if x := schema.MaxLength; x != nil { + m["maxLength"] = x + } + if x := schema.Pattern; x != "" { + m["pattern"] = x + } + + // Array + if x := schema.MinItems; x != 0 { + m["minItems"] = x + } + if x := schema.MaxItems; x != nil { + m["maxItems"] = x + } + if x := schema.Items; x != nil { + m["items"] = x + } + + // Object + if x := schema.Required; len(x) != 0 { + m["required"] = x + } + if x := schema.Properties; len(x) != 0 { + m["properties"] = x + } + if x := schema.MinProps; x != 0 { + m["minProperties"] = x + } + if x := schema.MaxProps; x != nil { + m["maxProperties"] = x + } + if x := schema.AdditionalProperties; x.Has != nil || x.Schema != nil { + m["additionalProperties"] = &x + } + if x := schema.Discriminator; x != nil { + m["discriminator"] = x + } + + return json.Marshal(m) } // UnmarshalJSON sets Schema to a copy of data. func (schema *Schema) UnmarshalJSON(data []byte) error { - err := jsoninfo.UnmarshalStrictStruct(data, schema) + type SchemaBis Schema + var x SchemaBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, "oneOf") + delete(x.Extensions, "anyOf") + delete(x.Extensions, "allOf") + delete(x.Extensions, "not") + delete(x.Extensions, "type") + delete(x.Extensions, "title") + delete(x.Extensions, "format") + delete(x.Extensions, "description") + delete(x.Extensions, "enum") + delete(x.Extensions, "default") + delete(x.Extensions, "example") + delete(x.Extensions, "externalDocs") + + // Array-related + delete(x.Extensions, "uniqueItems") + // Number-related + delete(x.Extensions, "exclusiveMinimum") + delete(x.Extensions, "exclusiveMaximum") + // Properties + delete(x.Extensions, "nullable") + delete(x.Extensions, "readOnly") + delete(x.Extensions, "writeOnly") + delete(x.Extensions, "allowEmptyValue") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "xml") + + // Number + delete(x.Extensions, "minimum") + delete(x.Extensions, "maximum") + delete(x.Extensions, "multipleOf") + + // String + delete(x.Extensions, "minLength") + delete(x.Extensions, "maxLength") + delete(x.Extensions, "pattern") + + // Array + delete(x.Extensions, "minItems") + delete(x.Extensions, "maxItems") + delete(x.Extensions, "items") + + // Object + delete(x.Extensions, "required") + delete(x.Extensions, "properties") + delete(x.Extensions, "minProperties") + delete(x.Extensions, "maxProperties") + delete(x.Extensions, "additionalProperties") + delete(x.Extensions, "discriminator") + + *schema = Schema(x) + if schema.Format == "date" { // This is a fix for: https://github.com/getkin/kin-openapi/issues/697 if eg, ok := schema.Example.(string); ok { schema.Example = strings.TrimSuffix(eg, "T00:00:00Z") } } - return err + return nil } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (schema Schema) JSONLookup(token string) (interface{}, error) { switch token { case "additionalProperties": - if schema.AdditionalProperties != nil { - if schema.AdditionalProperties.Ref != "" { - return &Ref{Ref: schema.AdditionalProperties.Ref}, nil + if addProps := schema.AdditionalProperties.Has; addProps != nil { + return *addProps, nil + } + if addProps := schema.AdditionalProperties.Schema; addProps != nil { + if addProps.Ref != "" { + return &Ref{Ref: addProps.Ref}, nil } - return schema.AdditionalProperties.Value, nil + return addProps.Value, nil } case "not": if schema.Not != nil { @@ -237,8 +473,6 @@ func (schema Schema) JSONLookup(token string) (interface{}, error) { return schema.Example, nil case "externalDocs": return schema.ExternalDocs, nil - case "additionalPropertiesAllowed": - return schema.AdditionalPropertiesAllowed, nil case "uniqueItems": return schema.UniqueItems, nil case "exclusiveMin": @@ -285,7 +519,7 @@ func (schema Schema) JSONLookup(token string) (interface{}, error) { return schema.Discriminator, nil } - v, _, err := jsonpointer.GetForToken(schema.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(schema.Extensions, token) return v, err } @@ -546,23 +780,19 @@ func (schema *Schema) WithMaxProperties(i int64) *Schema { } func (schema *Schema) WithAnyAdditionalProperties() *Schema { - schema.AdditionalProperties = nil - t := true - schema.AdditionalPropertiesAllowed = &t + schema.AdditionalProperties = AdditionalProperties{Has: BoolPtr(true)} return schema } func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema { - if v == nil { - schema.AdditionalProperties = nil - } else { - schema.AdditionalProperties = &SchemaRef{ - Value: v, - } + schema.AdditionalProperties = AdditionalProperties{} + if v != nil { + schema.AdditionalProperties.Schema = &SchemaRef{Value: v} } return schema } +// IsEmpty tells whether schema is equivalent to the empty schema `{}`. func (schema *Schema) IsEmpty() bool { if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 || schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax || @@ -577,10 +807,10 @@ func (schema *Schema) IsEmpty() bool { if n := schema.Not; n != nil && !n.Value.IsEmpty() { return false } - if ap := schema.AdditionalProperties; ap != nil && !ap.Value.IsEmpty() { + if ap := schema.AdditionalProperties.Schema; ap != nil && !ap.Value.IsEmpty() { return false } - if apa := schema.AdditionalPropertiesAllowed; apa != nil && !*apa { + if apa := schema.AdditionalProperties.Has; apa != nil && *apa { return false } if items := schema.Items; items != nil && !items.Value.IsEmpty() { @@ -615,12 +845,12 @@ func (schema *Schema) Validate(ctx context.Context, opts ...ValidationOption) er return schema.validate(ctx, []*Schema{}) } -func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { +func (schema *Schema) validate(ctx context.Context, stack []*Schema) error { validationOpts := getValidationOptions(ctx) for _, existing := range stack { if existing == schema { - return + return nil } } stack = append(stack, schema) @@ -634,8 +864,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -644,8 +874,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -654,8 +884,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -664,8 +894,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -716,7 +946,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } if schema.Pattern != "" && !validationOpts.schemaPatternValidationDisabled { - if err = schema.compilePattern(); err != nil { + if err := schema.compilePattern(); err != nil { return err } } @@ -734,8 +964,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -750,23 +980,26 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } - if ref := schema.AdditionalProperties; ref != nil { + if schema.AdditionalProperties.Has != nil && schema.AdditionalProperties.Schema != nil { + return errors.New("additionalProperties are set to both boolean and schema") + } + if ref := schema.AdditionalProperties.Schema; ref != nil { v := ref.Value if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } if v := schema.ExternalDocs; v != nil { - if err = v.Validate(ctx); err != nil { + if err := v.Validate(ctx); err != nil { return fmt.Errorf("invalid external docs: %w", err) } } @@ -783,7 +1016,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } - return + return validateExtensions(ctx, schema.Extensions) } func (schema *Schema) IsMatching(value interface{}) bool { @@ -1572,7 +1805,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value // "additionalProperties" var additionalProperties *Schema - if ref := schema.AdditionalProperties; ref != nil { + if ref := schema.AdditionalProperties.Schema; ref != nil { additionalProperties = ref.Value } keys := make([]string, 0, len(value)) @@ -1606,8 +1839,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value continue } } - allowed := schema.AdditionalPropertiesAllowed - if additionalProperties != nil || allowed == nil || *allowed { + if allowed := schema.AdditionalProperties.Has; allowed == nil || *allowed { if additionalProperties != nil { if err := additionalProperties.visitJSON(settings, v); err != nil { if settings.failfast { diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 89971cff0..2bd9848dd 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -801,11 +801,11 @@ var schemaExamples = []schemaExample{ { Schema: &Schema{ Type: "object", - AdditionalProperties: &SchemaRef{ + AdditionalProperties: AdditionalProperties{Schema: &SchemaRef{ Value: &Schema{ Type: "number", }, - }, + }}, }, Serialization: map[string]interface{}{ "type": "object", @@ -828,8 +828,8 @@ var schemaExamples = []schemaExample{ }, { Schema: &Schema{ - Type: "object", - AdditionalPropertiesAllowed: BoolPtr(true), + Type: "object", + AdditionalProperties: AdditionalProperties{Has: BoolPtr(true)}, }, Serialization: map[string]interface{}{ "type": "object", diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index 3f5bd9510..87891c954 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -45,7 +45,7 @@ func (security SecurityRequirement) Authenticate(provider string, scopes ...stri // Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec. func (security *SecurityRequirement) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) return nil } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 83330f24a..f9a08385b 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -2,13 +2,12 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "net/url" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type SecuritySchemes map[string]*SecuritySchemeRef @@ -31,7 +30,7 @@ var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) // SecurityScheme is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object type SecurityScheme struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -71,13 +70,56 @@ func NewJWTSecurityScheme() *SecurityScheme { } // MarshalJSON returns the JSON encoding of SecurityScheme. -func (ss *SecurityScheme) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(ss) +func (ss SecurityScheme) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 8+len(ss.Extensions)) + for k, v := range ss.Extensions { + m[k] = v + } + if x := ss.Type; x != "" { + m["type"] = x + } + if x := ss.Description; x != "" { + m["description"] = x + } + if x := ss.Name; x != "" { + m["name"] = x + } + if x := ss.In; x != "" { + m["in"] = x + } + if x := ss.Scheme; x != "" { + m["scheme"] = x + } + if x := ss.BearerFormat; x != "" { + m["bearerFormat"] = x + } + if x := ss.Flows; x != nil { + m["flows"] = x + } + if x := ss.OpenIdConnectUrl; x != "" { + m["openIdConnectUrl"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets SecurityScheme to a copy of data. func (ss *SecurityScheme) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, ss) + type SecuritySchemeBis SecurityScheme + var x SecuritySchemeBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "type") + delete(x.Extensions, "description") + delete(x.Extensions, "name") + delete(x.Extensions, "in") + delete(x.Extensions, "scheme") + delete(x.Extensions, "bearerFormat") + delete(x.Extensions, "flows") + delete(x.Extensions, "openIdConnectUrl") + *ss = SecurityScheme(x) + return nil } func (ss *SecurityScheme) WithType(value string) *SecurityScheme { @@ -173,13 +215,14 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption } else if ss.Flows != nil { return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type) } - return nil + + return validateExtensions(ctx, ss.Extensions) } // OAuthFlows is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object type OAuthFlows struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -197,13 +240,40 @@ const ( ) // MarshalJSON returns the JSON encoding of OAuthFlows. -func (flows *OAuthFlows) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(flows) +func (flows OAuthFlows) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(flows.Extensions)) + for k, v := range flows.Extensions { + m[k] = v + } + if x := flows.Implicit; x != nil { + m["implicit"] = x + } + if x := flows.Password; x != nil { + m["password"] = x + } + if x := flows.ClientCredentials; x != nil { + m["clientCredentials"] = x + } + if x := flows.AuthorizationCode; x != nil { + m["authorizationCode"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets OAuthFlows to a copy of data. func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, flows) + type OAuthFlowsBis OAuthFlows + var x OAuthFlowsBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "implicit") + delete(x.Extensions, "password") + delete(x.Extensions, "clientCredentials") + delete(x.Extensions, "authorizationCode") + *flows = OAuthFlows(x) + return nil } // Validate returns an error if OAuthFlows does not comply with the OpenAPI spec. @@ -215,48 +285,77 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) return fmt.Errorf("the OAuth flow 'implicit' is invalid: %w", err) } } + if v := flows.Password; v != nil { if err := v.validate(ctx, oAuthFlowTypePassword, opts...); err != nil { return fmt.Errorf("the OAuth flow 'password' is invalid: %w", err) } } + if v := flows.ClientCredentials; v != nil { if err := v.validate(ctx, oAuthFlowTypeClientCredentials, opts...); err != nil { return fmt.Errorf("the OAuth flow 'clientCredentials' is invalid: %w", err) } } + if v := flows.AuthorizationCode; v != nil { if err := v.validate(ctx, oAuthFlowAuthorizationCode, opts...); err != nil { return fmt.Errorf("the OAuth flow 'authorizationCode' is invalid: %w", err) } } - return nil + + return validateExtensions(ctx, flows.Extensions) } // OAuthFlow is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object type OAuthFlow struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes map[string]string `json:"scopes" yaml:"scopes"` + Scopes map[string]string `json:"scopes" yaml:"scopes"` // required } // MarshalJSON returns the JSON encoding of OAuthFlow. -func (flow *OAuthFlow) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(flow) +func (flow OAuthFlow) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(flow.Extensions)) + for k, v := range flow.Extensions { + m[k] = v + } + if x := flow.AuthorizationURL; x != "" { + m["authorizationUrl"] = x + } + if x := flow.TokenURL; x != "" { + m["tokenUrl"] = x + } + if x := flow.RefreshURL; x != "" { + m["refreshUrl"] = x + } + m["scopes"] = flow.Scopes + return json.Marshal(m) } // UnmarshalJSON sets OAuthFlow to a copy of data. func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, flow) + type OAuthFlowBis OAuthFlow + var x OAuthFlowBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "authorizationUrl") + delete(x.Extensions, "tokenUrl") + delete(x.Extensions, "refreshUrl") + delete(x.Extensions, "scopes") + *flow = OAuthFlow(x) + return nil } // Validate returns an error if OAuthFlows does not comply with the OpenAPI spec. func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if v := flow.RefreshURL; v != "" { if _, err := url.Parse(v); err != nil { @@ -268,7 +367,7 @@ func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) e return errors.New("field 'scopes' is empty or missing") } - return nil + return validateExtensions(ctx, flow.Extensions) } func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ...ValidationOption) error { diff --git a/openapi3/server.go b/openapi3/server.go index 587e8e0e1..9fc99f90c 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -2,14 +2,13 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "math" "net/url" "sort" "strings" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Servers is specified by OpenAPI/Swagger standard version 3. @@ -52,9 +51,9 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) // Server is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object type Server struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` - URL string `json:"url" yaml:"url"` + URL string `json:"url" yaml:"url"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` } @@ -84,13 +83,34 @@ func (server *Server) BasePath() (string, error) { } // MarshalJSON returns the JSON encoding of Server. -func (server *Server) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(server) +func (server Server) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 3+len(server.Extensions)) + for k, v := range server.Extensions { + m[k] = v + } + m["url"] = server.URL + if x := server.Description; x != "" { + m["description"] = x + } + if x := server.Variables; len(x) != 0 { + m["variables"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Server to a copy of data. func (server *Server) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, server) + type ServerBis Server + var x ServerBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "url") + delete(x.Extensions, "description") + delete(x.Extensions, "variables") + *server = Server(x) + return nil } func (server Server) ParameterNames() ([]string, error) { @@ -195,13 +215,14 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (e return } } - return + + return validateExtensions(ctx, server.Extensions) } // ServerVariable is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object type ServerVariable struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` @@ -209,18 +230,41 @@ type ServerVariable struct { } // MarshalJSON returns the JSON encoding of ServerVariable. -func (serverVariable *ServerVariable) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(serverVariable) +func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(serverVariable.Extensions)) + for k, v := range serverVariable.Extensions { + m[k] = v + } + if x := serverVariable.Enum; len(x) != 0 { + m["enum"] = x + } + if x := serverVariable.Default; x != "" { + m["default"] = x + } + if x := serverVariable.Description; x != "" { + m["description"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets ServerVariable to a copy of data. func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, serverVariable) + type ServerVariableBis ServerVariable + var x ServerVariableBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "enum") + delete(x.Extensions, "default") + delete(x.Extensions, "description") + *serverVariable = ServerVariable(x) + return nil } // Validate returns an error if ServerVariable does not comply with the OpenAPI spec. func (serverVariable *ServerVariable) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if serverVariable.Default == "" { data, err := serverVariable.MarshalJSON() @@ -229,5 +273,6 @@ func (serverVariable *ServerVariable) Validate(ctx context.Context, opts ...Vali } return fmt.Errorf("field default is required in %s", data) } - return nil + + return validateExtensions(ctx, serverVariable.Extensions) } diff --git a/openapi3/tag.go b/openapi3/tag.go index b5cb7f899..93009a13c 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -2,9 +2,8 @@ package openapi3 import ( "context" + "encoding/json" "fmt" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Tags is specified by OpenAPI/Swagger 3.0 standard. @@ -34,7 +33,7 @@ func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error { // Tag is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object type Tag struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -42,13 +41,36 @@ type Tag struct { } // MarshalJSON returns the JSON encoding of Tag. -func (t *Tag) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(t) +func (t Tag) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 3+len(t.Extensions)) + for k, v := range t.Extensions { + m[k] = v + } + if x := t.Name; x != "" { + m["name"] = x + } + if x := t.Description; x != "" { + m["description"] = x + } + if x := t.ExternalDocs; x != nil { + m["externalDocs"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Tag to a copy of data. func (t *Tag) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, t) + type TagBis Tag + var x TagBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "name") + delete(x.Extensions, "description") + delete(x.Extensions, "externalDocs") + *t = Tag(x) + return nil } // Validate returns an error if Tag does not comply with the OpenAPI spec. @@ -60,5 +82,6 @@ func (t *Tag) Validate(ctx context.Context, opts ...ValidationOption) error { return fmt.Errorf("invalid external docs: %w", err) } } - return nil + + return validateExtensions(ctx, t.Extensions) } diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 343b6836e..0ca12e5ab 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -12,10 +12,23 @@ type ValidationOptions struct { schemaDefaultsValidationDisabled bool schemaFormatValidationEnabled bool schemaPatternValidationDisabled bool + extraSiblingFieldsAllowed map[string]struct{} } type validationOptionsKey struct{} +// AllowExtraSiblingFields called as AllowExtraSiblingFields("description") makes Validate not return an error when said field appears next to a $ref. +func AllowExtraSiblingFields(fields ...string) ValidationOption { + return func(options *ValidationOptions) { + for _, field := range fields { + if options.extraSiblingFieldsAllowed == nil { + options.extraSiblingFieldsAllowed = make(map[string]struct{}, len(fields)) + } + options.extraSiblingFieldsAllowed[field] = struct{}{} + } + } +} + // EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. // By default, schema format validation is disabled. func EnableSchemaFormatValidation() ValidationOption { diff --git a/openapi3/xml.go b/openapi3/xml.go index a55ff410d..34ed3be32 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -2,14 +2,13 @@ package openapi3 import ( "context" - - "github.com/getkin/kin-openapi/jsoninfo" + "encoding/json" ) // XML is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object type XML struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` @@ -19,18 +18,49 @@ type XML struct { } // MarshalJSON returns the JSON encoding of XML. -func (xml *XML) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(xml) +func (xml XML) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 5+len(xml.Extensions)) + for k, v := range xml.Extensions { + m[k] = v + } + if x := xml.Name; x != "" { + m["name"] = x + } + if x := xml.Namespace; x != "" { + m["namespace"] = x + } + if x := xml.Prefix; x != "" { + m["prefix"] = x + } + if x := xml.Attribute; x { + m["attribute"] = x + } + if x := xml.Wrapped; x { + m["wrapped"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets XML to a copy of data. func (xml *XML) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, xml) + type XMLBis XML + var x XMLBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "name") + delete(x.Extensions, "namespace") + delete(x.Extensions, "prefix") + delete(x.Extensions, "attribute") + delete(x.Extensions, "wrapped") + *xml = XML(x) + return nil } // Validate returns an error if XML does not comply with the OpenAPI spec. func (xml *XML) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) - return nil // TODO + return validateExtensions(ctx, xml.Extensions) } diff --git a/openapi3filter/issue707_test.go b/openapi3filter/issue707_test.go index c0dbe6462..a7cbc39ed 100644 --- a/openapi3filter/issue707_test.go +++ b/openapi3filter/issue707_test.go @@ -15,27 +15,26 @@ func TestIssue707(t *testing.T) { loader := openapi3.NewLoader() ctx := loader.Context spec := ` - openapi: 3.0.0 - info: - version: 1.0.0 - title: Sample API - paths: - /items: - get: - description: Returns a list of stuff - parameters: - - description: parameter with a default value - explode: true - in: query - name: param-with-default - schema: - default: 124 - type: integer - style: form - required: false - responses: +openapi: 3.0.0 +info: + version: 1.0.0 + title: Sample API +paths: + /items: + get: + description: Returns a list of stuff + parameters: + - description: parameter with a default value + explode: true + in: query + name: param-with-default + schema: + default: 124 + type: integer + required: false + responses: '200': - description: Successful response + description: Successful response `[1:] doc, err := loader.LoadFromData([]byte(spec)) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 2cd700cd1..9381a27fd 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1125,8 +1125,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S if len(schema.Value.AllOf) > 0 { var exists bool for _, sr := range schema.Value.AllOf { - valueSchema, exists = sr.Value.Properties[name] - if exists { + if valueSchema, exists = sr.Value.Properties[name]; exists { break } } @@ -1137,10 +1136,8 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S // If the property's schema has type "array" it is means that the form contains a few parts with the same name. // Every such part has a type that is defined by an items schema in the property's schema. var exists bool - valueSchema, exists = schema.Value.Properties[name] - if !exists { - anyProperties := schema.Value.AdditionalPropertiesAllowed - if anyProperties != nil { + if valueSchema, exists = schema.Value.Properties[name]; !exists { + if anyProperties := schema.Value.AdditionalProperties.Has; anyProperties != nil { switch *anyProperties { case true: //additionalProperties: true @@ -1150,11 +1147,10 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } } - if schema.Value.AdditionalProperties == nil { + if schema.Value.AdditionalProperties.Schema == nil { return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } - valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name] - if !exists { + if valueSchema, exists = schema.Value.AdditionalProperties.Schema.Value.Properties[name]; !exists { return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } } @@ -1179,8 +1175,8 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S for k, v := range sr.Value.Properties { allTheProperties[k] = v } - if sr.Value.AdditionalProperties != nil { - for k, v := range sr.Value.AdditionalProperties.Value.Properties { + if addProps := sr.Value.AdditionalProperties.Schema; addProps != nil { + for k, v := range addProps.Value.Properties { allTheProperties[k] = v } } @@ -1189,8 +1185,8 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S for k, v := range schema.Value.Properties { allTheProperties[k] = v } - if schema.Value.AdditionalProperties != nil { - for k, v := range schema.Value.AdditionalProperties.Value.Properties { + if addProps := schema.Value.AdditionalProperties.Schema; addProps != nil { + for k, v := range addProps.Value.Properties { allTheProperties[k] = v } } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index a61c57a09..7245cbe03 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -346,10 +346,6 @@ func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationI // validateSecurityRequirement validates a single OpenAPI 3 security requirement func validateSecurityRequirement(ctx context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error { - doc := input.Route.Spec - securitySchemes := doc.Components.SecuritySchemes - - // Ensure deterministic order names := make([]string, 0, len(securityRequirement)) for name := range securityRequirement { names = append(names, name) @@ -366,6 +362,11 @@ func validateSecurityRequirement(ctx context.Context, input *RequestValidationIn return ErrAuthenticationServiceMissing } + var securitySchemes openapi3.SecuritySchemes + if components := input.Route.Spec.Components; components != nil { + securitySchemes = components.SecuritySchemes + } + // For each scheme for the requirement for _, name := range names { var securityScheme *openapi3.SecurityScheme diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index cdbeb1262..d3a1b45bb 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -541,7 +541,7 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test securitySchemes[1].Name: {}, }, }, - Components: openapi3.Components{ + Components: &openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } @@ -670,7 +670,7 @@ func TestAnySecurityRequirementMet(t *testing.T) { Version: "0.1", }, Paths: map[string]*openapi3.PathItem{}, - Components: openapi3.Components{ + Components: &openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } @@ -767,7 +767,7 @@ func TestAllSchemesMet(t *testing.T) { Version: "0.1", }, Paths: map[string]*openapi3.PathItem{}, - Components: openapi3.Components{ + Components: &openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } diff --git a/jsoninfo/field_info.go b/openapi3gen/field_info.go similarity index 78% rename from jsoninfo/field_info.go rename to openapi3gen/field_info.go index 6b45f8c69..13f5ba048 100644 --- a/jsoninfo/field_info.go +++ b/openapi3gen/field_info.go @@ -1,4 +1,4 @@ -package jsoninfo +package openapi3gen import ( "reflect" @@ -7,9 +7,8 @@ import ( "unicode/utf8" ) -// FieldInfo contains information about JSON serialization of a field. -type FieldInfo struct { - MultipleFields bool // Whether multiple Go fields share this JSON name +// theFieldInfo contains information about JSON serialization of a field. +type theFieldInfo struct { HasJSONTag bool TypeIsMarshaller bool TypeIsUnmarshaller bool @@ -20,7 +19,7 @@ type FieldInfo struct { JSONName string } -func AppendFields(fields []FieldInfo, parentIndex []int, t reflect.Type) []FieldInfo { +func appendFields(fields []theFieldInfo, parentIndex []int, t reflect.Type) []theFieldInfo { if t.Kind() == reflect.Ptr { t = t.Elem() } @@ -40,7 +39,7 @@ iteration: continue } if jsonTag == "" { - fields = AppendFields(fields, index, f.Type) + fields = appendFields(fields, index, f.Type) continue iteration } } @@ -58,7 +57,7 @@ iteration: } // Declare a field - field := FieldInfo{ + field := theFieldInfo{ Index: index, Type: f.Type, JSONName: f.Name, @@ -67,13 +66,6 @@ iteration: // Read "json" tag jsonTag := f.Tag.Get("json") - // Read our custom "multijson" tag that - // allows multiple fields with the same name. - if v := f.Tag.Get("multijson"); v != "" { - field.MultipleFields = true - jsonTag = v - } - // Handle "-" if jsonTag == "-" { continue @@ -108,7 +100,7 @@ iteration: return fields } -type sortableFieldInfos []FieldInfo +type sortableFieldInfos []theFieldInfo func (list sortableFieldInfos) Len() int { return len(list) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 4387727f7..eccabd85d 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" ) @@ -119,7 +118,7 @@ func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Sch return ref, nil } -func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { +func (g *Generator) generateSchemaRefFor(parents []*theTypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { if ref := g.Types[t]; ref != nil && g.opts.schemaCustomizer == nil { g.SchemaRefs[ref]++ return ref, nil @@ -139,7 +138,7 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect return ref, nil } -func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.StructField { +func getStructField(t reflect.Type, fieldInfo theFieldInfo) reflect.StructField { var ff reflect.StructField // fieldInfo.Index is an array of indexes starting from the root of the type for i := 0; i < len(fieldInfo.Index); i++ { @@ -152,8 +151,8 @@ func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.Struct return ff } -func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { - typeInfo := jsoninfo.GetTypeInfo(t) +func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { + typeInfo := getTypeInfo(t) for _, parent := range parents { if parent == typeInfo { return nil, &CycleError{} @@ -161,7 +160,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } if cap(parents) == 0 { - parents = make([]*jsoninfo.TypeInfo, 0, 4) + parents = make([]*theTypeInfo, 0, 4) } parents = append(parents, typeInfo) @@ -284,7 +283,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } if additionalProperties != nil { g.SchemaRefs[additionalProperties]++ - schema.AdditionalProperties = additionalProperties + schema.AdditionalProperties = openapi3.AdditionalProperties{Schema: additionalProperties} } case reflect.Struct: @@ -374,7 +373,7 @@ func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Sche ref := g.generateCycleSchemaRef(t.Elem(), schema) mapSchema := openapi3.NewSchema() mapSchema.Type = "object" - mapSchema.AdditionalProperties = ref + mapSchema.AdditionalProperties = openapi3.AdditionalProperties{Schema: ref} return openapi3.NewSchemaRef("", mapSchema) default: typeName = t.Name() diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index bfa3120ec..9a143e415 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -379,7 +379,7 @@ func TestCyclicReferences(t *testing.T) { require.NotNil(t, schemaRef.Value.Properties["MapCycle"]) require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) - require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) + require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Schema.Ref) } func ExampleSchemaCustomizer() { diff --git a/openapi3gen/type_info.go b/openapi3gen/type_info.go new file mode 100644 index 000000000..062882b4c --- /dev/null +++ b/openapi3gen/type_info.go @@ -0,0 +1,54 @@ +package openapi3gen + +import ( + "reflect" + "sort" + "sync" +) + +var ( + typeInfos = map[reflect.Type]*theTypeInfo{} + typeInfosMutex sync.RWMutex +) + +// theTypeInfo contains information about JSON serialization of a type +type theTypeInfo struct { + Type reflect.Type + Fields []theFieldInfo +} + +// getTypeInfo returns theTypeInfo for the given type. +func getTypeInfo(t reflect.Type) *theTypeInfo { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + typeInfosMutex.RLock() + typeInfo, exists := typeInfos[t] + typeInfosMutex.RUnlock() + if exists { + return typeInfo + } + if t.Kind() != reflect.Struct { + typeInfo = &theTypeInfo{ + Type: t, + } + } else { + // Allocate + typeInfo = &theTypeInfo{ + Type: t, + Fields: make([]theFieldInfo, 0, 16), + } + + // Add fields + typeInfo.Fields = appendFields(nil, nil, t) + + // Sort fields + sort.Sort(sortableFieldInfos(typeInfo.Fields)) + } + + // Publish + typeInfosMutex.Lock() + typeInfos[t] = typeInfo + typeInfosMutex.Unlock() + return typeInfo +} diff --git a/refs.sh b/refs.sh new file mode 100755 index 000000000..bbcbe54bc --- /dev/null +++ b/refs.sh @@ -0,0 +1,125 @@ +#!/bin/bash -eux +set -o pipefail + +types=() +types+=("Callback") +types+=("Example") +types+=("Header") +types+=("Link") +types+=("Parameter") +types+=("RequestBody") +types+=("Response") +types+=("Schema") +types+=("SecurityScheme") + +cat < Date: Wed, 4 Jan 2023 22:08:39 +0900 Subject: [PATCH 239/260] openapi3filter: RegisterBodyDecoder for application/zip (#730) --- openapi3filter/req_resp_decoder.go | 62 ++++++++++++- openapi3filter/zip_file_upload_test.go | 116 +++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 openapi3filter/zip_file_upload_test.go diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 9381a27fd..2e374d4c4 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1,6 +1,8 @@ package openapi3filter import ( + "archive/zip" + "bytes" "encoding/json" "errors" "fmt" @@ -1004,15 +1006,16 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, } func init() { - RegisterBodyDecoder("text/plain", plainBodyDecoder) RegisterBodyDecoder("application/json", jsonBodyDecoder) RegisterBodyDecoder("application/json-patch+json", jsonBodyDecoder) - RegisterBodyDecoder("application/x-yaml", yamlBodyDecoder) - RegisterBodyDecoder("application/yaml", yamlBodyDecoder) + RegisterBodyDecoder("application/octet-stream", FileBodyDecoder) RegisterBodyDecoder("application/problem+json", jsonBodyDecoder) RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder) + RegisterBodyDecoder("application/x-yaml", yamlBodyDecoder) + RegisterBodyDecoder("application/yaml", yamlBodyDecoder) + RegisterBodyDecoder("application/zip", ZipFileBodyDecoder) RegisterBodyDecoder("multipart/form-data", multipartBodyDecoder) - RegisterBodyDecoder("application/octet-stream", FileBodyDecoder) + RegisterBodyDecoder("text/plain", plainBodyDecoder) } func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { @@ -1217,3 +1220,54 @@ func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema } return string(data), nil } + +// ZipFileBodyDecoder is a body decoder that decodes a zip file body to a string. +func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + buff := bytes.NewBuffer([]byte{}) + size, err := io.Copy(buff, body) + if err != nil { + return nil, err + } + + zr, err := zip.NewReader(bytes.NewReader(buff.Bytes()), size) + if err != nil { + return nil, err + } + + const bufferSize = 256 + content := make([]byte, 0, bufferSize*len(zr.File)) + buffer := make([]byte, bufferSize) + + for _, f := range zr.File { + err := func() error { + rc, err := f.Open() + if err != nil { + return err + } + defer func() { + _ = rc.Close() + }() + + for { + n, err := rc.Read(buffer) + if 0 < n { + content = append(content, buffer...) + } + if err == io.EOF { + break + } + if err != nil { + return err + } + } + + return nil + }() + + if err != nil { + return nil, err + } + } + + return string(content), nil +} diff --git a/openapi3filter/zip_file_upload_test.go b/openapi3filter/zip_file_upload_test.go new file mode 100644 index 000000000..69c6419cc --- /dev/null +++ b/openapi3filter/zip_file_upload_test.go @@ -0,0 +1,116 @@ +package openapi3filter_test + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestValidateZipFileUpload(t *testing.T) { + const spec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: 0.0.1 +paths: + /test: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + responses: + '200': + description: Created +` + + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + tests := []struct { + zipData []byte + wantErr bool + }{ + { + []byte{ + 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x7d, 0x23, 0x56, 0xcd, 0xfd, 0x67, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x09, 0x00, 0x1c, 0x00, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x74, 0x78, 0x74, 0x55, 0x54, 0x09, 0x00, 0x03, 0xac, 0xce, 0xb3, 0x63, 0xaf, 0xce, 0xb3, 0x63, 0x75, 0x78, 0x0b, 0x00, 0x01, 0x04, 0xf7, 0x01, 0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x0a, 0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x7d, 0x23, 0x56, 0xcd, 0xfd, 0x67, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x09, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x00, 0x00, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x74, 0x78, 0x74, 0x55, 0x54, 0x05, 0x00, 0x03, 0xac, 0xce, 0xb3, 0x63, 0x75, 0x78, 0x0b, 0x00, 0x01, 0x04, 0xf7, 0x01, 0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + false, + }, + { + []byte{ + 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, // No entry + true, + }, + } + for _, tt := range tests { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + { // Add file data + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="file"; filename="hello.zip"`) + h.Set("Content-Type", "application/zip") + + fw, err := writer.CreatePart(h) + require.NoError(t, err) + _, err = io.Copy(fw, bytes.NewReader(tt.zipData)) + + require.NoError(t, err) + } + + writer.Close() + + req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes())) + require.NoError(t, err) + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + if err = openapi3filter.ValidateRequestBody( + context.Background(), + &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + }, + route.Operation.RequestBody.Value, + ); err != nil { + if !tt.wantErr { + t.Errorf("got %v", err) + } + continue + } + if tt.wantErr { + t.Errorf("want err") + } + } +} From e7d649f3f7d6ddbaaaed74a7d2f819a82118aab4 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 4 Jan 2023 17:23:35 +0100 Subject: [PATCH 240/260] Keep track of API changes with CI (#732) --- .github/docs/openapi2.txt | 9 ++ .github/docs/openapi2conv.txt | 25 +++ .github/docs/openapi3.txt | 153 ++++++++++++++++++ .github/docs/openapi3filter.txt | 55 +++++++ .github/docs/openapi3filter_fixtures.txt | 0 .github/docs/openapi3gen.txt | 11 ++ .github/docs/routers.txt | 5 + .github/docs/routers_gorillamux.txt | 2 + .github/docs/routers_legacy.txt | 3 + .github/docs/routers_legacy_pathpattern.txt | 9 ++ .github/workflows/go.yml | 3 + docs.sh | 25 +++ openapi3filter/req_resp_decoder.go | 2 +- .../{ => testdata}/fixtures/petstore.json | 0 openapi3filter/validation_error_test.go | 6 +- 15 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 .github/docs/openapi2.txt create mode 100644 .github/docs/openapi2conv.txt create mode 100644 .github/docs/openapi3.txt create mode 100644 .github/docs/openapi3filter.txt create mode 100644 .github/docs/openapi3filter_fixtures.txt create mode 100644 .github/docs/openapi3gen.txt create mode 100644 .github/docs/routers.txt create mode 100644 .github/docs/routers_gorillamux.txt create mode 100644 .github/docs/routers_legacy.txt create mode 100644 .github/docs/routers_legacy_pathpattern.txt create mode 100755 docs.sh rename openapi3filter/{ => testdata}/fixtures/petstore.json (100%) diff --git a/.github/docs/openapi2.txt b/.github/docs/openapi2.txt new file mode 100644 index 000000000..e2ac28b20 --- /dev/null +++ b/.github/docs/openapi2.txt @@ -0,0 +1,9 @@ +type Header struct{ ... } +type Operation struct{ ... } +type Parameter struct{ ... } +type Parameters []*Parameter +type PathItem struct{ ... } +type Response struct{ ... } +type SecurityRequirements []map[string][]string +type SecurityScheme struct{ ... } +type T struct{ ... } diff --git a/.github/docs/openapi2conv.txt b/.github/docs/openapi2conv.txt new file mode 100644 index 000000000..e7c1db1bc --- /dev/null +++ b/.github/docs/openapi2conv.txt @@ -0,0 +1,25 @@ +func FromV3(doc3 *openapi3.T) (*openapi2.T, error) +func FromV3Headers(defs openapi3.Headers, components *openapi3.Components) (map[string]*openapi2.Header, error) +func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2.Operation, error) +func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components) (*openapi2.Parameter, error) +func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) +func FromV3Ref(ref string) string +func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, ...) (*openapi2.Parameter, error) +func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameters +func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error) +func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) +func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) +func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) +func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements +func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) +func ToV3(doc2 *openapi2.T) (*openapi3.T, error) +func ToV3Headers(defs map[string]*openapi2.Header) openapi3.Headers +func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, ...) (*openapi3.Operation, error) +func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter, ...) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, ...) +func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, ...) (*openapi3.PathItem, error) +func ToV3Ref(ref string) string +func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error) +func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef +func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef +func ToV3SecurityRequirements(requirements openapi2.SecurityRequirements) openapi3.SecurityRequirements +func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.SecuritySchemeRef, error) diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt new file mode 100644 index 000000000..28819d73a --- /dev/null +++ b/.github/docs/openapi3.txt @@ -0,0 +1,153 @@ +const ParameterInPath = "path" ... +const TypeArray = "array" ... +const FormatOfStringForUUIDOfRFC4122 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$` ... +const SerializationSimple = "simple" ... +var SchemaErrorDetailsDisabled = false ... +var CircularReferenceCounter = 3 +var CircularReferenceError = "kin-openapi bug found: circular schema reference not handled" +var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile)) +var ErrURINotSupported = errors.New("unsupported URI") +var IdentifierRegExp = regexp.MustCompile(identifierPattern) +var SchemaStringFormats = make(map[string]Format, 4) +func BoolPtr(value bool) *bool +func DefaultRefNameResolver(ref string) string +func DefineIPv4Format() +func DefineIPv6Format() +func DefineStringFormat(name string, pattern string) +func DefineStringFormatCallback(name string, callback FormatCallback) +func Float64Ptr(value float64) *float64 +func Int64Ptr(value int64) *int64 +func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) +func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) +func Uint64Ptr(value uint64) *uint64 +func ValidateIdentifier(value string) error +func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context +type AdditionalProperties struct{ ... } +type Callback map[string]*PathItem +type CallbackRef struct{ ... } +type Callbacks map[string]*CallbackRef +type Components struct{ ... } + func NewComponents() Components +type Contact struct{ ... } +type Content map[string]*MediaType + func NewContent() Content + func NewContentWithFormDataSchema(schema *Schema) Content + func NewContentWithFormDataSchemaRef(schema *SchemaRef) Content + func NewContentWithJSONSchema(schema *Schema) Content + func NewContentWithJSONSchemaRef(schema *SchemaRef) Content + func NewContentWithSchema(schema *Schema, consumes []string) Content + func NewContentWithSchemaRef(schema *SchemaRef, consumes []string) Content +type Discriminator struct{ ... } +type Encoding struct{ ... } + func NewEncoding() *Encoding +type Example struct{ ... } + func NewExample(value interface{}) *Example +type ExampleRef struct{ ... } +type Examples map[string]*ExampleRef +type ExternalDocs struct{ ... } +type Format struct{ ... } +type FormatCallback func(value string) error +type Header struct{ ... } +type HeaderRef struct{ ... } +type Headers map[string]*HeaderRef +type Info struct{ ... } +type License struct{ ... } +type Link struct{ ... } +type LinkRef struct{ ... } +type Links map[string]*LinkRef +type Loader struct{ ... } + func NewLoader() *Loader +type MediaType struct{ ... } + func NewMediaType() *MediaType +type MultiError []error +type OAuthFlow struct{ ... } +type OAuthFlows struct{ ... } +type Operation struct{ ... } + func NewOperation() *Operation +type Parameter struct{ ... } + func NewCookieParameter(name string) *Parameter + func NewHeaderParameter(name string) *Parameter + func NewPathParameter(name string) *Parameter + func NewQueryParameter(name string) *Parameter +type ParameterRef struct{ ... } +type Parameters []*ParameterRef + func NewParameters() Parameters +type ParametersMap map[string]*ParameterRef +type PathItem struct{ ... } +type Paths map[string]*PathItem +type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error) + func ReadFromHTTP(cl *http.Client) ReadFromURIFunc + func ReadFromURIs(readers ...ReadFromURIFunc) ReadFromURIFunc + func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc +type Ref struct{ ... } +type RefNameResolver func(string) string +type RequestBodies map[string]*RequestBodyRef +type RequestBody struct{ ... } + func NewRequestBody() *RequestBody +type RequestBodyRef struct{ ... } +type Response struct{ ... } + func NewResponse() *Response +type ResponseRef struct{ ... } +type Responses map[string]*ResponseRef + func NewResponses() Responses +type Schema struct{ ... } + func NewAllOfSchema(schemas ...*Schema) *Schema + func NewAnyOfSchema(schemas ...*Schema) *Schema + func NewArraySchema() *Schema + func NewBoolSchema() *Schema + func NewBytesSchema() *Schema + func NewDateTimeSchema() *Schema + func NewFloat64Schema() *Schema + func NewInt32Schema() *Schema + func NewInt64Schema() *Schema + func NewIntegerSchema() *Schema + func NewObjectSchema() *Schema + func NewOneOfSchema(schemas ...*Schema) *Schema + func NewSchema() *Schema + func NewStringSchema() *Schema + func NewUUIDSchema() *Schema +type SchemaError struct{ ... } +type SchemaRef struct{ ... } + func NewSchemaRef(ref string, value *Schema) *SchemaRef +type SchemaRefs []*SchemaRef +type SchemaValidationOption func(*schemaValidationSettings) + func DefaultsSet(f func()) SchemaValidationOption + func DisablePatternValidation() SchemaValidationOption + func EnableFormatValidation() SchemaValidationOption + func FailFast() SchemaValidationOption + func MultiErrors() SchemaValidationOption + func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaValidationOption + func VisitAsRequest() SchemaValidationOption + func VisitAsResponse() SchemaValidationOption +type Schemas map[string]*SchemaRef +type SecurityRequirement map[string][]string + func NewSecurityRequirement() SecurityRequirement +type SecurityRequirements []SecurityRequirement + func NewSecurityRequirements() *SecurityRequirements +type SecurityScheme struct{ ... } + func NewCSRFSecurityScheme() *SecurityScheme + func NewJWTSecurityScheme() *SecurityScheme + func NewOIDCSecurityScheme(oidcUrl string) *SecurityScheme + func NewSecurityScheme() *SecurityScheme +type SecuritySchemeRef struct{ ... } +type SecuritySchemes map[string]*SecuritySchemeRef +type SerializationMethod struct{ ... } +type Server struct{ ... } +type ServerVariable struct{ ... } +type Servers []*Server +type SliceUniqueItemsChecker func(items []interface{}) bool +type T struct{ ... } +type Tag struct{ ... } +type Tags []*Tag +type ValidationOption func(options *ValidationOptions) + func AllowExtraSiblingFields(fields ...string) ValidationOption + func DisableExamplesValidation() ValidationOption + func DisableSchemaDefaultsValidation() ValidationOption + func DisableSchemaFormatValidation() ValidationOption + func DisableSchemaPatternValidation() ValidationOption + func EnableExamplesValidation() ValidationOption + func EnableSchemaDefaultsValidation() ValidationOption + func EnableSchemaFormatValidation() ValidationOption + func EnableSchemaPatternValidation() ValidationOption +type ValidationOptions struct{ ... } +type XML struct{ ... } diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt new file mode 100644 index 000000000..094da23ea --- /dev/null +++ b/.github/docs/openapi3filter.txt @@ -0,0 +1,55 @@ +const ErrCodeOK = 0 ... +var DefaultOptions = &Options{} +var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc") +var ErrInvalidEmptyValue = errors.New("empty value is not allowed") +var ErrInvalidRequired = errors.New("value is required but missing") +var JSONPrefixes = []string{ ... } +func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter) +func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, ...) (interface{}, error) +func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error +func RegisterBodyDecoder(contentType string, decoder BodyDecoder) +func RegisterBodyEncoder(contentType string, encoder BodyEncoder) +func TrimJSONPrefix(data []byte) []byte +func UnregisterBodyDecoder(contentType string) +func UnregisterBodyEncoder(contentType string) +func ValidateParameter(ctx context.Context, input *RequestValidationInput, ...) error +func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error) +func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, ...) error +func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error +func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, ...) error +func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, ...) (interface{}, error) +type AuthenticationFunc func(context.Context, *AuthenticationInput) error +type AuthenticationInput struct{ ... } +type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error) + func RegisteredBodyDecoder(contentType string) BodyDecoder +type BodyEncoder func(body interface{}) ([]byte, error) + func RegisteredBodyEncoder(contentType string) BodyEncoder +type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) +type CustomSchemaErrorFunc func(err *openapi3.SchemaError) string +type EncodingFn func(partName string) *openapi3.Encoding +type ErrCode int +type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error) +type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter) +type Headerer interface{ ... } +type LogFunc func(message string, err error) +type Options struct{ ... } +type ParseError struct{ ... } +type ParseErrorKind int + const KindOther ParseErrorKind = iota ... +type RequestError struct{ ... } +type RequestValidationInput struct{ ... } +type ResponseError struct{ ... } +type ResponseValidationInput struct{ ... } +type SecurityRequirementsError struct{ ... } +type StatusCoder interface{ ... } +type ValidationError struct{ ... } +type ValidationErrorEncoder struct{ ... } +type ValidationErrorSource struct{ ... } +type ValidationHandler struct{ ... } +type Validator struct{ ... } + func NewValidator(router routers.Router, options ...ValidatorOption) *Validator +type ValidatorOption func(*Validator) + func OnErr(f ErrFunc) ValidatorOption + func OnLog(f LogFunc) ValidatorOption + func Strict(strict bool) ValidatorOption + func ValidationOptions(options Options) ValidatorOption diff --git a/.github/docs/openapi3filter_fixtures.txt b/.github/docs/openapi3filter_fixtures.txt new file mode 100644 index 000000000..e69de29bb diff --git a/.github/docs/openapi3gen.txt b/.github/docs/openapi3gen.txt new file mode 100644 index 000000000..741a5043f --- /dev/null +++ b/.github/docs/openapi3gen.txt @@ -0,0 +1,11 @@ +var RefSchemaRef = openapi3.NewSchemaRef("Ref", ...) +func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) +type CycleError struct{} +type ExcludeSchemaSentinel struct{} +type Generator struct{ ... } + func NewGenerator(opts ...Option) *Generator +type Option func(*generatorOpt) + func SchemaCustomizer(sc SchemaCustomizerFn) Option + func ThrowErrorOnCycle() Option + func UseAllExportedFields() Option +type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error diff --git a/.github/docs/routers.txt b/.github/docs/routers.txt new file mode 100644 index 000000000..fdd5a9dda --- /dev/null +++ b/.github/docs/routers.txt @@ -0,0 +1,5 @@ +var ErrMethodNotAllowed error = &RouteError{ ... } +var ErrPathNotFound error = &RouteError{ ... } +type Route struct{ ... } +type RouteError struct{ ... } +type Router interface{ ... } diff --git a/.github/docs/routers_gorillamux.txt b/.github/docs/routers_gorillamux.txt new file mode 100644 index 000000000..82aad106d --- /dev/null +++ b/.github/docs/routers_gorillamux.txt @@ -0,0 +1,2 @@ +func NewRouter(doc *openapi3.T) (routers.Router, error) +type Router struct{ ... } diff --git a/.github/docs/routers_legacy.txt b/.github/docs/routers_legacy.txt new file mode 100644 index 000000000..71017a6c1 --- /dev/null +++ b/.github/docs/routers_legacy.txt @@ -0,0 +1,3 @@ +func NewRouter(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error) +type Router struct{ ... } +type Routers []*Router diff --git a/.github/docs/routers_legacy_pathpattern.txt b/.github/docs/routers_legacy_pathpattern.txt new file mode 100644 index 000000000..7ad87f2b5 --- /dev/null +++ b/.github/docs/routers_legacy_pathpattern.txt @@ -0,0 +1,9 @@ +const SuffixKindConstant = SuffixKind(iota) ... +var DefaultOptions = &Options{ ... } +func EqualSuffix(a, b Suffix) bool +func PathFromHost(host string, specialDashes bool) string +type Node struct{ ... } +type Options struct{ ... } +type Suffix struct{ ... } +type SuffixKind int +type SuffixList []Suffix diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index dab35cb89..b38f10fba 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -57,6 +57,9 @@ jobs: ./refs.sh | tee openapi3/refs.go git --no-pager diff --exit-code + - name: Check docsgen + run: ./docs.sh + - run: go mod download && go mod tidy && go mod verify - run: git --no-pager diff --exit-code diff --git a/docs.sh b/docs.sh new file mode 100755 index 000000000..5485feb2f --- /dev/null +++ b/docs.sh @@ -0,0 +1,25 @@ +#!/bin/bash -eux +set -o pipefail + +outdir=.github/docs +mkdir -p "$outdir" +for pkgpath in $(git ls-files | grep / | while read -r path; do dirname "$path"; done | sort -u | grep -vE '[.]git|testdata|cmd/'); do + go doc -short "./$pkgpath" | tee "$outdir/${pkgpath////_}.txt" +done + +git --no-pager diff -- .github/docs/ + +count_missing_mentions() { + local errors=0 + for thing in $(git --no-pager diff -- .github/docs/ \ + | grep -vE '[-]{3}' \ + | grep -Eo '^-[^ ]+ ([^ (]+)[ (]' \ + | sed 's%(% %' \ + | cut -d' ' -f2); do + if ! grep -A999999 '## Sub-v0 breaking API changes' README.md | grep -F "$thing"; then + ((errors++)) || true + fi + done + return $errors +} +count_missing_mentions diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 2e374d4c4..5853826bd 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1236,7 +1236,7 @@ func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Sch const bufferSize = 256 content := make([]byte, 0, bufferSize*len(zr.File)) - buffer := make([]byte, bufferSize) + buffer := make([]byte /*0,*/, bufferSize) for _, f := range zr.File { err := func() error { diff --git a/openapi3filter/fixtures/petstore.json b/openapi3filter/testdata/fixtures/petstore.json similarity index 100% rename from openapi3filter/fixtures/petstore.json rename to openapi3filter/testdata/fixtures/petstore.json diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index b84d8bdb6..a27556f77 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -556,7 +556,7 @@ func TestValidationErrorEncoder(t *testing.T) { func buildValidationHandler(tt *validationTest) (*ValidationHandler, error) { if tt.fields.File == "" { - tt.fields.File = "fixtures/petstore.json" + tt.fields.File = "testdata/fixtures/petstore.json" } h := &ValidationHandler{ Handler: tt.fields.Handler, @@ -600,7 +600,7 @@ func runTest_ServeHTTP(t *testing.T, handler http.Handler, encoder ErrorEncoder, h := &ValidationHandler{ Handler: handler, ErrorEncoder: encoder, - File: "fixtures/petstore.json", + File: "testdata/fixtures/petstore.json", } err := h.Load() require.NoError(t, err) @@ -612,7 +612,7 @@ func runTest_ServeHTTP(t *testing.T, handler http.Handler, encoder ErrorEncoder, func runTest_Middleware(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response { h := &ValidationHandler{ ErrorEncoder: encoder, - File: "fixtures/petstore.json", + File: "testdata/fixtures/petstore.json", } err := h.Load() require.NoError(t, err) From f0dcc53357fc6ae8ced12e2016f66f981c9cc934 Mon Sep 17 00:00:00 2001 From: Katsumi Kato Date: Mon, 9 Jan 2023 05:27:19 +0900 Subject: [PATCH 241/260] openapi3filter: RegisterBodyDecoder for text/csv (#734) fix https://github.com/getkin/kin-openapi/issues/696 --- .github/docs/openapi3filter.txt | 1 - openapi3filter/csv_file_upload_test.go | 127 ++++++++++++++++++++++++ openapi3filter/req_resp_decoder.go | 28 +++++- openapi3filter/req_resp_decoder_test.go | 4 +- 4 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 openapi3filter/csv_file_upload_test.go diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt index 094da23ea..ac738e75a 100644 --- a/.github/docs/openapi3filter.txt +++ b/.github/docs/openapi3filter.txt @@ -17,7 +17,6 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, ...) error func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, ...) error -func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, ...) (interface{}, error) type AuthenticationFunc func(context.Context, *AuthenticationInput) error type AuthenticationInput struct{ ... } type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error) diff --git a/openapi3filter/csv_file_upload_test.go b/openapi3filter/csv_file_upload_test.go new file mode 100644 index 000000000..89efb96d9 --- /dev/null +++ b/openapi3filter/csv_file_upload_test.go @@ -0,0 +1,127 @@ +package openapi3filter_test + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestValidateCsvFileUpload(t *testing.T) { + const spec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: 0.0.1 +paths: + /test: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: string + responses: + '200': + description: Created +` + + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + tests := []struct { + csvData string + wantErr bool + }{ + { + `foo,bar`, + false, + }, + { + `"foo","bar"`, + false, + }, + { + `foo,bar +baz,qux`, + false, + }, + { + `foo,bar +baz,qux,quux`, + true, + }, + { + `"""`, + true, + }, + } + for _, tt := range tests { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + { // Add file data + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="file"; filename="hello.csv"`) + h.Set("Content-Type", "text/csv") + + fw, err := writer.CreatePart(h) + require.NoError(t, err) + _, err = io.Copy(fw, strings.NewReader(tt.csvData)) + + require.NoError(t, err) + } + + writer.Close() + + req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes())) + require.NoError(t, err) + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + if err = openapi3filter.ValidateRequestBody( + context.Background(), + &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + }, + route.Operation.RequestBody.Value, + ); err != nil { + if !tt.wantErr { + t.Errorf("got %v", err) + } + continue + } + if tt.wantErr { + t.Errorf("want err") + } + } +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 5853826bd..44abacb0f 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -3,6 +3,7 @@ package openapi3filter import ( "archive/zip" "bytes" + "encoding/csv" "encoding/json" "errors" "fmt" @@ -1013,8 +1014,9 @@ func init() { RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder) RegisterBodyDecoder("application/x-yaml", yamlBodyDecoder) RegisterBodyDecoder("application/yaml", yamlBodyDecoder) - RegisterBodyDecoder("application/zip", ZipFileBodyDecoder) + RegisterBodyDecoder("application/zip", zipFileBodyDecoder) RegisterBodyDecoder("multipart/form-data", multipartBodyDecoder) + RegisterBodyDecoder("text/csv", csvBodyDecoder) RegisterBodyDecoder("text/plain", plainBodyDecoder) } @@ -1221,8 +1223,8 @@ func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema return string(data), nil } -// ZipFileBodyDecoder is a body decoder that decodes a zip file body to a string. -func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { +// zipFileBodyDecoder is a body decoder that decodes a zip file body to a string. +func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { buff := bytes.NewBuffer([]byte{}) size, err := io.Copy(buff, body) if err != nil { @@ -1271,3 +1273,23 @@ func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Sch return string(content), nil } + +// csvBodyDecoder is a body decoder that decodes a csv body to a string. +func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + r := csv.NewReader(body) + + var content string + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + content += strings.Join(record, ",") + "\n" + } + + return content, nil +} diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 709cdc929..1e71e0ac5 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -1345,7 +1345,7 @@ func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { } return strings.Split(string(data), ","), nil } - contentType := "text/csv" + contentType := "application/csv" h := make(http.Header) h.Set(headerCT, contentType) @@ -1371,7 +1371,7 @@ func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { _, _, err = decodeBody(body, h, schema, encFn) require.Equal(t, &ParseError{ Kind: KindUnsupportedFormat, - Reason: prefixUnsupportedCT + ` "text/csv"`, + Reason: prefixUnsupportedCT + ` "application/csv"`, }, err) } From 1e5c86eeb045499f248a8393b8a28409d561b286 Mon Sep 17 00:00:00 2001 From: Graham Crowell Date: Thu, 12 Jan 2023 06:50:17 -0800 Subject: [PATCH 242/260] #741 uri cache mutex (#742) --- .github/workflows/go.yml | 3 +++ openapi3/issue741_test.go | 43 +++++++++++++++++++++++++++++++++++ openapi3/loader_uri_reader.go | 8 +++++++ 3 files changed, 54 insertions(+) create mode 100644 openapi3/issue741_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b38f10fba..8dbb0620f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -79,6 +79,9 @@ jobs: - run: go test -v -run TestRaceyPatternSchema -race ./... env: CGO_ENABLED: '1' + - run: go test -v -run TestIssue741 -race ./... + env: + CGO_ENABLED: '1' - run: git --no-pager diff --exit-code - if: runner.os == 'Linux' diff --git a/openapi3/issue741_test.go b/openapi3/issue741_test.go new file mode 100644 index 000000000..aad522023 --- /dev/null +++ b/openapi3/issue741_test.go @@ -0,0 +1,43 @@ +package openapi3 + +import ( + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue741(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + body := `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Foo":{"type":"string"}}}}` + _, err := w.Write([]byte(body)) + if err != nil { + panic(err) + } + })) + defer ts.Close() + + rootSpec := []byte(fmt.Sprintf( + `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Bar1":{"$ref":"%s#/components/schemas/Foo"}}}}`, + ts.URL, + )) + + wg := &sync.WaitGroup{} + n := 10 + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + loader := NewLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadFromData(rootSpec) + require.NoError(t, err) + require.NotNil(t, doc) + }() + } + wg.Wait() +} diff --git a/openapi3/loader_uri_reader.go b/openapi3/loader_uri_reader.go index 8357a980d..92ac043f9 100644 --- a/openapi3/loader_uri_reader.go +++ b/openapi3/loader_uri_reader.go @@ -7,12 +7,15 @@ import ( "net/http" "net/url" "path/filepath" + "sync" ) // ReadFromURIFunc defines a function which reads the contents of a resource // located at a URI. type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error) +var uriMu = &sync.RWMutex{} + // ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a // given URI. var ErrURINotSupported = errors.New("unsupported URI") @@ -92,12 +95,17 @@ func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc { } uri := location.String() var ok bool + uriMu.RLock() if buf, ok = cache[uri]; ok { + uriMu.RUnlock() return } + uriMu.RUnlock() if buf, err = reader(loader, location); err != nil { return } + uriMu.Lock() + defer uriMu.Unlock() cache[uri] = buf return } From 3077c08b58e07ac0c550cf6a96173178bbcc67c1 Mon Sep 17 00:00:00 2001 From: ShouheiNishi <96609867+ShouheiNishi@users.noreply.github.com> Date: Fri, 20 Jan 2023 23:59:33 +0900 Subject: [PATCH 243/260] Specify UseNumber() in the JSON decoder during JSON validation (#738) --- openapi3/schema.go | 13 +++ openapi3filter/issue733_test.go | 109 ++++++++++++++++++++++++ openapi3filter/req_resp_decoder.go | 4 +- openapi3filter/req_resp_decoder_test.go | 2 +- 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 openapi3filter/issue733_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 9d21c00e6..25d244a14 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1077,6 +1077,19 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf switch value := value.(type) { case bool: return schema.visitJSONBoolean(settings, value) + case json.Number: + valueFloat64, err := value.Float64() + if err != nil { + return &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "type", + Reason: "cannot convert json.Number to float64", + customizeMessageError: settings.customizeMessageError, + Origin: err, + } + } + return schema.visitJSONNumber(settings, valueFloat64) case int: return schema.visitJSONNumber(settings, float64(value)) case int32: diff --git a/openapi3filter/issue733_test.go b/openapi3filter/issue733_test.go new file mode 100644 index 000000000..0d2214b58 --- /dev/null +++ b/openapi3filter/issue733_test.go @@ -0,0 +1,109 @@ +package openapi3filter_test + +import ( + "bytes" + "context" + "encoding/json" + "math" + "math/big" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIntMax(t *testing.T) { + spec := ` +openapi: 3.0.0 +info: + version: 1.0.0 + title: test large integer value +paths: + /test: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + testInteger: + type: integer + format: int64 + testDefault: + type: boolean + default: false + responses: + '200': + description: Successful response +`[1:] + + loader := openapi3.NewLoader() + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + testOne := func(value *big.Int, pass bool) { + valueString := value.String() + + req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader([]byte(`{"testInteger":`+valueString+`}`))) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + err = openapi3filter.ValidateRequest( + context.Background(), + &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + }) + if pass { + require.NoError(t, err) + + dec := json.NewDecoder(req.Body) + dec.UseNumber() + var jsonAfter map[string]interface{} + err = dec.Decode(&jsonAfter) + require.NoError(t, err) + + valueAfter := jsonAfter["testInteger"] + require.IsType(t, json.Number(""), valueAfter) + assert.Equal(t, valueString, string(valueAfter.(json.Number))) + } else { + if assert.Error(t, err) { + var serr *openapi3.SchemaError + if assert.ErrorAs(t, err, &serr) { + assert.Equal(t, "number must be an int64", serr.Reason) + } + } + } + } + + bigMaxInt64 := big.NewInt(math.MaxInt64) + bigMaxInt64Plus1 := new(big.Int).Add(bigMaxInt64, big.NewInt(1)) + bigMinInt64 := big.NewInt(math.MinInt64) + bigMinInt64Minus1 := new(big.Int).Sub(bigMinInt64, big.NewInt(1)) + + testOne(bigMaxInt64, true) + // XXX not yet fixed + // testOne(bigMaxInt64Plus1, false) + testOne(bigMaxInt64Plus1, true) + testOne(bigMinInt64, true) + // XXX not yet fixed + // testOne(bigMinInt64Minus1, false) + testOne(bigMinInt64Minus1, true) +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 44abacb0f..384b5122e 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1030,7 +1030,9 @@ func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schem func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { var value interface{} - if err := json.NewDecoder(body).Decode(&value); err != nil { + dec := json.NewDecoder(body) + dec.UseNumber() + if err := dec.Decode(&value); err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} } return value, nil diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 1e71e0ac5..449ba0e3a 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -1226,7 +1226,7 @@ func TestDecodeBody(t *testing.T) { WithProperty("d", openapi3.NewObjectSchema().WithProperty("d1", openapi3.NewStringSchema())). WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")). WithProperty("g", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"}, + want: map[string]interface{}{"a": "a1", "b": json.Number("10"), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"}, }, { name: "multipartExtraPart", From ef2fe1bd6b09c107fbe1ff525cfabedf441dafc4 Mon Sep 17 00:00:00 2001 From: Jeffrey Ying Date: Fri, 20 Jan 2023 10:02:53 -0500 Subject: [PATCH 244/260] openapi3: fix error phrase in security scheme (#745) Co-authored-by: Pierre Fenoll --- openapi3/security_scheme.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index f9a08385b..788c73e2d 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -194,7 +194,7 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption } else if len(ss.In) > 0 { return fmt.Errorf("security scheme of type %q can't have 'in'", ss.Type) } else if len(ss.Name) > 0 { - return errors.New("security scheme of type 'apiKey' can't have 'name'") + return fmt.Errorf("security scheme of type %q can't have 'name'", ss.Type) } // Validate "format" From 5c0555e4120f1ec285e261e69933701557557abc Mon Sep 17 00:00:00 2001 From: Ori Shalom Date: Fri, 20 Jan 2023 18:21:59 +0200 Subject: [PATCH 245/260] openapi3: remove value data from `SchemaError.Reason` field (#737) Resolves https://github.com/getkin/kin-openapi/issues/735 --- README.md | 68 +++++++++++++++++++++++++ openapi3/schema.go | 25 ++++----- openapi3/schema_oneOf_test.go | 6 +-- openapi3filter/issue201_test.go | 2 +- openapi3filter/issue641_test.go | 2 +- openapi3filter/unpack_errors_test.go | 2 +- openapi3filter/validation_error_test.go | 20 ++++---- 7 files changed, 95 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 27d6699cf..b85af9864 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,74 @@ func arrayUniqueItemsChecker(items []interface{}) bool { } ``` +## Custom function to change schema error messages + +By default, the error message returned when validating a value includes the error reason, the schema, and the input value. + +For example, given the following schema: + +```json +{ + "type": "string", + "allOf": [ + { "pattern": "[A-Z]" }, + { "pattern": "[a-z]" }, + { "pattern": "[0-9]" }, + { "pattern": "[!@#$%^&*()_+=-?~]" } + ] +} +``` + +Passing the input value `"secret"` to this schema will produce the following error message: + +``` +string doesn't match the regular expression "[A-Z]" +Schema: + { + "pattern": "[A-Z]" + } + +Value: + "secret" +``` + +Including the original value in the error message can be helpful for debugging, but it may not be appropriate for sensitive information such as secrets. + +To disable the extra details in the schema error message, you can set the `openapi3.SchemaErrorDetailsDisabled` option to `true`: + +```go +func main() { + // ... + + // Disable schema error detailed error messages + openapi3.SchemaErrorDetailsDisabled = true + + // ... other validate codes +} +``` + +This will shorten the error message to present only the reason: + +``` +string doesn't match the regular expression "[A-Z]" +``` + +For more fine-grained control over the error message, you can pass a custom `openapi3filter.Options` object to `openapi3filter.RequestValidationInput` that includes a `openapi3filter.CustomSchemaErrorFunc`. + +```go +func validationOptions() *openapi3filter.Options { + options := openapi3filter.DefaultOptions + options.WithCustomSchemaErrorFunc(safeErrorMessage) + return options +} + +func safeErrorMessage(err *openapi3.SchemaError) string { + return err.Reason +} +``` + +This will change the schema validation errors to return only the `Reason` field, which is guaranteed to not include the original value. + ## Sub-v0 breaking API changes ### v0.113.0 diff --git a/openapi3/schema.go b/openapi3/schema.go index 25d244a14..892f32541 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1138,7 +1138,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Value: value, Schema: schema, SchemaField: "enum", - Reason: fmt.Sprintf("value %q is not one of the allowed values", value), + Reason: fmt.Sprintf("value is not one of the allowed values %q", schema.Enum), customizeMessageError: settings.customizeMessageError, } } @@ -1177,16 +1177,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val discriminatorValString, okcheck := discriminatorVal.(string) if !okcheck { - valStr := "null" - if discriminatorVal != nil { - valStr = fmt.Sprintf("%v", discriminatorVal) - } - return &SchemaError{ Value: discriminatorVal, Schema: schema, SchemaField: "discriminator", - Reason: fmt.Sprintf("value of discriminator property %q is not a string: %v", pn, valStr), + Reason: fmt.Sprintf("value of discriminator property %q is not a string", pn), } } @@ -1195,7 +1190,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Value: discriminatorVal, Schema: schema, SchemaField: "discriminator", - Reason: fmt.Sprintf("discriminator property %q has invalid value: %q", pn, discriminatorVal), + Reason: fmt.Sprintf("discriminator property %q has invalid value", pn), } } } @@ -1364,7 +1359,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "type", - Reason: fmt.Sprintf("value \"%g\" must be an integer", value), + Reason: fmt.Sprintf("value must be an integer"), customizeMessageError: settings.customizeMessageError, } if !settings.multiError { @@ -1584,7 +1579,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "pattern", - Reason: fmt.Sprintf(`string %q doesn't match the regular expression "%s"`, value, schema.Pattern), + Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern), customizeMessageError: settings.customizeMessageError, } if !settings.multiError { @@ -1945,10 +1940,12 @@ func (schema *Schema) compilePattern() (err error) { } type SchemaError struct { - Value interface{} - reversePath []string - Schema *Schema - SchemaField string + Value interface{} + reversePath []string + Schema *Schema + SchemaField string + // Reason is a human-readable message describing the error. + // The message should never include the original value to prevent leakage of potentially sensitive inputs in error messages. Reason string Origin error customizeMessageError func(err *SchemaError) string diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go index 1a8ea8138..90a23cc98 100644 --- a/openapi3/schema_oneOf_test.go +++ b/openapi3/schema_oneOf_test.go @@ -96,7 +96,7 @@ func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) { "name": "snoopy", "$type": "snake", }) - require.ErrorContains(t, err, "discriminator property \"$type\" has invalid value: \"snake\"") + require.ErrorContains(t, err, "discriminator property \"$type\" has invalid value") } func TestVisitJSON_OneOf_MissingField(t *testing.T) { @@ -126,14 +126,14 @@ func TestVisitJSON_OneOf_BadDescriminatorType(t *testing.T) { "scratches": true, "$type": 1, }) - require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string: 1") + require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string") err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ "name": "snoopy", "barks": true, "$type": nil, }) - require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string: null") + require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string") } func TestVisitJSON_OneOf_Path(t *testing.T) { diff --git a/openapi3filter/issue201_test.go b/openapi3filter/issue201_test.go index 7e2eaabe1..ec0b2a1f1 100644 --- a/openapi3filter/issue201_test.go +++ b/openapi3filter/issue201_test.go @@ -98,7 +98,7 @@ paths: }, "invalid required header": { - err: `response header "X-Blup" doesn't match schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`, + err: `response header "X-Blup" doesn't match schema: string doesn't match the regular expression "^blup$"`, headers: map[string]string{ "X-Blip": "blip", "x-blop": "blop", diff --git a/openapi3filter/issue641_test.go b/openapi3filter/issue641_test.go index 9a2964284..ee0be1019 100644 --- a/openapi3filter/issue641_test.go +++ b/openapi3filter/issue641_test.go @@ -69,7 +69,7 @@ paths: name: "failed allof pattern", spec: allOfSpec, req: `/items?test=999999`, - errStr: `parameter "test" in query has an error: string "999999" doesn't match the regular expression "^[0-9]{1,4}$"`, + errStr: `parameter "test" in query has an error: string doesn't match the regular expression "^[0-9]{1,4}$"`, }, } diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go index 0ee48ad39..9fb7cfefd 100644 --- a/openapi3filter/unpack_errors_test.go +++ b/openapi3filter/unpack_errors_test.go @@ -93,7 +93,7 @@ func Example() { // // ===== Start New Error ===== // @body.status: - // Error at "/status": value "invalidStatus" is not one of the allowed values + // Error at "/status": value is not one of the allowed values ["available" "pending" "sold"] // Schema: // { // "description": "pet status in the store", diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index a27556f77..bc2730064 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -244,11 +244,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "value \"available,sold\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", wantErrSchemaPath: "/0", wantErrSchemaValue: "available,sold", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value \"available,sold\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", Detail: "value available,sold at /0 must be one of: available, pending, sold; " + // TODO: do we really want to use this heuristic to guess // that they're using the wrong serialization? @@ -262,11 +262,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", wantErrSchemaPath: "/1", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value \"watdis\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", Detail: "value watdis at /1 must be one of: available, pending, sold", Source: &ValidationErrorSource{Parameter: "status"}}, }, @@ -278,11 +278,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "kind", wantErrParamIn: "query", - wantErrSchemaReason: "value \"fish,with,commas\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"dog\" \"cat\" \"turtle\" \"bird,with,commas\"]", wantErrSchemaPath: "/1", wantErrSchemaValue: "fish,with,commas", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value \"fish,with,commas\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"dog\" \"cat\" \"turtle\" \"bird,with,commas\"]", Detail: "value fish,with,commas at /1 must be one of: dog, cat, turtle, bird,with,commas", // No 'perhaps you intended' because its the right serialization format Source: &ValidationErrorSource{Parameter: "kind"}}, @@ -304,11 +304,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "x-environment", wantErrParamIn: "header", - wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"demo\" \"prod\"]", wantErrSchemaPath: "/", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value \"watdis\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"demo\" \"prod\"]", Detail: "value watdis at / must be one of: demo, prod", Source: &ValidationErrorSource{Parameter: "x-environment"}}, }, @@ -323,11 +323,11 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), }, wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", - wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", wantErrSchemaValue: "watdis", wantErrSchemaPath: "/status", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "value \"watdis\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", Detail: "value watdis at /status must be one of: available, pending, sold", Source: &ValidationErrorSource{Pointer: "/status"}}, }, From abdca6cb88a70ffa6faee4cd9fd8a0f830b45e51 Mon Sep 17 00:00:00 2001 From: Ori Shalom Date: Sun, 22 Jan 2023 21:09:51 +0200 Subject: [PATCH 246/260] fix additional properties false not validated (#747) --- openapi3/issue746_test.go | 26 ++++++++++++++++++++++++++ openapi3/schema.go | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 openapi3/issue746_test.go diff --git a/openapi3/issue746_test.go b/openapi3/issue746_test.go new file mode 100644 index 000000000..390a34848 --- /dev/null +++ b/openapi3/issue746_test.go @@ -0,0 +1,26 @@ +package openapi3 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue746(t *testing.T) { + schema := &Schema{} + err := schema.UnmarshalJSON([]byte(`{"additionalProperties": false}`)) + require.NoError(t, err) + + var value interface{} + err = json.Unmarshal([]byte(`{"foo": "bar"}`), &value) + require.NoError(t, err) + + err = schema.VisitJSON(value) + require.Error(t, err) + + schemaErr := &SchemaError{} + require.ErrorAs(t, err, &schemaErr) + require.Equal(t, "properties", schemaErr.SchemaField) + require.Equal(t, `property "foo" is unsupported`, schemaErr.Reason) +} diff --git a/openapi3/schema.go b/openapi3/schema.go index 892f32541..fbf1900db 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -810,7 +810,7 @@ func (schema *Schema) IsEmpty() bool { if ap := schema.AdditionalProperties.Schema; ap != nil && !ap.Value.IsEmpty() { return false } - if apa := schema.AdditionalProperties.Has; apa != nil && *apa { + if apa := schema.AdditionalProperties.Has; apa != nil && !*apa { return false } if items := schema.Items; items != nil && !items.Value.IsEmpty() { From 9f99fee5a643dd7522772022cda4eee8b7f3f614 Mon Sep 17 00:00:00 2001 From: Ori Shalom Date: Mon, 23 Jan 2023 15:13:27 +0200 Subject: [PATCH 247/260] Refine schema error reason message (#748) --- openapi3/issue136_test.go | 2 +- openapi3/issue735_test.go | 278 ++++++++++++++++++++++++ openapi3/schema.go | 77 +++++-- openapi3/schema_oneOf_test.go | 2 +- openapi3filter/issue641_test.go | 2 +- openapi3filter/unpack_errors_test.go | 6 +- openapi3filter/validation_error_test.go | 31 +-- routers/gorillamux/example_test.go | 4 +- 8 files changed, 355 insertions(+), 47 deletions(-) create mode 100644 openapi3/issue735_test.go diff --git a/openapi3/issue136_test.go b/openapi3/issue136_test.go index b5e9eebe5..3aa7edd8f 100644 --- a/openapi3/issue136_test.go +++ b/openapi3/issue136_test.go @@ -31,7 +31,7 @@ components: }, { dflt: `1`, - err: "invalid components: invalid schema default: field must be set to string or not be present", + err: "invalid components: invalid schema default: value must be a string", }, } { t.Run(testcase.dflt, func(t *testing.T) { diff --git a/openapi3/issue735_test.go b/openapi3/issue735_test.go new file mode 100644 index 000000000..f7e420c5d --- /dev/null +++ b/openapi3/issue735_test.go @@ -0,0 +1,278 @@ +package openapi3 + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +type testCase struct { + name string + schema *Schema + value interface{} + extraNotContains []interface{} + options []SchemaValidationOption +} + +func TestIssue735(t *testing.T) { + DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) + DefineStringFormat("email", FormatOfStringForEmail) + DefineIPv4Format() + DefineIPv6Format() + + testCases := []testCase{ + { + name: "type string", + schema: NewStringSchema(), + value: 42, + }, + { + name: "type boolean", + schema: NewBoolSchema(), + value: 42, + }, + { + name: "type integer", + schema: NewIntegerSchema(), + value: "foo", + }, + { + name: "type number", + schema: NewFloat64Schema(), + value: "foo", + }, + { + name: "type array", + schema: NewArraySchema(), + value: 42, + }, + { + name: "type object", + schema: NewObjectSchema(), + value: 42, + }, + { + name: "min", + schema: NewSchema().WithMin(100), + value: 42, + }, + { + name: "max", + schema: NewSchema().WithMax(0), + value: 42, + }, + { + name: "exclusive min", + schema: NewSchema().WithMin(100).WithExclusiveMin(true), + value: 42, + }, + { + name: "exclusive max", + schema: NewSchema().WithMax(0).WithExclusiveMax(true), + value: 42, + }, + { + name: "multiple of", + schema: &Schema{MultipleOf: Float64Ptr(5.0)}, + value: 42, + }, + { + name: "enum", + schema: NewSchema().WithEnum(3, 5), + value: 42, + }, + { + name: "min length", + schema: NewSchema().WithMinLength(100), + value: "foo", + }, + { + name: "max length", + schema: NewSchema().WithMaxLength(0), + value: "foo", + }, + { + name: "pattern", + schema: NewSchema().WithPattern("[0-9]"), + value: "foo", + }, + { + name: "items", + schema: NewSchema().WithItems(NewStringSchema()), + value: []interface{}{42}, + extraNotContains: []interface{}{42}, + }, + { + name: "min items", + schema: NewSchema().WithMinItems(100), + value: []interface{}{42}, + extraNotContains: []interface{}{42}, + }, + { + name: "max items", + schema: NewSchema().WithMaxItems(0), + value: []interface{}{42}, + extraNotContains: []interface{}{42}, + }, + { + name: "unique items", + schema: NewSchema().WithUniqueItems(true), + value: []interface{}{42, 42}, + extraNotContains: []interface{}{42}, + }, + { + name: "min properties", + schema: NewSchema().WithMinProperties(100), + value: map[string]interface{}{"foo": 42}, + extraNotContains: []interface{}{42}, + }, + { + name: "max properties", + schema: NewSchema().WithMaxProperties(0), + value: map[string]interface{}{"foo": 42}, + extraNotContains: []interface{}{42}, + }, + { + name: "additional properties other schema type", + schema: NewSchema().WithAdditionalProperties(NewStringSchema()), + value: map[string]interface{}{"foo": 42}, + extraNotContains: []interface{}{42}, + }, + { + name: "additional properties false", + schema: &Schema{AdditionalProperties: AdditionalProperties{ + Has: BoolPtr(false), + }}, + value: map[string]interface{}{"foo": 42}, + extraNotContains: []interface{}{42}, + }, + { + name: "invalid properties schema", + schema: NewSchema().WithProperties(map[string]*Schema{ + "foo": NewStringSchema(), + }), + value: map[string]interface{}{"foo": 42}, + extraNotContains: []interface{}{42}, + }, + // TODO: uncomment when https://github.com/getkin/kin-openapi/issues/502 is fixed + //{ + // name: "read only properties", + // schema: NewSchema().WithProperties(map[string]*Schema{ + // "foo": {ReadOnly: true}, + // }).WithoutAdditionalProperties(), + // value: map[string]interface{}{"foo": 42}, + // extraNotContains: []interface{}{42}, + // options: []SchemaValidationOption{VisitAsRequest()}, + //}, + //{ + // name: "write only properties", + // schema: NewSchema().WithProperties(map[string]*Schema{ + // "foo": {WriteOnly: true}, + // }).WithoutAdditionalProperties(), + // value: map[string]interface{}{"foo": 42}, + // extraNotContains: []interface{}{42}, + // options: []SchemaValidationOption{VisitAsResponse()}, + //}, + { + name: "required properties", + schema: &Schema{ + Properties: Schemas{ + "bar": NewStringSchema().NewRef(), + }, + Required: []string{"bar"}, + }, + value: map[string]interface{}{"foo": 42}, + extraNotContains: []interface{}{42}, + }, + { + name: "one of (matches more then one)", + schema: NewOneOfSchema( + &Schema{MultipleOf: Float64Ptr(6)}, + &Schema{MultipleOf: Float64Ptr(7)}, + ), + value: 42, + }, + { + name: "one of (no matches)", + schema: NewOneOfSchema( + &Schema{MultipleOf: Float64Ptr(5)}, + &Schema{MultipleOf: Float64Ptr(10)}, + ), + value: 42, + }, + { + name: "any of", + schema: NewAnyOfSchema( + &Schema{MultipleOf: Float64Ptr(5)}, + &Schema{MultipleOf: Float64Ptr(10)}, + ), + value: 42, + }, + { + name: "all of (match some)", + schema: NewAllOfSchema( + &Schema{MultipleOf: Float64Ptr(6)}, + &Schema{MultipleOf: Float64Ptr(5)}, + ), + value: 42, + }, + { + name: "all of (no match)", + schema: NewAllOfSchema( + &Schema{MultipleOf: Float64Ptr(10)}, + &Schema{MultipleOf: Float64Ptr(5)}, + ), + value: 42, + }, + { + name: "uuid format", + schema: NewUUIDSchema(), + value: "foo", + }, + { + name: "date time format", + schema: NewDateTimeSchema(), + value: "foo", + }, + { + name: "date format", + schema: NewSchema().WithFormat("date"), + value: "foo", + }, + { + name: "ipv4 format", + schema: NewSchema().WithFormat("ipv4"), + value: "foo", + }, + { + name: "ipv6 format", + schema: NewSchema().WithFormat("ipv6"), + value: "foo", + }, + { + name: "email format", + schema: NewSchema().WithFormat("email"), + value: "foo", + }, + { + name: "byte format", + schema: NewBytesSchema(), + value: "foo!", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.schema.VisitJSON(tc.value, tc.options...) + var schemaError = &SchemaError{} + require.Error(t, err) + require.ErrorAs(t, err, &schemaError) + require.NotZero(t, schemaError.Reason) + require.NotContains(t, schemaError.Reason, fmt.Sprint(tc.value)) + for _, extra := range tc.extraNotContains { + require.NotContains(t, schemaError.Reason, fmt.Sprint(extra)) + } + }) + } +} diff --git a/openapi3/schema.go b/openapi3/schema.go index fbf1900db..0d1d18382 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -784,6 +784,11 @@ func (schema *Schema) WithAnyAdditionalProperties() *Schema { return schema } +func (schema *Schema) WithoutAdditionalProperties() *Schema { + schema.AdditionalProperties = AdditionalProperties{Has: BoolPtr(false)} + return schema +} + func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema { schema.AdditionalProperties = AdditionalProperties{} if v != nil { @@ -1134,11 +1139,12 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val if settings.failfast { return errSchema } + allowedValues, _ := json.Marshal(enum) return &SchemaError{ Value: value, Schema: schema, SchemaField: "enum", - Reason: fmt.Sprintf("value is not one of the allowed values %q", schema.Enum), + Reason: fmt.Sprintf("value is not one of the allowed values %s", string(allowedValues)), customizeMessageError: settings.customizeMessageError, } } @@ -1197,10 +1203,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val } var ( - ok = 0 - validationErrors = multiErrorForOneOf{} - matchedOneOfIdx = 0 - tempValue = value + ok = 0 + validationErrors = multiErrorForOneOf{} + matchedOneOfIndices = make([]int, 0) + tempValue = value ) for idx, item := range v { v := item.Value @@ -1222,14 +1228,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val continue } - matchedOneOfIdx = idx + matchedOneOfIndices = append(matchedOneOfIndices, idx) ok++ } if ok != 1 { - if len(validationErrors) > 1 { - return fmt.Errorf("doesn't match schema due to: %w", validationErrors) - } if settings.failfast { return errSchema } @@ -1241,15 +1244,18 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val } if ok > 1 { e.Origin = ErrOneOfConflict - } else if len(validationErrors) == 1 { - e.Origin = validationErrors[0] + e.Reason = fmt.Sprintf(`value matches more than one schema from "oneOf" (matches schemas at indices %v)`, matchedOneOfIndices) + } else { + e.Origin = fmt.Errorf("doesn't match schema due to: %w", validationErrors) + e.Reason = `value doesn't match any schema from "oneOf"` } return e } + // run again to inject default value that defined in matched oneOf schema if settings.asreq || settings.asrep { - _ = v[matchedOneOfIdx].Value.visitJSON(settings, value) + _ = v[matchedOneOfIndices[0]].Value.visitJSON(settings, value) } } @@ -1282,6 +1288,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Value: value, Schema: schema, SchemaField: "anyOf", + Reason: `doesn't match any schema from "anyOf"`, customizeMessageError: settings.customizeMessageError, } } @@ -1302,6 +1309,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Value: value, Schema: schema, SchemaField: "allOf", + Reason: `doesn't match all schemas from "allOf"`, Origin: err, customizeMessageError: settings.customizeMessageError, } @@ -1337,7 +1345,7 @@ func (schema *Schema) VisitJSONBoolean(value bool) error { func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) { if schemaType := schema.Type; schemaType != "" && schemaType != TypeBoolean { - return schema.expectedType(settings, TypeBoolean) + return schema.expectedType(settings, value) } return } @@ -1368,7 +1376,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value me = append(me, err) } } else if schemaType != "" && schemaType != TypeNumber { - return schema.expectedType(settings, "number, integer") + return schema.expectedType(settings, value) } // formats @@ -1489,6 +1497,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "multipleOf", + Reason: fmt.Sprintf("number must be a multiple of %g", *v), customizeMessageError: settings.customizeMessageError, } if !settings.multiError { @@ -1512,7 +1521,7 @@ func (schema *Schema) VisitJSONString(value string) error { func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error { if schemaType := schema.Type; schemaType != "" && schemaType != TypeString { - return schema.expectedType(settings, TypeString) + return schema.expectedType(settings, value) } var me MultiError @@ -1600,6 +1609,12 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } case f.regexp == nil && f.callback != nil: if err := f.callback(value); err != nil { + var schemaErr = &SchemaError{} + if errors.As(err, &schemaErr) { + formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%s)`, format, schemaErr.Reason) + } else { + formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, err) + } formatErr = err } default: @@ -1637,7 +1652,7 @@ func (schema *Schema) VisitJSONArray(value []interface{}) error { func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error { if schemaType := schema.Type; schemaType != "" && schemaType != TypeArray { - return schema.expectedType(settings, TypeArray) + return schema.expectedType(settings, value) } var me MultiError @@ -1736,7 +1751,7 @@ func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error { if schemaType := schema.Type; schemaType != "" && schemaType != TypeObject { - return schema.expectedType(settings, TypeObject) + return schema.expectedType(settings, value) } var me MultiError @@ -1915,15 +1930,21 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return nil } -func (schema *Schema) expectedType(settings *schemaValidationSettings, typ string) error { +func (schema *Schema) expectedType(settings *schemaValidationSettings, value interface{}) error { if settings.failfast { return errSchema } + + a := "a" + switch schema.Type { + case TypeArray, TypeObject, TypeInteger: + a = "an" + } return &SchemaError{ - Value: typ, + Value: value, Schema: schema, SchemaField: "type", - Reason: fmt.Sprintf("field must be set to %s or not be present", schema.Type), + Reason: fmt.Sprintf("value must be %s %s", a, schema.Type), customizeMessageError: settings.customizeMessageError, } } @@ -1933,21 +1954,29 @@ func (schema *Schema) compilePattern() (err error) { return &SchemaError{ Schema: schema, SchemaField: "pattern", + Origin: err, Reason: fmt.Sprintf("cannot compile pattern %q: %v", schema.Pattern, err), } } return nil } +// SchemaError is an error that occurs during schema validation. type SchemaError struct { - Value interface{} + // Value is the value that failed validation. + Value interface{} + // reversePath is the path to the value that failed validation. reversePath []string - Schema *Schema + // Schema is the schema that failed validation. + Schema *Schema + // SchemaField is the field of the schema that failed validation. SchemaField string // Reason is a human-readable message describing the error. // The message should never include the original value to prevent leakage of potentially sensitive inputs in error messages. - Reason string - Origin error + Reason string + // Origin is the original error that caused this error. + Origin error + // customizeMessageError is a function that can be used to customize the error message. customizeMessageError func(err *SchemaError) string } diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go index 90a23cc98..8d5451950 100644 --- a/openapi3/schema_oneOf_test.go +++ b/openapi3/schema_oneOf_test.go @@ -106,7 +106,7 @@ func TestVisitJSON_OneOf_MissingField(t *testing.T) { "name": "snoopy", "$type": "dog", }) - require.EqualError(t, err, "Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"$type\": {\n \"enum\": [\n \"dog\"\n ],\n \"type\": \"string\"\n },\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\",\n \"$type\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"$type\": \"dog\",\n \"name\": \"snoopy\"\n }\n") + require.EqualError(t, err, "doesn't match schema due to: Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"$type\": {\n \"enum\": [\n \"dog\"\n ],\n \"type\": \"string\"\n },\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\",\n \"$type\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"$type\": \"dog\",\n \"name\": \"snoopy\"\n }\n") } func TestVisitJSON_OneOf_NoDiscriptor_MissingField(t *testing.T) { diff --git a/openapi3filter/issue641_test.go b/openapi3filter/issue641_test.go index ee0be1019..1c5277d0d 100644 --- a/openapi3filter/issue641_test.go +++ b/openapi3filter/issue641_test.go @@ -57,7 +57,7 @@ paths: name: "failed anyof pattern", spec: anyOfSpec, req: "/items?test=999999", - errStr: `parameter "test" in query has an error: Doesn't match schema "anyOf"`, + errStr: `parameter "test" in query has an error: doesn't match any schema from "anyOf"`, }, { diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go index 9fb7cfefd..befff1054 100644 --- a/openapi3filter/unpack_errors_test.go +++ b/openapi3filter/unpack_errors_test.go @@ -81,7 +81,7 @@ func Example() { // Output: // ===== Start New Error ===== // @body.name: - // Error at "/name": field must be set to string or not be present + // Error at "/name": value must be a string // Schema: // { // "example": "doggie", @@ -89,11 +89,11 @@ func Example() { // } // // Value: - // "number, integer" + // 100 // // ===== Start New Error ===== // @body.status: - // Error at "/status": value is not one of the allowed values ["available" "pending" "sold"] + // Error at "/status": value is not one of the allowed values ["available","pending","sold"] // Schema: // { // "description": "pet status in the store", diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index bc2730064..4652adac9 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -244,11 +244,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", + wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]", wantErrSchemaPath: "/0", wantErrSchemaValue: "available,sold", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", + Title: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]", Detail: "value available,sold at /0 must be one of: available, pending, sold; " + // TODO: do we really want to use this heuristic to guess // that they're using the wrong serialization? @@ -262,11 +262,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", + wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]", wantErrSchemaPath: "/1", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", + Title: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]", Detail: "value watdis at /1 must be one of: available, pending, sold", Source: &ValidationErrorSource{Parameter: "status"}}, }, @@ -278,11 +278,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "kind", wantErrParamIn: "query", - wantErrSchemaReason: "value is not one of the allowed values [\"dog\" \"cat\" \"turtle\" \"bird,with,commas\"]", + wantErrSchemaReason: "value is not one of the allowed values [\"dog\",\"cat\",\"turtle\",\"bird,with,commas\"]", wantErrSchemaPath: "/1", wantErrSchemaValue: "fish,with,commas", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value is not one of the allowed values [\"dog\" \"cat\" \"turtle\" \"bird,with,commas\"]", + Title: "value is not one of the allowed values [\"dog\",\"cat\",\"turtle\",\"bird,with,commas\"]", Detail: "value fish,with,commas at /1 must be one of: dog, cat, turtle, bird,with,commas", // No 'perhaps you intended' because its the right serialization format Source: &ValidationErrorSource{Parameter: "kind"}}, @@ -304,11 +304,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "x-environment", wantErrParamIn: "header", - wantErrSchemaReason: "value is not one of the allowed values [\"demo\" \"prod\"]", + wantErrSchemaReason: "value is not one of the allowed values [\"demo\",\"prod\"]", wantErrSchemaPath: "/", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value is not one of the allowed values [\"demo\" \"prod\"]", + Title: "value is not one of the allowed values [\"demo\",\"prod\"]", Detail: "value watdis at / must be one of: demo, prod", Source: &ValidationErrorSource{Parameter: "x-environment"}}, }, @@ -323,11 +323,11 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), }, wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", - wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", + wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]", wantErrSchemaValue: "watdis", wantErrSchemaPath: "/status", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", + Title: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]", Detail: "value watdis at /status must be one of: available, pending, sold", Source: &ValidationErrorSource{Pointer: "/status"}}, }, @@ -379,13 +379,13 @@ func getValidationTests(t *testing.T) []*validationTest { bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)), }, wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", - wantErrSchemaReason: "field must be set to array or not be present", + wantErrSchemaReason: "value must be an array", wantErrSchemaPath: "/photoUrls", - wantErrSchemaValue: "string", + wantErrSchemaValue: "http://cat", // TODO: this shouldn't say "or not be present", but this requires recursively resolving // innerErr.JSONPointer() against e.RequestBody.Content["application/json"].Schema.Value (.Required, .Properties) wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "field must be set to array or not be present", + Title: "value must be an array", Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { @@ -396,6 +396,7 @@ func getValidationTests(t *testing.T) []*validationTest { wantErrReason: "doesn't match schema", wantErrSchemaPath: "/", wantErrSchemaValue: map[string]string{"name": "Bahama"}, + wantErrSchemaReason: `doesn't match all schemas from "allOf"`, wantErrSchemaOriginReason: `property "photoUrls" is missing`, wantErrSchemaOriginValue: map[string]string{"name": "Bahama"}, wantErrSchemaOriginPath: "/photoUrls", @@ -659,7 +660,7 @@ func TestValidationHandler_ServeHTTP(t *testing.T) { body, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) - require.Equal(t, "[422][][] field must be set to array or not be present [source pointer=/photoUrls]", string(body)) + require.Equal(t, "[422][][] value must be an array [source pointer=/photoUrls]", string(body)) }) } @@ -701,6 +702,6 @@ func TestValidationHandler_Middleware(t *testing.T) { body, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) - require.Equal(t, "[422][][] field must be set to array or not be present [source pointer=/photoUrls]", string(body)) + require.Equal(t, "[422][][] value must be an array [source pointer=/photoUrls]", string(body)) }) } diff --git a/routers/gorillamux/example_test.go b/routers/gorillamux/example_test.go index 54058cde2..9f949bdc5 100644 --- a/routers/gorillamux/example_test.go +++ b/routers/gorillamux/example_test.go @@ -53,12 +53,12 @@ func Example() { err = openapi3filter.ValidateResponse(ctx, responseValidationInput) fmt.Println(err) // Output: - // response body doesn't match schema pathref.openapi.yml#/components/schemas/TestSchema: field must be set to string or not be present + // response body doesn't match schema pathref.openapi.yml#/components/schemas/TestSchema: value must be a string // Schema: // { // "type": "string" // } // // Value: - // "object" + // {} } From a3a19f07cd9760b471ea9df1d7313135f4e6c686 Mon Sep 17 00:00:00 2001 From: Andrew Yang Date: Wed, 25 Jan 2023 05:26:45 -0800 Subject: [PATCH 248/260] openapi3: fix validation of non-empty interface slice value against array schema (#752) Resolves https://github.com/getkin/kin-openapi/issues/751 --- openapi3/schema.go | 11 +++++++++++ openapi3/schema_test.go | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/openapi3/schema.go b/openapi3/schema.go index 0d1d18382..415c28170 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1120,6 +1120,17 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf return schema.visitJSONObject(settings, values) } } + + // Catch slice of non-empty interface type + if reflect.TypeOf(value).Kind() == reflect.Slice { + valueR := reflect.ValueOf(value) + newValue := make([]interface{}, valueR.Len()) + for i := 0; i < valueR.Len(); i++ { + newValue[i] = valueR.Index(i).Interface() + } + return schema.visitJSONArray(settings, newValue) + } + return &SchemaError{ Value: value, Schema: schema, diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 2bd9848dd..26669799a 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1354,3 +1354,15 @@ enum: err = schema.VisitJSON(map[string]interface{}{"d": "e"}) require.Error(t, err) } + +func TestIssue751(t *testing.T) { + schema := &Schema{ + Type: "array", + UniqueItems: true, + Items: NewStringSchema().NewRef(), + } + validData := []string{"foo", "bar"} + invalidData := []string{"foo", "foo"} + require.NoError(t, schema.VisitJSON(validData)) + require.ErrorContains(t, schema.VisitJSON(invalidData), "duplicate items found") +} From 84703aa522d3540c1631fd57eb8257da5f71ddaf Mon Sep 17 00:00:00 2001 From: Nodar Jarrar <36896519+nodar963@users.noreply.github.com> Date: Sat, 28 Jan 2023 13:18:11 +0100 Subject: [PATCH 249/260] openapi3: empty scopes are valid (#754) --- openapi3/security_scheme.go | 4 ++-- openapi3/security_scheme_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 788c73e2d..76cc21f37 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -363,8 +363,8 @@ func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) e } } - if v := flow.Scopes; len(v) == 0 { - return errors.New("field 'scopes' is empty or missing") + if flow.Scopes == nil { + return errors.New("field 'scopes' is missing") } return validateExtensions(ctx, flow.Extensions) diff --git a/openapi3/security_scheme_test.go b/openapi3/security_scheme_test.go index 48ea04604..790414ca2 100644 --- a/openapi3/security_scheme_test.go +++ b/openapi3/security_scheme_test.go @@ -197,7 +197,7 @@ var securitySchemeExamples = []securitySchemeExample{ } } }`), - valid: false, + valid: true, }, { From 6e233af317f2505016f2824d3d5df0fa4eddc5fa Mon Sep 17 00:00:00 2001 From: Katsumi Kato Date: Sat, 28 Jan 2023 21:20:15 +0900 Subject: [PATCH 250/260] openapi3: fix integer enum schema validation after json.Number PR (#755) --- openapi3/schema.go | 16 ++- openapi3filter/validation_enum_test.go | 175 +++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 openapi3filter/validation_enum_test.go diff --git a/openapi3/schema.go b/openapi3/schema.go index 415c28170..4e2bf1419 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1143,8 +1143,20 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) { if enum := schema.Enum; len(enum) != 0 { for _, v := range enum { - if reflect.DeepEqual(v, value) { - return + switch c := value.(type) { + case json.Number: + var f float64 + f, err = strconv.ParseFloat(c.String(), 64) + if err != nil { + return err + } + if v == f { + return + } + default: + if reflect.DeepEqual(v, value) { + return + } } } if settings.failfast { diff --git a/openapi3filter/validation_enum_test.go b/openapi3filter/validation_enum_test.go new file mode 100644 index 000000000..898c4027a --- /dev/null +++ b/openapi3filter/validation_enum_test.go @@ -0,0 +1,175 @@ +package openapi3filter + +import ( + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + legacyrouter "github.com/getkin/kin-openapi/routers/legacy" +) + +func TestValidationWithIntegerEnum(t *testing.T) { + const spec = ` +openapi: 3.0.0 +info: + title: Example integer enum + version: '0.1' +paths: + /sample: + put: + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + exenum: + type: integer + enum: + - 0 + - 1 + - 2 + - 3 + example: 0 + nullable: true + responses: + '200': + description: Ok +` + + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + router, err := legacyrouter.NewRouter(doc) + require.NoError(t, err) + + tests := []struct { + data []byte + wantErr bool + }{ + { + []byte(`{"exenum": 1}`), + false, + }, + { + []byte(`{"exenum": "1"}`), + true, + }, + { + []byte(`{"exenum": null}`), + false, + }, + { + []byte(`{}`), + false, + }, + } + + for _, tt := range tests { + body := bytes.NewReader(tt.data) + req, err := http.NewRequest("PUT", "/sample", body) + require.NoError(t, err) + req.Header.Add(headerCT, "application/json") + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + requestValidationInput := &RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + } + err = ValidateRequest(loader.Context, requestValidationInput) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } +} + +func TestValidationWithStringEnum(t *testing.T) { + const spec = ` +openapi: 3.0.0 +info: + title: Example string enum + version: '0.1' +paths: + /sample: + put: + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + exenum: + type: string + enum: + - "0" + - "1" + - "2" + - "3" + example: "0" + responses: + '200': + description: Ok +` + + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + router, err := legacyrouter.NewRouter(doc) + require.NoError(t, err) + + tests := []struct { + data []byte + wantErr bool + }{ + { + []byte(`{"exenum": "1"}`), + false, + }, + { + []byte(`{"exenum": 1}`), + true, + }, + { + []byte(`{"exenum": null}`), + true, + }, + { + []byte(`{}`), + false, + }, + } + + for _, tt := range tests { + body := bytes.NewReader(tt.data) + req, err := http.NewRequest("PUT", "/sample", body) + require.NoError(t, err) + req.Header.Add(headerCT, "application/json") + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + requestValidationInput := &RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + } + err = ValidateRequest(loader.Context, requestValidationInput) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } +} From 91455633f1fb39ec6d8fd96af7209e133382c622 Mon Sep 17 00:00:00 2001 From: orshlom <44160965+orshlom@users.noreply.github.com> Date: Wed, 1 Feb 2023 15:06:07 +0200 Subject: [PATCH 251/260] optional readOnly and writeOnly validations (#758) --- .github/docs/openapi3.txt | 2 + openapi3/issue689_test.go | 107 ++++++++++++++++ openapi3/schema.go | 4 +- openapi3/schema_validation_settings.go | 22 +++- openapi3filter/issue689_test.go | 168 +++++++++++++++++++++++++ openapi3filter/options.go | 6 + openapi3filter/validate_request.go | 5 +- openapi3filter/validate_response.go | 5 +- 8 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 openapi3/issue689_test.go create mode 100644 openapi3filter/issue689_test.go diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 28819d73a..3ce7ed959 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -113,6 +113,8 @@ type SchemaRefs []*SchemaRef type SchemaValidationOption func(*schemaValidationSettings) func DefaultsSet(f func()) SchemaValidationOption func DisablePatternValidation() SchemaValidationOption + func DisableReadOnlyValidation() SchemaValidationOption + func DisableWriteOnlyValidation() SchemaValidationOption func EnableFormatValidation() SchemaValidationOption func FailFast() SchemaValidationOption func MultiErrors() SchemaValidationOption diff --git a/openapi3/issue689_test.go b/openapi3/issue689_test.go new file mode 100644 index 000000000..cafbadfac --- /dev/null +++ b/openapi3/issue689_test.go @@ -0,0 +1,107 @@ +package openapi3_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestIssue689(t *testing.T) { + t.Parallel() + + tests := [...]struct { + name string + schema *openapi3.Schema + value map[string]interface{} + opts []openapi3.SchemaValidationOption + checkErr require.ErrorAssertionFunc + }{ + // read-only + { + name: "read-only property succeeds when read-only validation is disabled", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", ReadOnly: true}}), + value: map[string]interface{}{"foo": true}, + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest(), + openapi3.DisableReadOnlyValidation()}, + checkErr: require.NoError, + }, + { + name: "non read-only property succeeds when read-only validation is disabled", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", ReadOnly: false}}), + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest()}, + value: map[string]interface{}{"foo": true}, + checkErr: require.NoError, + }, + { + name: "read-only property fails when read-only validation is enabled", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", ReadOnly: true}}), + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest()}, + value: map[string]interface{}{"foo": true}, + checkErr: require.Error, + }, + { + name: "non read-only property succeeds when read-only validation is enabled", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", ReadOnly: false}}), + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest()}, + value: map[string]interface{}{"foo": true}, + checkErr: require.NoError, + }, + // write-only + { + name: "write-only property succeeds when write-only validation is disabled", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", WriteOnly: true}}), + value: map[string]interface{}{"foo": true}, + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsResponse(), + openapi3.DisableWriteOnlyValidation()}, + checkErr: require.NoError, + }, + { + name: "non write-only property succeeds when write-only validation is disabled", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", WriteOnly: false}}), + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsResponse()}, + value: map[string]interface{}{"foo": true}, + checkErr: require.NoError, + }, + { + name: "write-only property fails when write-only validation is enabled", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", WriteOnly: true}}), + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsResponse()}, + value: map[string]interface{}{"foo": true}, + checkErr: require.Error, + }, + { + name: "non write-only property succeeds when write-only validation is enabled", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", WriteOnly: false}}), + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsResponse()}, + value: map[string]interface{}{"foo": true}, + checkErr: require.NoError, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + err := test.schema.VisitJSON(test.value, test.opts...) + test.checkErr(t, err) + }) + } +} diff --git a/openapi3/schema.go b/openapi3/schema.go index 4e2bf1419..5f867ff44 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1787,8 +1787,8 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value sort.Strings(properties) for _, propName := range properties { propSchema := schema.Properties[propName] - reqRO := settings.asreq && propSchema.Value.ReadOnly - repWO := settings.asrep && propSchema.Value.WriteOnly + reqRO := settings.asreq && propSchema.Value.ReadOnly && !settings.readOnlyValidationDisabled + repWO := settings.asrep && propSchema.Value.WriteOnly && !settings.writeOnlyValidationDisabled if value[propName] == nil { if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO { diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 5a28c8d8d..17aad2fa7 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -8,11 +8,13 @@ import ( type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { - failfast bool - multiError bool - asreq, asrep bool // exclusive (XOR) fields - formatValidationEnabled bool - patternValidationDisabled bool + failfast bool + multiError bool + asreq, asrep bool // exclusive (XOR) fields + formatValidationEnabled bool + patternValidationDisabled bool + readOnlyValidationDisabled bool + writeOnlyValidationDisabled bool onceSettingDefaults sync.Once defaultsSet func() @@ -47,6 +49,16 @@ func DisablePatternValidation() SchemaValidationOption { return func(s *schemaValidationSettings) { s.patternValidationDisabled = true } } +// DisableReadOnlyValidation setting makes Validate not return an error when validating properties marked as read-only +func DisableReadOnlyValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.readOnlyValidationDisabled = true } +} + +// DisableWriteOnlyValidation setting makes Validate not return an error when validating properties marked as write-only +func DisableWriteOnlyValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.writeOnlyValidationDisabled = true } +} + // DefaultsSet executes the given callback (once) IFF schema validation set default values. func DefaultsSet(f func()) SchemaValidationOption { return func(s *schemaValidationSettings) { s.defaultsSet = f } diff --git a/openapi3filter/issue689_test.go b/openapi3filter/issue689_test.go new file mode 100644 index 000000000..592d53f74 --- /dev/null +++ b/openapi3filter/issue689_test.go @@ -0,0 +1,168 @@ +package openapi3filter + +import ( + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue689(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + spec := ` + openapi: 3.0.0 + info: + version: 1.0.0 + title: Sample API + paths: + /items: + put: + requestBody: + content: + application/json: + schema: + properties: + testWithReadOnly: + readOnly: true + type: boolean + testNoReadOnly: + type: boolean + type: object + responses: + '200': + description: OK + get: + responses: + '200': + description: OK + content: + application/json: + schema: + properties: + testWithWriteOnly: + writeOnly: true + type: boolean + testNoWriteOnly: + type: boolean +`[1:] + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + tests := []struct { + name string + options *Options + body string + method string + checkErr require.ErrorAssertionFunc + }{ + // read-only + { + name: "non read-only property is added to request when validation enabled", + body: `{"testNoReadOnly": true}`, + method: http.MethodPut, + checkErr: require.NoError, + }, + { + name: "non read-only property is added to request when validation disabled", + body: `{"testNoReadOnly": true}`, + method: http.MethodPut, + options: &Options{ + ExcludeReadOnlyValidations: true, + }, + checkErr: require.NoError, + }, + { + name: "read-only property is added to requests when validation enabled", + body: `{"testWithReadOnly": true}`, + method: http.MethodPut, + checkErr: require.Error, + }, + { + name: "read-only property is added to requests when validation disabled", + body: `{"testWithReadOnly": true}`, + method: http.MethodPut, + options: &Options{ + ExcludeReadOnlyValidations: true, + }, + checkErr: require.NoError, + }, + // write-only + { + name: "non write-only property is added to request when validation enabled", + body: `{"testNoWriteOnly": true}`, + method: http.MethodGet, + checkErr: require.NoError, + }, + { + name: "non write-only property is added to request when validation disabled", + body: `{"testNoWriteOnly": true}`, + method: http.MethodGet, + options: &Options{ + ExcludeWriteOnlyValidations: true, + }, + checkErr: require.NoError, + }, + { + name: "write-only property is added to requests when validation enabled", + body: `{"testWithWriteOnly": true}`, + method: http.MethodGet, + checkErr: require.Error, + }, + { + name: "write-only property is added to requests when validation disabled", + body: `{"testWithWriteOnly": true}`, + method: http.MethodGet, + options: &Options{ + ExcludeWriteOnlyValidations: true, + }, + checkErr: require.NoError, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + httpReq, err := http.NewRequest(test.method, "/items", strings.NewReader(test.body)) + require.NoError(t, err) + httpReq.Header.Set("Content-Type", "application/json") + require.NoError(t, err) + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + requestValidationInput := &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + Options: test.options, + } + + if test.method == http.MethodGet { + responseValidationInput := &ResponseValidationInput{ + RequestValidationInput: requestValidationInput, + Status: 200, + Header: httpReq.Header, + Body: io.NopCloser(strings.NewReader(test.body)), + Options: test.options, + } + err = ValidateResponse(ctx, responseValidationInput) + + } else { + err = ValidateRequest(ctx, requestValidationInput) + } + test.checkErr(t, err) + }) + } +} diff --git a/openapi3filter/options.go b/openapi3filter/options.go index 14c35d5da..4ea9e9907 100644 --- a/openapi3filter/options.go +++ b/openapi3filter/options.go @@ -15,6 +15,12 @@ type Options struct { // Set ExcludeResponseBody so ValidateResponse skips response body validation ExcludeResponseBody bool + // Set ExcludeReadOnlyValidations so ValidateRequest skips read-only validations + ExcludeReadOnlyValidations bool + + // Set ExcludeWriteOnlyValidations so ValidateResponse skips write-only validations + ExcludeWriteOnlyValidations bool + // Set IncludeResponseStatus so ValidateResponse fails on response // status not defined in OpenAPI spec IncludeResponseStatus bool diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 7245cbe03..a8106a7c8 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -272,7 +272,7 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req } defaultsSet := false - opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential opts here + opts := make([]openapi3.SchemaValidationOption, 0, 4) // 4 potential opts here opts = append(opts, openapi3.VisitAsRequest()) if !options.SkipSettingDefaults { opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true })) @@ -283,6 +283,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req if options.customSchemaErrorFunc != nil { opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc)) } + if options.ExcludeReadOnlyValidations { + opts = append(opts, openapi3.DisableReadOnlyValidation()) + } // Validate JSON with the schema if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index c1be31928..08ea4e19d 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -63,13 +63,16 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error return &ResponseError{Input: input, Reason: "response has not been resolved"} } - opts := make([]openapi3.SchemaValidationOption, 0, 2) + opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential options here if options.MultiError { opts = append(opts, openapi3.MultiErrors()) } if options.customSchemaErrorFunc != nil { opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc)) } + if options.ExcludeWriteOnlyValidations { + opts = append(opts, openapi3.DisableWriteOnlyValidation()) + } headers := make([]string, 0, len(response.Headers)) for k := range response.Headers { From 546590b163786ac18b145ca729f00bf6e85db4e1 Mon Sep 17 00:00:00 2001 From: ShouheiNishi <96609867+ShouheiNishi@users.noreply.github.com> Date: Wed, 1 Feb 2023 22:19:54 +0900 Subject: [PATCH 252/260] openapi3: fix resolving Callbacks (#757) Co-authored-by: Pierre Fenoll fix https://github.com/getkin/kin-openapi/issues/341 --- openapi3/internalize_refs.go | 90 +++++++++++++++++++------ openapi3/issue341_test.go | 39 ++++++++++- openapi3/issue753_test.go | 20 ++++++ openapi3/loader.go | 118 +++++++++++---------------------- openapi3/testdata/issue753.yml | 53 +++++++++++++++ 5 files changed, 217 insertions(+), 103 deletions(-) create mode 100644 openapi3/issue753_test.go create mode 100644 openapi3/testdata/issue753.yml diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index acb83cd0c..b8506535e 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -78,11 +78,16 @@ func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolve return false } name := refNameResolver(p.Ref) - if _, ok := doc.Components.Parameters[name]; ok { - p.Ref = "#/components/parameters/" + name - return true + if doc.Components != nil { + if _, ok := doc.Components.Parameters[name]; ok { + p.Ref = "#/components/parameters/" + name + return true + } } + if doc.Components == nil { + doc.Components = &Components{} + } if doc.Components.Parameters == nil { doc.Components.Parameters = make(ParametersMap) } @@ -96,9 +101,15 @@ func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver, par return false } name := refNameResolver(h.Ref) - if _, ok := doc.Components.Headers[name]; ok { - h.Ref = "#/components/headers/" + name - return true + if doc.Components != nil { + if _, ok := doc.Components.Headers[name]; ok { + h.Ref = "#/components/headers/" + name + return true + } + } + + if doc.Components == nil { + doc.Components = &Components{} } if doc.Components.Headers == nil { doc.Components.Headers = make(Headers) @@ -113,9 +124,15 @@ func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameRes return false } name := refNameResolver(r.Ref) - if _, ok := doc.Components.RequestBodies[name]; ok { - r.Ref = "#/components/requestBodies/" + name - return true + if doc.Components != nil { + if _, ok := doc.Components.RequestBodies[name]; ok { + r.Ref = "#/components/requestBodies/" + name + return true + } + } + + if doc.Components == nil { + doc.Components = &Components{} } if doc.Components.RequestBodies == nil { doc.Components.RequestBodies = make(RequestBodies) @@ -130,9 +147,15 @@ func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver, return false } name := refNameResolver(r.Ref) - if _, ok := doc.Components.Responses[name]; ok { - r.Ref = "#/components/responses/" + name - return true + if doc.Components != nil { + if _, ok := doc.Components.Responses[name]; ok { + r.Ref = "#/components/responses/" + name + return true + } + } + + if doc.Components == nil { + doc.Components = &Components{} } if doc.Components.Responses == nil { doc.Components.Responses = make(Responses) @@ -147,9 +170,15 @@ func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver Ref return } name := refNameResolver(ss.Ref) - if _, ok := doc.Components.SecuritySchemes[name]; ok { - ss.Ref = "#/components/securitySchemes/" + name - return + if doc.Components != nil { + if _, ok := doc.Components.SecuritySchemes[name]; ok { + ss.Ref = "#/components/securitySchemes/" + name + return + } + } + + if doc.Components == nil { + doc.Components = &Components{} } if doc.Components.SecuritySchemes == nil { doc.Components.SecuritySchemes = make(SecuritySchemes) @@ -164,9 +193,15 @@ func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver, p return } name := refNameResolver(e.Ref) - if _, ok := doc.Components.Examples[name]; ok { - e.Ref = "#/components/examples/" + name - return + if doc.Components != nil { + if _, ok := doc.Components.Examples[name]; ok { + e.Ref = "#/components/examples/" + name + return + } + } + + if doc.Components == nil { + doc.Components = &Components{} } if doc.Components.Examples == nil { doc.Components.Examples = make(Examples) @@ -181,9 +216,15 @@ func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver, parentI return } name := refNameResolver(l.Ref) - if _, ok := doc.Components.Links[name]; ok { - l.Ref = "#/components/links/" + name - return + if doc.Components != nil { + if _, ok := doc.Components.Links[name]; ok { + l.Ref = "#/components/links/" + name + return + } + } + + if doc.Components == nil { + doc.Components = &Components{} } if doc.Components.Links == nil { doc.Components.Links = make(Links) @@ -198,6 +239,10 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver, return false } name := refNameResolver(c.Ref) + + if doc.Components == nil { + doc.Components = &Components{} + } if doc.Components.Callbacks == nil { doc.Components.Callbacks = make(Callbacks) } @@ -293,6 +338,9 @@ func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver, p func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver, parentIsExternal bool) { for _, ops := range paths { + if isExternalRef(ops.Ref, parentIsExternal) { + parentIsExternal = true + } // inline full operations ops.Ref = "" diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go index 15ea9d48c..ba9bed76b 100644 --- a/openapi3/issue341_test.go +++ b/openapi3/issue341_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -20,7 +21,43 @@ 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":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`, 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) + + doc.InternalizeRefs(context.Background(), nil) + bs, err = doc.MarshalJSON() + require.NoError(t, err) + require.JSONEq(t, `{ + "components": { + "responses": { + "testpath_200_response": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "description": "a custom response" + } + } + }, + "info": { + "title": "test file", + "version": "n/a" + }, + "openapi": "3.0.0", + "paths": { + "/testpath": { + "get": { + "responses": { + "200": { + "$ref": "#/components/responses/testpath_200_response" + } + } + } + } + } + }`, string(bs)) } diff --git a/openapi3/issue753_test.go b/openapi3/issue753_test.go new file mode 100644 index 000000000..4390641a4 --- /dev/null +++ b/openapi3/issue753_test.go @@ -0,0 +1,20 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue753(t *testing.T) { + loader := NewLoader() + + doc, err := loader.LoadFromFile("testdata/issue753.yml") + require.NoError(t, err) + + 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) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 72ab8c46a..03d45ff7f 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -44,6 +44,7 @@ type Loader struct { visitedDocuments map[string]*T + visitedCallback map[*Callback]struct{} visitedExample map[*Example]struct{} visitedHeader map[*Header]struct{} visitedLink map[*Link]struct{} @@ -243,11 +244,11 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { } // Visit all operations - for entrypoint, pathItem := range doc.Paths { + for _, pathItem := range doc.Paths { if pathItem == nil { continue } - if err = loader.resolvePathItemRef(doc, entrypoint, pathItem, location); err != nil { + if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil { return } } @@ -850,6 +851,16 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP } func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) { + if component != nil && component.Value != nil { + if loader.visitedCallback == nil { + loader.visitedCallback = make(map[*Callback]struct{}) + } + if _, ok := loader.visitedCallback[component.Value]; ok { + return nil + } + loader.visitedCallback[component.Value] = struct{}{} + } + if component == nil { return errors.New("invalid callback: value MUST be an object") } @@ -878,57 +889,8 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen return nil } - for entrypoint, pathItem := range *value { - entrypoint, pathItem := entrypoint, pathItem - err = func() (err error) { - key := "-" - if documentPath != nil { - key = documentPath.EscapedPath() - } - key += entrypoint - if _, ok := loader.visitedPathItemRefs[key]; ok { - return nil - } - loader.visitedPathItemRefs[key] = struct{}{} - - if pathItem == nil { - return errors.New("invalid path item: value MUST be an object") - } - ref := pathItem.Ref - if ref != "" { - if isSingleRefElement(ref) { - var p PathItem - if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { - return err - } - *pathItem = p - } else { - if doc, ref, documentPath, err = loader.resolveRef(doc, ref, documentPath); err != nil { - return - } - - rest := strings.TrimPrefix(ref, "#/components/callbacks/") - if rest == ref { - return fmt.Errorf(`expected prefix "#/components/callbacks/" in URI %q`, ref) - } - id := unescapeRefString(rest) - - if doc.Components == nil || doc.Components.Callbacks == nil { - return failedToResolveRefFragmentPart(ref, "callbacks") - } - resolved := doc.Components.Callbacks[id] - if resolved == nil { - return failedToResolveRefFragmentPart(ref, id) - } - - for _, p := range *resolved.Value { - *pathItem = *p - break - } - } - } - return loader.resolvePathItemRefContinued(doc, pathItem, documentPath) - }() + for _, pathItem := range *value { + err := loader.resolvePathItemRef(doc, pathItem, documentPath) if err != nil { return err } @@ -973,22 +935,27 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u return nil } -func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) { - key := "_" - if documentPath != nil { - key = documentPath.EscapedPath() - } - key += entrypoint - if _, ok := loader.visitedPathItemRefs[key]; ok { - return nil - } - loader.visitedPathItemRefs[key] = struct{}{} - +func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) { if pathItem == nil { return errors.New("invalid path item: value MUST be an object") } ref := pathItem.Ref if ref != "" { + if pathItem.Summary != "" || + pathItem.Description != "" || + pathItem.Connect != nil || + pathItem.Delete != nil || + pathItem.Get != nil || + pathItem.Head != nil || + pathItem.Options != nil || + pathItem.Patch != nil || + pathItem.Post != nil || + pathItem.Put != nil || + pathItem.Trace != nil || + len(pathItem.Servers) != 0 || + len(pathItem.Parameters) != 0 { + return nil + } if isSingleRefElement(ref) { var p PathItem if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { @@ -996,25 +963,14 @@ func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *Pa } *pathItem = p } else { - if doc, ref, documentPath, err = loader.resolveRef(doc, ref, documentPath); err != nil { - return - } - - rest := strings.TrimPrefix(ref, "#/paths/") - if rest == ref { - return fmt.Errorf(`expected prefix "#/paths/" in URI %q`, ref) - } - id := unescapeRefString(rest) - - if doc.Paths == nil { - return failedToResolveRefFragmentPart(ref, "paths") - } - resolved := doc.Paths[id] - if resolved == nil { - return failedToResolveRefFragmentPart(ref, id) + var resolved PathItem + doc, documentPath, err = loader.resolveComponent(doc, ref, documentPath, &resolved) + if err != nil { + return err } - *pathItem = *resolved + *pathItem = resolved } + pathItem.Ref = ref } return loader.resolvePathItemRefContinued(doc, pathItem, documentPath) } diff --git a/openapi3/testdata/issue753.yml b/openapi3/testdata/issue753.yml new file mode 100644 index 000000000..2123a6dbd --- /dev/null +++ b/openapi3/testdata/issue753.yml @@ -0,0 +1,53 @@ +openapi: '3' +info: + version: 0.0.1 + title: 'test' +paths: + /test1: + post: + requestBody: + content: + application/json: + schema: + type: object + responses: + '200': + description: 'test' + callbacks: + callback1: + '{$request.body#/callback}': + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/test' + responses: + '200': + description: 'test' + /test2: + post: + requestBody: + content: + application/json: + schema: + type: object + responses: + '200': + description: 'test' + callbacks: + callback2: + '{$request.body#/callback}': + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/test' + responses: + '200': + description: 'test' +components: + schemas: + test: + type: string From ac2fd9baa088ff49d0ff43fbf658a4f78805ca92 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 1 Feb 2023 14:41:34 +0100 Subject: [PATCH 253/260] fixup some coding style divergences (#760) --- openapi3/loader.go | 8 +++----- openapi3/schema.go | 7 +++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openapi3/loader.go b/openapi3/loader.go index 03d45ff7f..7f389cdfe 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -877,7 +877,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen if err != nil { return err } - if err := loader.resolveCallbackRef(doc, &resolved, componentPath); err != nil { + if err = loader.resolveCallbackRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value @@ -890,8 +890,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen } for _, pathItem := range *value { - err := loader.resolvePathItemRef(doc, pathItem, documentPath) - if err != nil { + if err = loader.resolvePathItemRef(doc, pathItem, documentPath); err != nil { return err } } @@ -964,8 +963,7 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat *pathItem = p } else { var resolved PathItem - doc, documentPath, err = loader.resolveComponent(doc, ref, documentPath, &resolved) - if err != nil { + if doc, documentPath, err = loader.resolveComponent(doc, ref, documentPath, &resolved); err != nil { return err } *pathItem = resolved diff --git a/openapi3/schema.go b/openapi3/schema.go index 5f867ff44..b6be8c1bd 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1124,9 +1124,9 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf // Catch slice of non-empty interface type if reflect.TypeOf(value).Kind() == reflect.Slice { valueR := reflect.ValueOf(value) - newValue := make([]interface{}, valueR.Len()) + newValue := make([]interface{}, 0, valueR.Len()) for i := 0; i < valueR.Len(); i++ { - newValue[i] = valueR.Index(i).Interface() + newValue = append(newValue, valueR.Index(i).Interface()) } return schema.visitJSONArray(settings, newValue) } @@ -1146,8 +1146,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val switch c := value.(type) { case json.Number: var f float64 - f, err = strconv.ParseFloat(c.String(), 64) - if err != nil { + if f, err = strconv.ParseFloat(c.String(), 64); err != nil { return err } if v == f { From 409e0dc4812862bcbb6713a67f8a5e8cfd6828f9 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Wed, 1 Feb 2023 16:44:19 +0100 Subject: [PATCH 254/260] openapi3: make `bad data ...` error more actionable (#761) --- .github/workflows/go.yml | 7 +++++++ openapi3/issue759_test.go | 34 ++++++++++++++++++++++++++++++++++ openapi3/loader.go | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 openapi3/issue759_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8dbb0620f..9d9ff726f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -176,6 +176,13 @@ jobs: Tag XML + - if: runner.os == 'Linux' + name: Ensure readableType() covers all possible values of resolved var + run: | + [[ "$(git grep -F 'var resolved ' -- openapi3/loader.go | awk '{print $4}' | sort | tr '\n' ' ')" = "$RESOLVEDS" ]] + env: + RESOLVEDS: 'Callback CallbackRef ExampleRef HeaderRef LinkRef ParameterRef PathItem RequestBodyRef ResponseRef SchemaRef SecuritySchemeRef ' + check-goimports: runs-on: ubuntu-latest steps: diff --git a/openapi3/issue759_test.go b/openapi3/issue759_test.go new file mode 100644 index 000000000..255d8b7b6 --- /dev/null +++ b/openapi3/issue759_test.go @@ -0,0 +1,34 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue759(t *testing.T) { + spec := []byte(` +openapi: 3.0.0 +info: + title: title + description: description + version: 0.0.0 +paths: + /slash: + get: + responses: + "200": + # Ref should point to a response, not a schema + $ref: "#/components/schemas/UserStruct" +components: + schemas: + UserStruct: + type: object +`[1:]) + + loader := NewLoader() + + doc, err := loader.LoadFromData(spec) + require.Nil(t, doc) + require.EqualError(t, err, `bad data in "#/components/schemas/UserStruct" (expecting ref to response object)`) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 7f389cdfe..51020111e 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -351,12 +351,41 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv return nil } if err := codec(cursor, resolved); err != nil { - return nil, nil, fmt.Errorf("bad data in %q", ref) + return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved)) } return componentDoc, componentPath, nil default: - return nil, nil, fmt.Errorf("bad data in %q", ref) + return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved)) + } +} + +func readableType(x interface{}) string { + switch x.(type) { + case *Callback: + return "callback object" + case *CallbackRef: + return "ref to callback object" + case *ExampleRef: + return "ref to example object" + case *HeaderRef: + return "ref to header object" + case *LinkRef: + return "ref to link object" + case *ParameterRef: + return "ref to parameter object" + case *PathItem: + return "pathItem object" + case *RequestBodyRef: + return "ref to requestBody object" + case *ResponseRef: + return "ref to response object" + case *SchemaRef: + return "ref to schema object" + case *SecuritySchemeRef: + return "ref to securityScheme object" + default: + panic(fmt.Sprintf("unreachable %T", x)) } } From ecb06bc5c05b010b9c1322486ff74f2178f7927c Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 2 Feb 2023 15:35:40 +0100 Subject: [PATCH 255/260] openapi3: add test from #731 showing validating doc first is required (#762) closes https://github.com/getkin/kin-openapi/issues/731 --- openapi3/loader_test.go | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 3515586a0..9756f55fc 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -55,18 +55,53 @@ paths: `) loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) require.Equal(t, "An API", doc.Info.Title) require.Equal(t, 2, len(doc.Components.Schemas)) require.Equal(t, 1, len(doc.Paths)) - def := doc.Paths["/items"].Put.Responses.Default().Value - desc := "unexpected error" - require.Equal(t, &desc, def.Description) + require.Equal(t, "unexpected error", *doc.Paths["/items"].Put.Responses.Default().Value.Description) + err = doc.Validate(loader.Context) require.NoError(t, err) } +func TestIssue731(t *testing.T) { + spec := []byte(` +openapi: 3.0.0 +info: + title: An API + version: v1 +paths: + /items: + put: + description: '' + requestBody: + required: true + # Note mis-indented content block + content: + application/json: + schema: + type: object + responses: + default: + description: unexpected error + content: + application/json: + schema: + type: object +`[1:]) + + loader := NewLoader() + + doc, err := loader.LoadFromData(spec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.ErrorContains(t, err, `content of the request body is required`) +} + func ExampleLoader() { const source = `{"info":{"description":"An API"}}` doc, err := NewLoader().LoadFromData([]byte(source)) From 28d8a4ee48f85f3e66369c6f6f3d852965bca547 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 13 Feb 2023 15:40:36 +0100 Subject: [PATCH 256/260] cmd/validate: more expressive errors (#769) --- cmd/validate/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/validate/main.go b/cmd/validate/main.go index 9759564fa..d8c0fe6ad 100644 --- a/cmd/validate/main.go +++ b/cmd/validate/main.go @@ -59,7 +59,7 @@ func main() { doc, err := loader.LoadFromFile(filename) if err != nil { - log.Fatal(err) + log.Fatalln("Loading error:", err) } var opts []openapi3.ValidationOption @@ -74,7 +74,7 @@ func main() { } if err = doc.Validate(loader.Context, opts...); err != nil { - log.Fatal(err) + log.Fatalln("Validation error:", err) } case vd.Swagger == "2" || strings.HasPrefix(vd.Swagger, "2."): @@ -93,7 +93,7 @@ func main() { var doc openapi2.T if err := yaml.Unmarshal(data, &doc); err != nil { - log.Fatal(err) + log.Fatalln("Loading error:", err) } default: From 47d329de64b53c80e00aa1ae9aff91df509af418 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 13 Feb 2023 15:42:46 +0100 Subject: [PATCH 257/260] openapi3: fix an infinite loop that may have been introduced in #700 (#768) --- openapi3/issue542_test.go | 25 +++++++++++++++++++- openapi3/issue615_test.go | 11 +++++++-- openapi3/loader.go | 12 ---------- openapi3/testdata/issue542.yml | 43 ---------------------------------- 4 files changed, 33 insertions(+), 58 deletions(-) delete mode 100644 openapi3/testdata/issue542.yml diff --git a/openapi3/issue542_test.go b/openapi3/issue542_test.go index 4ba017aed..05f5db64d 100644 --- a/openapi3/issue542_test.go +++ b/openapi3/issue542_test.go @@ -7,8 +7,31 @@ import ( ) func TestIssue542(t *testing.T) { + spec := []byte(` +openapi: '3.0.0' +info: + version: '1.0.0' + title: Swagger Petstore + license: + name: MIT +servers: +- url: http://petstore.swagger.io/v1 +paths: {} +components: + schemas: + Cat: + anyOf: + - $ref: '#/components/schemas/Kitten' + - type: object + Kitten: + type: string +`[1:]) + sl := NewLoader() - _, err := sl.LoadFromFile("testdata/issue542.yml") + doc, err := sl.LoadFromData(spec) + require.NoError(t, err) + + doc.Validate(sl.Context) require.NoError(t, err) } diff --git a/openapi3/issue615_test.go b/openapi3/issue615_test.go index e7bd01e92..496a972bb 100644 --- a/openapi3/issue615_test.go +++ b/openapi3/issue615_test.go @@ -10,10 +10,14 @@ import ( func TestIssue615(t *testing.T) { { + var old int + old, openapi3.CircularReferenceCounter = openapi3.CircularReferenceCounter, 1 + defer func() { openapi3.CircularReferenceCounter = old }() + loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = true _, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml") - require.NoError(t, err) + require.ErrorContains(t, err, openapi3.CircularReferenceError) } var old int @@ -22,6 +26,9 @@ func TestIssue615(t *testing.T) { loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = true - _, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml") + doc, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml") + require.NoError(t, err) + + doc.Validate(loader.Context) require.NoError(t, err) } diff --git a/openapi3/loader.go b/openapi3/loader.go index 51020111e..4a14f67f0 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -504,7 +504,6 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat return err } component.Value = resolved.Value - return nil } } value := component.Value @@ -552,7 +551,6 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum return err } component.Value = resolved.Value - return nil } } value := component.Value @@ -609,7 +607,6 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d return err } component.Value = resolved.Value - return nil } } value := component.Value @@ -671,7 +668,6 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen return err } component.Value = resolved.Value - return nil } } value := component.Value @@ -754,7 +750,6 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } component.Value = resolved.Value - return nil } if loader.visitedSchema == nil { loader.visitedSchema = make(map[*Schema]struct{}) @@ -836,7 +831,6 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme return err } component.Value = resolved.Value - return nil } } return nil @@ -873,7 +867,6 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP return err } component.Value = resolved.Value - return nil } } return nil @@ -910,7 +903,6 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen return err } component.Value = resolved.Value - return nil } } value := component.Value @@ -957,7 +949,6 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u return err } component.Value = resolved.Value - return nil } } return nil @@ -999,10 +990,7 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat } pathItem.Ref = ref } - return loader.resolvePathItemRefContinued(doc, pathItem, documentPath) -} -func (loader *Loader) resolvePathItemRefContinued(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) { for _, parameter := range pathItem.Parameters { if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil { return diff --git a/openapi3/testdata/issue542.yml b/openapi3/testdata/issue542.yml deleted file mode 100644 index 887702557..000000000 --- a/openapi3/testdata/issue542.yml +++ /dev/null @@ -1,43 +0,0 @@ -openapi: "3.0.0" -info: - version: 1.0.0 - title: Swagger Petstore - license: - name: MIT -servers: - - url: http://petstore.swagger.io/v1 -paths: {} -#paths: -# /pets: -# patch: -# requestBody: -# content: -# application/json: -# schema: -# oneOf: -# - $ref: '#/components/schemas/Cat' -# - $ref: '#/components/schemas/Kitten' -# discriminator: -# propertyName: pet_type -# responses: -# '200': -# description: Updated -components: - schemas: - Cat: - anyOf: - - $ref: "#/components/schemas/Kitten" - - type: object - # properties: - # hunts: - # type: boolean - # age: - # type: integer - # offspring: - Kitten: - $ref: "#/components/schemas/Cat" #ko - -# type: string #ok - -# allOf: #ko -# - $ref: '#/components/schemas/Cat' From cb687bf864ca2b3c141f7883e5b16bd9e7b42379 Mon Sep 17 00:00:00 2001 From: orshlom <44160965+orshlom@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:23:46 +0200 Subject: [PATCH 258/260] openapi3: fix default values count even when disabled (#767) (#770) --- openapi3/issue767_test.go | 90 +++++++++++++++++++++++++++++++++++++++ openapi3/schema.go | 10 ++--- 2 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 openapi3/issue767_test.go diff --git a/openapi3/issue767_test.go b/openapi3/issue767_test.go new file mode 100644 index 000000000..d498877c9 --- /dev/null +++ b/openapi3/issue767_test.go @@ -0,0 +1,90 @@ +package openapi3_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestIssue767(t *testing.T) { + t.Parallel() + + tests := [...]struct { + name string + schema *openapi3.Schema + value map[string]interface{} + opts []openapi3.SchemaValidationOption + checkErr require.ErrorAssertionFunc + }{ + { + name: "default values disabled should fail with minProps 1", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", Default: true}}).WithMinProperties(1), + value: map[string]interface{}{}, + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest(), + }, + checkErr: require.Error, + }, + { + name: "default values enabled should pass with minProps 1", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", Default: true}}).WithMinProperties(1), + value: map[string]interface{}{}, + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest(), + openapi3.DefaultsSet(func() {}), + }, + checkErr: require.NoError, + }, + { + name: "default values enabled should pass with minProps 2", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", Default: true}, + "bar": {Type: "boolean"}, + }).WithMinProperties(2), + value: map[string]interface{}{"bar": false}, + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest(), + openapi3.DefaultsSet(func() {}), + }, + checkErr: require.NoError, + }, + { + name: "default values enabled should fail with maxProps 1", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", Default: true}, + "bar": {Type: "boolean"}, + }).WithMaxProperties(1), + value: map[string]interface{}{"bar": false}, + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest(), + openapi3.DefaultsSet(func() {}), + }, + checkErr: require.Error, + }, + { + name: "default values disabled should pass with maxProps 1", + schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ + "foo": {Type: "boolean", Default: true}, + "bar": {Type: "boolean"}, + }).WithMaxProperties(1), + value: map[string]interface{}{"bar": false}, + opts: []openapi3.SchemaValidationOption{ + openapi3.VisitAsRequest(), + }, + checkErr: require.NoError, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + err := test.schema.VisitJSON(test.value, test.opts...) + test.checkErr(t, err) + }) + } +} diff --git a/openapi3/schema.go b/openapi3/schema.go index b6be8c1bd..4bfbca0bd 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1789,12 +1789,10 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value reqRO := settings.asreq && propSchema.Value.ReadOnly && !settings.readOnlyValidationDisabled repWO := settings.asrep && propSchema.Value.WriteOnly && !settings.writeOnlyValidationDisabled - if value[propName] == nil { - if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO { - value[propName] = dlft - if f := settings.defaultsSet; f != nil { - settings.onceSettingDefaults.Do(f) - } + if f := settings.defaultsSet; f != nil && value[propName] == nil { + if dflt := propSchema.Value.Default; dflt != nil && !reqRO && !repWO { + value[propName] = dflt + settings.onceSettingDefaults.Do(f) } } From e53fe386960a0f532637da538535f22c9a9249e6 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 27 Feb 2023 11:49:09 +0100 Subject: [PATCH 259/260] openapi3: sort extra fields only once, during deserialization (#773) --- openapi3/refs.go | 27 +++++++++------------------ refs.sh | 3 +-- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/openapi3/refs.go b/openapi3/refs.go index cc9b41a45..15f5179da 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -46,6 +46,7 @@ func (x *CallbackRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -56,8 +57,6 @@ func (x *CallbackRef) UnmarshalJSON(data []byte) error { func (x *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { @@ -123,6 +122,7 @@ func (x *ExampleRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -133,8 +133,6 @@ func (x *ExampleRef) UnmarshalJSON(data []byte) error { func (x *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { @@ -200,6 +198,7 @@ func (x *HeaderRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -210,8 +209,6 @@ func (x *HeaderRef) UnmarshalJSON(data []byte) error { func (x *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { @@ -277,6 +274,7 @@ func (x *LinkRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -287,8 +285,6 @@ func (x *LinkRef) UnmarshalJSON(data []byte) error { func (x *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { @@ -354,6 +350,7 @@ func (x *ParameterRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -364,8 +361,6 @@ func (x *ParameterRef) UnmarshalJSON(data []byte) error { func (x *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { @@ -431,6 +426,7 @@ func (x *RequestBodyRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -441,8 +437,6 @@ func (x *RequestBodyRef) UnmarshalJSON(data []byte) error { func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { @@ -508,6 +502,7 @@ func (x *ResponseRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -518,8 +513,6 @@ func (x *ResponseRef) UnmarshalJSON(data []byte) error { func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { @@ -585,6 +578,7 @@ func (x *SchemaRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -595,8 +589,6 @@ func (x *SchemaRef) UnmarshalJSON(data []byte) error { func (x *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { @@ -662,6 +654,7 @@ func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -672,8 +665,6 @@ func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error { func (x *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { diff --git a/refs.sh b/refs.sh index bbcbe54bc..9ade24196 100755 --- a/refs.sh +++ b/refs.sh @@ -80,6 +80,7 @@ func (x *${type}Ref) UnmarshalJSON(data []byte) error { for key := range extra { x.extra = append(x.extra, key) } + sort.Strings(x.extra) } return nil } @@ -90,8 +91,6 @@ func (x *${type}Ref) UnmarshalJSON(data []byte) error { func (x *${type}Ref) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) if extra := x.extra; len(extra) != 0 { - sort.Strings(extra) - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed if allowed == nil { From cc09e847f314b23f0c9a9afb399208625c9af402 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Fri, 24 Mar 2023 12:53:13 +0100 Subject: [PATCH 260/260] feat: support nil uuid (#778) --- .github/docs/openapi3.txt | 2 +- openapi3/schema_formats.go | 2 +- openapi3/schema_formats_test.go | 49 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 3ce7ed959..82a5e0bc8 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -1,6 +1,6 @@ const ParameterInPath = "path" ... const TypeArray = "array" ... -const FormatOfStringForUUIDOfRFC4122 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$` ... +const FormatOfStringForUUIDOfRFC4122 = `^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$` ... const SerializationSimple = "simple" ... var SchemaErrorDetailsDisabled = false ... var CircularReferenceCounter = 3 diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index ecbc0ebfa..ea38400c2 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -9,7 +9,7 @@ import ( const ( // FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122 - FormatOfStringForUUIDOfRFC4122 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$` + FormatOfStringForUUIDOfRFC4122 = `^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$` // FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses. // Use DefineStringFormat(...) if you need something stricter. diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go index 3899fabdc..70092d6de 100644 --- a/openapi3/schema_formats_test.go +++ b/openapi3/schema_formats_test.go @@ -3,6 +3,7 @@ package openapi3 import ( "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -105,3 +106,51 @@ components: delete(SchemaStringFormats, "ipv4") SchemaErrorDetailsDisabled = false } + +func TestUuidFormat(t *testing.T) { + + type testCase struct { + name string + value string + wantErr bool + } + + DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) + testCases := []testCase{ + { + name: "invalid", + value: "foo", + wantErr: true, + }, + { + name: "uuid v1", + value: "77e66540-ca29-11ed-afa1-0242ac120002", + wantErr: false, + }, + { + name: "uuid v4", + value: "00f4d301-b9f4-4366-8907-2b5a03430aa1", + wantErr: false, + }, + { + name: "uuid nil", + value: "00000000-0000-0000-0000-000000000000", + wantErr: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := NewUUIDSchema().VisitJSON(tc.value) + var schemaError = &SchemaError{} + if tc.wantErr { + require.Error(t, err) + require.ErrorAs(t, err, &schemaError) + + require.NotZero(t, schemaError.Reason) + require.NotContains(t, schemaError.Reason, fmt.Sprint(tc.value)) + } else { + require.Nil(t, err) + } + }) + } +}