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 }