From fac3a34696b31940391873da688035afadfa4e09 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 16 Sep 2022 19:06:15 +0000 Subject: [PATCH 01/12] context examples validation options rework and test setup --- openapi3/example_validation.go | 13 +++- openapi3/example_validation_test.go | 105 +++++++++++++++++++++++++--- openapi3/media_type.go | 9 ++- openapi3/openapi3.go | 2 +- openapi3/parameter.go | 9 ++- openapi3/request_body.go | 11 ++- openapi3/response.go | 14 +++- openapi3/schema.go | 4 +- openapi3/schema_test.go | 6 +- openapi3/validation_options.go | 21 ++++-- 10 files changed, 163 insertions(+), 31 deletions(-) diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go index 4c75e360b..6d2a316b8 100644 --- a/openapi3/example_validation.go +++ b/openapi3/example_validation.go @@ -1,5 +1,14 @@ package openapi3 -func validateExampleValue(input interface{}, schema *Schema) error { - return schema.VisitJSON(input, MultiErrors()) +func validateExampleValue(input interface{}, schema *Schema, validationOpts *ValidationOptions) error { + opts := make([]SchemaValidationOption, 0, 3) + + if validationOpts.ExamplesValidation.AsReq { + opts = append(opts, VisitAsRequest()) + } else if validationOpts.ExamplesValidation.AsRes { + 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 85e158e6b..3cc12351d 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{ @@ -137,6 +140,55 @@ func TestExamplesSchemaValidation(t *testing.T) { access_token: "abcd" `, }, + { + name: "valid_readonly_writeonly_examples", + readWriteOnlyMediaTypeRequestExample: ` + examples: + BadUser: + $ref: '#/components/examples/ReadWriteOnlyRequestData' +`, + readWriteOnlyMediaTypeResponseExample: ` + examples: + BadUser: + $ref: '#/components/examples/ReadWriteOnlyResponseData' +`, + componentExamples: ` + examples: + ReadWriteOnlyRequestData: + value: + username: user + password: password + ReadWriteOnlyResponseData: + value: + user_id: 1 + `, + }, + { + name: "invalid_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 + user_id: 1 + ReadWriteOnlyResponseData: + value: + password: password + user_id: 1 + `, + errContains: "readOnly writeOnly error", + }, } testOptions := []struct { @@ -198,7 +250,24 @@ 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 + responses: + '201': + description: a response + content: + application/json: + schema: + $ref: "#/components/schemas/ReadWriteOnlyData" components: schemas: CreateUserRequest:`) @@ -223,7 +292,6 @@ components: CreateUserResponse:`) spec.WriteString(tc.responseSchemaExample) spec.WriteString(` - description: represents the response to a User creation required: - access_token - user_id @@ -234,6 +302,25 @@ 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 + writeOnly: true # only sent in a request + password: + type: string + writeOnly: true # only sent in a request + user_id: + format: int64 + type: integer + readOnly: true # only returned in a response + type: object `) spec.WriteString(tc.componentExamples) diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 01df12ad0..8825d7f33 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -85,11 +85,14 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { if mediaType.Example != nil && mediaType.Examples != nil { return errors.New("example and examples are mutually exclusive") } - if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled { + + vo := getValidationOptions(ctx) + + if vo.ExamplesValidation.Disabled { return nil } if example := mediaType.Example; example != nil { - if err := validateExampleValue(example, schema.Value); err != nil { + if err := validateExampleValue(example, schema.Value, vo); err != nil { return err } } else if examples := mediaType.Examples; examples != nil { @@ -97,7 +100,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { if err := v.Validate(ctx); err != nil { return fmt.Errorf("%s: %s", k, err) } - if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { + if err := validateExampleValue(v.Value.Value, schema.Value, vo); err != nil { return fmt.Errorf("%s: %s", k, err) } } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 20549c2b7..986d45712 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -56,7 +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{} + validationOpts := NewValidationOptions() for _, opt := range opts { opt(validationOpts) } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index f5d7d1f2f..09609db41 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -317,11 +317,14 @@ 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 { + + vo := getValidationOptions(ctx) + + if vo.ExamplesValidation.Disabled { return nil } if example := parameter.Example; example != nil { - if err := validateExampleValue(example, schema.Value); err != nil { + if err := validateExampleValue(example, schema.Value, vo); err != nil { return err } } else if examples := parameter.Examples; examples != nil { @@ -329,7 +332,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error { if err := v.Validate(ctx); err != nil { return fmt.Errorf("%s: %s", k, err) } - if err := validateExampleValue(v.Value.Value, schema.Value); err != nil { + if err := validateExampleValue(v.Value.Value, schema.Value, vo); err != nil { return fmt.Errorf("%s: %s", k, err) } } diff --git a/openapi3/request_body.go b/openapi3/request_body.go index c97563a11..812886446 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -109,5 +109,14 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error { if requestBody.Content == nil { return errors.New("content of the request body is required") } - return requestBody.Content.Validate(ctx) + + newCtx := ctx + // TODO validate writeonly + if validationOpts := getValidationOptions(ctx); !validationOpts.ExamplesValidation.Disabled { + vo := NewValidationOptions() + vo.ExamplesValidation.AsReq = true + newCtx = WithValidationOptions(context.Background(), vo) + } + + return requestBody.Content.Validate(newCtx) } diff --git a/openapi3/response.go b/openapi3/response.go index 31ea257d1..02b599d83 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -107,20 +107,28 @@ func (response *Response) Validate(ctx context.Context) error { if response.Description == nil { return errors.New("a short description of the response is required") } + newCtx := ctx + // TODO validate readonly + if validationOpts := getValidationOptions(ctx); !validationOpts.ExamplesValidation.Disabled { + + vo := NewValidationOptions() + vo.ExamplesValidation.AsRes = true + newCtx = WithValidationOptions(context.Background(), vo) + } if content := response.Content; content != nil { - if err := content.Validate(ctx); err != nil { + if err := content.Validate(newCtx); err != nil { return err } } for _, header := range response.Headers { - if err := header.Validate(ctx); err != nil { + if err := header.Validate(newCtx); err != nil { return err } } for _, link := range response.Links { - if err := link.Validate(ctx); err != nil { + if err := link.Validate(newCtx); err != nil { return err } } diff --git a/openapi3/schema.go b/openapi3/schema.go index c59070ee1..df81ad73b 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -753,8 +753,8 @@ 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 x := schema.Example; x != nil && !validationOpts.ExamplesValidation.Disabled { + if err := validateExampleValue(x, schema, validationOpts); err != nil { return fmt.Errorf("invalid schema example: %s", err) } } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 4c14dcb10..01a21077d 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1028,9 +1028,9 @@ 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, - }) + vo := NewValidationOptions() + vo.SchemaFormatValidationEnabled = true + ctx := WithValidationOptions(context.Background(), vo) err := schema.Validate(ctx) require.Error(t, err) } diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 5c0d01d2f..a95420ca2 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -5,11 +5,24 @@ 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. +// ExamplesValidation provides configuration for validating examples against their schema. +type ExamplesValidation struct { + Disabled bool + AsReq bool + AsRes bool +} + +// ValidationOptions provides configuration for validating OpenAPI documents. type ValidationOptions struct { SchemaFormatValidationEnabled bool SchemaPatternValidationDisabled bool - ExamplesValidationDisabled bool + ExamplesValidation *ExamplesValidation +} + +func NewValidationOptions() *ValidationOptions { + return &ValidationOptions{ + ExamplesValidation: &ExamplesValidation{}, + } } type validationOptionsKey struct{} @@ -31,7 +44,7 @@ func DisableSchemaPatternValidation() ValidationOption { // DisableExamplesValidation disables all example schema validation. func DisableExamplesValidation() ValidationOption { return func(options *ValidationOptions) { - options.ExamplesValidationDisabled = true + options.ExamplesValidation.Disabled = true } } @@ -44,5 +57,5 @@ func getValidationOptions(ctx context.Context) *ValidationOptions { if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok { return options } - return &ValidationOptions{} + return NewValidationOptions() } From d616bf71f898aaa8e97029f61ad8da9fdd0ed6a4 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 16 Sep 2022 20:54:48 +0000 Subject: [PATCH 02/12] tests passing --- openapi3/example_validation.go | 6 +-- openapi3/example_validation_test.go | 82 +++++++++++++++++------------ openapi3/schema.go | 13 ++++- openapi3/validation_options.go | 5 +- 4 files changed, 63 insertions(+), 43 deletions(-) diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go index 6d2a316b8..9daee730a 100644 --- a/openapi3/example_validation.go +++ b/openapi3/example_validation.go @@ -1,11 +1,11 @@ package openapi3 -func validateExampleValue(input interface{}, schema *Schema, validationOpts *ValidationOptions) error { +func validateExampleValue(input interface{}, schema *Schema, vo *ValidationOptions) error { opts := make([]SchemaValidationOption, 0, 3) - if validationOpts.ExamplesValidation.AsReq { + if vo.ExamplesValidation.AsReq { opts = append(opts, VisitAsRequest()) - } else if validationOpts.ExamplesValidation.AsRes { + } else if vo.ExamplesValidation.AsRes { opts = append(opts, VisitAsResponse()) } opts = append(opts, MultiErrors()) diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index 3cc12351d..045ffa9fa 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -138,19 +138,19 @@ func TestExamplesSchemaValidation(t *testing.T) { example: user_id: 1 access_token: "abcd" - `, + `, }, { name: "valid_readonly_writeonly_examples", readWriteOnlyMediaTypeRequestExample: ` examples: - BadUser: + ReadWriteOnlyRequest: $ref: '#/components/examples/ReadWriteOnlyRequestData' `, readWriteOnlyMediaTypeResponseExample: ` - examples: - BadUser: - $ref: '#/components/examples/ReadWriteOnlyResponseData' + examples: + ReadWriteOnlyResponse: + $ref: '#/components/examples/ReadWriteOnlyResponseData' `, componentExamples: ` examples: @@ -161,33 +161,41 @@ func TestExamplesSchemaValidation(t *testing.T) { ReadWriteOnlyResponseData: value: user_id: 1 - `, + `, }, { - name: "invalid_readonly_writeonly_examples", + name: "invalid_readonly_request_examples", readWriteOnlyMediaTypeRequestExample: ` - examples: - ReadWriteOnlyRequest: - $ref: '#/components/examples/ReadWriteOnlyRequestData' - `, - readWriteOnlyMediaTypeResponseExample: ` examples: - ReadWriteOnlyResponse: - $ref: '#/components/examples/ReadWriteOnlyResponseData' - `, + ReadWriteOnlyRequest: + $ref: '#/components/examples/ReadWriteOnlyRequestData' +`, + componentExamples: ` + examples: + ReadWriteOnlyRequestData: + value: + username: user + password: password + user_id: 4321 +`, + errContains: "ReadWriteOnlyRequest: readOnly property", + }, + { + name: "invalid_writeonly_response_examples", + readWriteOnlyMediaTypeResponseExample: ` + examples: + ReadWriteOnlyResponse: + $ref: '#/components/examples/ReadWriteOnlyResponseData' +`, componentExamples: ` - examples: - ReadWriteOnlyRequestData: - value: - username: user - password: password - user_id: 1 - ReadWriteOnlyResponseData: - value: - password: password - user_id: 1 - `, - errContains: "readOnly writeOnly error", + examples: + ReadWriteOnlyResponseData: + value: + password: password + user_id: 0987 +`, + + errContains: "ReadWriteOnlyResponse: writeOnly property", }, } @@ -260,14 +268,18 @@ paths: application/json: schema: $ref: "#/components/schemas/ReadWriteOnlyData" - required: true + required: true`) + spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample) + spec.WriteString(` responses: '201': description: a response content: application/json: schema: - $ref: "#/components/schemas/ReadWriteOnlyData" + $ref: "#/components/schemas/ReadWriteOnlyData"`) + spec.WriteString(tc.readWriteOnlyMediaTypeResponseExample) + spec.WriteString(` components: schemas: CreateUserRequest:`) @@ -303,12 +315,12 @@ components: type: integer type: object ReadWriteOnlyData: - required: - # only required in request - - username - - password - # only required in response - - user_id + #required: + # # only required in request + # - username + # - password + # # only required in response + # - user_id properties: username: type: string diff --git a/openapi3/schema.go b/openapi3/schema.go index df81ad73b..11484b8e0 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -754,6 +754,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } if x := schema.Example; x != nil && !validationOpts.ExamplesValidation.Disabled { + fmt.Printf("validating schema %v with opts: %#v\n", schema.Example, validationOpts.ExamplesValidation) + if err := validateExampleValue(x, schema, validationOpts); err != nil { return fmt.Errorf("invalid schema example: %s", err) } @@ -1410,6 +1412,8 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return schema.expectedType(settings, TypeObject) } + var me MultiError + if settings.asreq || settings.asrep { for propName, propSchema := range schema.Properties { if value[propName] == nil { @@ -1419,12 +1423,17 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value settings.onceSettingDefaults.Do(f) } } + } else { + if settings.asreq && propSchema.Value.ReadOnly { + me = append(me, fmt.Errorf("readOnly property %q in request example", propName)) + } + if settings.asrep && propSchema.Value.WriteOnly { + me = append(me, fmt.Errorf("writeOnly property %q in response example", 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 a95420ca2..2b8f386f6 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -7,9 +7,8 @@ type ValidationOption func(options *ValidationOptions) // ExamplesValidation provides configuration for validating examples against their schema. type ExamplesValidation struct { - Disabled bool - AsReq bool - AsRes bool + Disabled bool + AsReq, AsRes bool } // ValidationOptions provides configuration for validating OpenAPI documents. From e7002c9e265b431a73c4774bbd7ec60b67d9d59e Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Sat, 17 Sep 2022 07:35:23 +0000 Subject: [PATCH 03/12] TODO fix external tests --- openapi3/example_validation.go | 6 ++++-- openapi3/example_validation_test.go | 20 ++++++++++---------- openapi3/media_type.go | 8 +++----- openapi3/parameter.go | 8 +++----- openapi3/request_body.go | 6 ++---- openapi3/response.go | 7 ++----- openapi3/schema.go | 15 ++++++--------- openapi3/schema_validation_settings.go | 6 +++--- 8 files changed, 33 insertions(+), 43 deletions(-) diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go index 9daee730a..92026fddf 100644 --- a/openapi3/example_validation.go +++ b/openapi3/example_validation.go @@ -1,9 +1,11 @@ package openapi3 -func validateExampleValue(input interface{}, schema *Schema, vo *ValidationOptions) error { +import "context" + +func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error { opts := make([]SchemaValidationOption, 0, 3) - if vo.ExamplesValidation.AsReq { + if vo := getValidationOptions(ctx); vo.ExamplesValidation.AsReq { opts = append(opts, VisitAsRequest()) } else if vo.ExamplesValidation.AsRes { opts = append(opts, VisitAsResponse()) diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index 045ffa9fa..f2b243d1e 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -160,7 +160,7 @@ func TestExamplesSchemaValidation(t *testing.T) { password: password ReadWriteOnlyResponseData: value: - user_id: 1 + user_id: 4321 `, }, { @@ -178,7 +178,7 @@ func TestExamplesSchemaValidation(t *testing.T) { password: password user_id: 4321 `, - errContains: "ReadWriteOnlyRequest: readOnly property", + errContains: "ReadWriteOnlyRequest: readOnly property \"user_id\" in request", }, { name: "invalid_writeonly_response_examples", @@ -192,10 +192,10 @@ func TestExamplesSchemaValidation(t *testing.T) { ReadWriteOnlyResponseData: value: password: password - user_id: 0987 + user_id: 4321 `, - errContains: "ReadWriteOnlyResponse: writeOnly property", + errContains: "ReadWriteOnlyResponse: writeOnly property \"password\" in response", }, } @@ -315,12 +315,12 @@ components: type: integer type: object ReadWriteOnlyData: - #required: - # # only required in request - # - username - # - password - # # only required in response - # - user_id + required: + # only required in request + - username + - password + # only required in response + - user_id properties: username: type: string diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 8825d7f33..afea3b5dd 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -86,13 +86,11 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { return errors.New("example and examples are mutually exclusive") } - vo := getValidationOptions(ctx) - - if vo.ExamplesValidation.Disabled { + if vo := getValidationOptions(ctx); vo.ExamplesValidation.Disabled { return nil } if example := mediaType.Example; example != nil { - if err := validateExampleValue(example, schema.Value, vo); err != nil { + if err := validateExampleValue(ctx, example, schema.Value); err != nil { return err } } else if examples := mediaType.Examples; examples != nil { @@ -100,7 +98,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { if err := v.Validate(ctx); err != nil { return fmt.Errorf("%s: %s", k, err) } - if err := validateExampleValue(v.Value.Value, schema.Value, vo); err != nil { + if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil { return fmt.Errorf("%s: %s", k, err) } } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 09609db41..9efebb955 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -318,13 +318,11 @@ func (parameter *Parameter) Validate(ctx context.Context) error { return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name) } - vo := getValidationOptions(ctx) - - if vo.ExamplesValidation.Disabled { + if vo := getValidationOptions(ctx); vo.ExamplesValidation.Disabled { return nil } if example := parameter.Example; example != nil { - if err := validateExampleValue(example, schema.Value, vo); err != nil { + if err := validateExampleValue(ctx, example, schema.Value); err != nil { return err } } else if examples := parameter.Examples; examples != nil { @@ -332,7 +330,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error { if err := v.Validate(ctx); err != nil { return fmt.Errorf("%s: %s", k, err) } - if err := validateExampleValue(v.Value.Value, schema.Value, vo); err != nil { + if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil { return fmt.Errorf("%s: %s", k, err) } } diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 812886446..1be207923 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -111,10 +111,8 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error { } newCtx := ctx - // TODO validate writeonly - if validationOpts := getValidationOptions(ctx); !validationOpts.ExamplesValidation.Disabled { - vo := NewValidationOptions() - vo.ExamplesValidation.AsReq = true + if vo := getValidationOptions(ctx); !vo.ExamplesValidation.Disabled { + vo.ExamplesValidation.AsReq, vo.ExamplesValidation.AsRes = true, false newCtx = WithValidationOptions(context.Background(), vo) } diff --git a/openapi3/response.go b/openapi3/response.go index 02b599d83..5e592b084 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -108,11 +108,8 @@ func (response *Response) Validate(ctx context.Context) error { return errors.New("a short description of the response is required") } newCtx := ctx - // TODO validate readonly - if validationOpts := getValidationOptions(ctx); !validationOpts.ExamplesValidation.Disabled { - - vo := NewValidationOptions() - vo.ExamplesValidation.AsRes = true + if vo := getValidationOptions(ctx); !vo.ExamplesValidation.Disabled { + vo.ExamplesValidation.AsReq, vo.ExamplesValidation.AsRes = false, true newCtx = WithValidationOptions(context.Background(), vo) } diff --git a/openapi3/schema.go b/openapi3/schema.go index 11484b8e0..436770c1b 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -754,9 +754,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } if x := schema.Example; x != nil && !validationOpts.ExamplesValidation.Disabled { - fmt.Printf("validating schema %v with opts: %#v\n", schema.Example, validationOpts.ExamplesValidation) - - if err := validateExampleValue(x, schema, validationOpts); err != nil { + if err := validateExampleValue(ctx, x, schema); err != nil { return fmt.Errorf("invalid schema example: %s", err) } } @@ -1414,7 +1412,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value var me MultiError - if settings.asreq || settings.asrep { + if settings.asReq || settings.asRes { for propName, propSchema := range schema.Properties { if value[propName] == nil { if dlft := propSchema.Value.Default; dlft != nil { @@ -1424,10 +1422,9 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value } } } else { - if settings.asreq && propSchema.Value.ReadOnly { + if settings.asReq && propSchema.Value.ReadOnly { me = append(me, fmt.Errorf("readOnly property %q in request example", propName)) - } - if settings.asrep && propSchema.Value.WriteOnly { + } else if settings.asRes && propSchema.Value.WriteOnly { me = append(me, fmt.Errorf("writeOnly property %q in response example", propName)) } } @@ -1540,10 +1537,10 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value // "required" for _, k := range schema.Required { if _, ok := value[k]; !ok { - if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asreq { + 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 { + if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asRes { continue } if settings.failfast { diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 854ae8480..c47224349 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -10,7 +10,7 @@ type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { failfast bool multiError bool - asreq, asrep bool // exclusive (XOR) fields + asReq, asRes bool // exclusive (XOR) fields formatValidationEnabled bool patternValidationDisabled bool @@ -28,11 +28,11 @@ func MultiErrors() SchemaValidationOption { } func VisitAsRequest() SchemaValidationOption { - return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false } + return func(s *schemaValidationSettings) { s.asReq, s.asRes = true, false } } func VisitAsResponse() SchemaValidationOption { - return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true } + return func(s *schemaValidationSettings) { s.asReq, s.asRes = 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. From 8122339954dff1bdc61120e003b9c3306400821e Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Sat, 17 Sep 2022 08:52:48 +0000 Subject: [PATCH 04/12] remove new ctx. fix validate response --- openapi3/request_body.go | 4 +--- openapi3/response.go | 8 +++----- openapi3filter/validate_response.go | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 1be207923..553ebf635 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -110,11 +110,9 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error { return errors.New("content of the request body is required") } - newCtx := ctx if vo := getValidationOptions(ctx); !vo.ExamplesValidation.Disabled { vo.ExamplesValidation.AsReq, vo.ExamplesValidation.AsRes = true, false - newCtx = WithValidationOptions(context.Background(), vo) } - return requestBody.Content.Validate(newCtx) + return requestBody.Content.Validate(ctx) } diff --git a/openapi3/response.go b/openapi3/response.go index 5e592b084..f0b60135c 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -107,25 +107,23 @@ func (response *Response) Validate(ctx context.Context) error { if response.Description == nil { return errors.New("a short description of the response is required") } - newCtx := ctx if vo := getValidationOptions(ctx); !vo.ExamplesValidation.Disabled { vo.ExamplesValidation.AsReq, vo.ExamplesValidation.AsRes = false, true - newCtx = WithValidationOptions(context.Background(), vo) } if content := response.Content; content != nil { - if err := content.Validate(newCtx); err != nil { + if err := content.Validate(ctx); err != nil { return err } } for _, header := range response.Headers { - if err := header.Validate(newCtx); err != nil { + if err := header.Validate(ctx); err != nil { return err } } for _, link := range response.Links { - if err := link.Validate(newCtx); err != nil { + if err := link.Validate(ctx); err != nil { return 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()) } From 99a0ce1d8ae835e4f30c3aec3f87bdcfc7d6930c Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Sat, 17 Sep 2022 12:00:44 +0000 Subject: [PATCH 05/12] tiny refactor --- openapi3/example_validation.go | 4 ++-- openapi3/request_body.go | 4 ++-- openapi3/response.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go index 92026fddf..3fb482d01 100644 --- a/openapi3/example_validation.go +++ b/openapi3/example_validation.go @@ -5,9 +5,9 @@ import "context" func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error { opts := make([]SchemaValidationOption, 0, 3) - if vo := getValidationOptions(ctx); vo.ExamplesValidation.AsReq { + if xv := getValidationOptions(ctx).ExamplesValidation; xv.AsReq { opts = append(opts, VisitAsRequest()) - } else if vo.ExamplesValidation.AsRes { + } else if xv.AsRes { opts = append(opts, VisitAsResponse()) } opts = append(opts, MultiErrors()) diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 553ebf635..6d50daad0 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -110,8 +110,8 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error { return errors.New("content of the request body is required") } - if vo := getValidationOptions(ctx); !vo.ExamplesValidation.Disabled { - vo.ExamplesValidation.AsReq, vo.ExamplesValidation.AsRes = true, false + if xv := getValidationOptions(ctx).ExamplesValidation; !xv.Disabled { + xv.AsReq, xv.AsRes = true, false } return requestBody.Content.Validate(ctx) diff --git a/openapi3/response.go b/openapi3/response.go index f0b60135c..8a785ecbf 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -107,8 +107,8 @@ 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.ExamplesValidation.Disabled { - vo.ExamplesValidation.AsReq, vo.ExamplesValidation.AsRes = false, true + if xv := getValidationOptions(ctx).ExamplesValidation; !xv.Disabled { + xv.AsReq, xv.AsRes = false, true } if content := response.Content; content != nil { From 1a0c473e772c3a91cbd2ffc57c4f27aab28060cf Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Thu, 22 Sep 2022 21:02:22 +0000 Subject: [PATCH 06/12] fix tests --- ...donly_test.go => validate_readonly_writeonly_test.go} | 4 +++- openapi3filter/validation_error_test.go | 9 --------- 2 files changed, 3 insertions(+), 10 deletions(-) rename openapi3filter/{validate_readonly_test.go => validate_readonly_writeonly_test.go} (97%) diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_writeonly_test.go similarity index 97% rename from openapi3filter/validate_readonly_test.go rename to openapi3filter/validate_readonly_writeonly_test.go index 1152ec886..ba9cd1f17 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_writeonly_test.go @@ -12,6 +12,8 @@ import ( legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) +// TODO writeOnly response validation + func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { const spec = `{ "openapi": "3.0.3", @@ -89,5 +91,5 @@ func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { PathParams: pathParams, Route: route, }) - require.NoError(t, err) + require.Error(t, err) } 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 cb08c7c1759ecbac8d20be6c951575eaa141989a Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 23 Sep 2022 12:30:02 +0000 Subject: [PATCH 07/12] add pending tests --- openapi3/schema.go | 4 +- .../validate_readonly_writeonly_test.go | 224 ++++++++++++------ 2 files changed, 153 insertions(+), 75 deletions(-) diff --git a/openapi3/schema.go b/openapi3/schema.go index 1872734f7..48ed9a62a 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1467,9 +1467,9 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value } } else { if settings.asReq && propSchema.Value.ReadOnly { - me = append(me, fmt.Errorf("readOnly property %q in request example", propName)) + me = append(me, fmt.Errorf("readOnly property %q in request", propName)) } else if settings.asRes && propSchema.Value.WriteOnly { - me = append(me, fmt.Errorf("writeOnly property %q in response example", propName)) + me = append(me, fmt.Errorf("writeOnly property %q in response", propName)) } } } diff --git a/openapi3filter/validate_readonly_writeonly_test.go b/openapi3filter/validate_readonly_writeonly_test.go index ba9cd1f17..f520858d3 100644 --- a/openapi3filter/validate_readonly_writeonly_test.go +++ b/openapi3filter/validate_readonly_writeonly_test.go @@ -12,84 +12,162 @@ import ( legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) -// TODO writeOnly response validation - -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 + errContains string + } - type Request struct { - ID string `json:"_id"` + testCases := []testCase{ + { + name: "invalid_readonly_in_request", + requestSchema: ` + "schema":{ + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "readOnly": true + } + } + }`, + errContains: "readOnly property \"_id\" in request", + }, + { + name: "valid_writeonly_in_request", + requestSchema: ` + "schema":{ + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "writeOnly": true + } + } + }`, + }, + { + name: "invalid_writeonly_in_response", + responseSchema: ` + "schema":{ + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "writeOnly": true + } + } + }`, + errContains: "writeOnly property \"_id\" in response", + }, + { + name: "valid_readonly_in_response", + responseSchema: ` + "schema":{ + "type": "object", + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "readOnly": true + } + } + }`, + }, } - 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", + "description": "desc", + "contact": { + "email": "email" + } + }, + "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", + } + } + } + } + } + }`) + + type Request struct { + ID string `json:"_id"` + } - b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) - require.NoError(t, err) + 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", bytes.NewReader(b)) - 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") - err = ValidateRequest(sl.Context, &RequestValidationInput{ - Request: httpReq, - PathParams: pathParams, - Route: route, - }) - require.Error(t, err) + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + reqValidationInput := &RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } + if tc.requestSchema != "" { + err = ValidateRequest(sl.Context, reqValidationInput) + } else if tc.responseSchema != "" { + err = ValidateResponse(sl.Context, &ResponseValidationInput{ + RequestValidationInput: reqValidationInput, + Status: 201, + Header: httpReq.Header, + Body: httpReq.Body, + }) + } + + if tc.errContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errContains) + } else { + require.NoError(t, err) + } + }) + } } From f452182c981dd470e417f3723f063d0d290f3bf3 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 23 Sep 2022 13:08:38 +0000 Subject: [PATCH 08/12] fix and refactor --- .../validate_readonly_writeonly_test.go | 127 +++++++++++++----- 1 file changed, 92 insertions(+), 35 deletions(-) diff --git a/openapi3filter/validate_readonly_writeonly_test.go b/openapi3filter/validate_readonly_writeonly_test.go index f520858d3..0455d927e 100644 --- a/openapi3filter/validate_readonly_writeonly_test.go +++ b/openapi3filter/validate_readonly_writeonly_test.go @@ -2,8 +2,9 @@ package openapi3filter import ( "bytes" - "encoding/json" + "io" "net/http" + "strings" "testing" "github.com/stretchr/testify/require" @@ -14,30 +15,45 @@ import ( func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { type testCase struct { - name string - requestSchema string - responseSchema string - errContains string + name string + requestSchema string + responseSchema string + requestBody string + responseBody string + responseErrContains string + requestErrContains string } testCases := []testCase{ { - name: "invalid_readonly_in_request", + 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 } } }`, - errContains: "readOnly property \"_id\" in request", + requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`, + responseBody: `{"access_token": "abcd"}`, }, { - name: "valid_writeonly_in_request", + name: "valid_readonly_in_response_and_invalid_readonly_in_request", requestSchema: ` "schema":{ "type": "object", @@ -45,14 +61,28 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { "properties": { "_id": { "type": "string", - "writeOnly": true + "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", - responseSchema: ` + name: "invalid_writeonly_in_response_and_valid_writeonly_in_request", + requestSchema: ` "schema":{ "type": "object", "required": ["_id"], @@ -63,11 +93,24 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { } } }`, - errContains: "writeOnly property \"_id\" in response", + 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: "valid_readonly_in_response", - responseSchema: ` + name: "invalid_writeonly_in_response_and_invalid_readonly_in_request", + requestSchema: ` "schema":{ "type": "object", "required": ["_id"], @@ -78,6 +121,21 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { } } }`, + 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", }, } @@ -88,11 +146,7 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { "openapi": "3.0.3", "info": { "version": "1.0.0", - "title": "title", - "description": "desc", - "contact": { - "email": "email" - } + "title": "title" }, "paths": { "/accounts": { @@ -124,10 +178,6 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { } }`) - type Request struct { - ID string `json:"_id"` - } - sl := openapi3.NewLoader() doc, err := sl.LoadFromData(spec.Bytes()) require.NoError(t, err) @@ -136,10 +186,7 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) - b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) - require.NoError(t, err) - - httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b)) + httpReq, err := http.NewRequest(http.MethodPost, "/accounts", strings.NewReader(tc.requestBody)) require.NoError(t, err) httpReq.Header.Add(headerCT, "application/json") @@ -151,22 +198,32 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { PathParams: pathParams, Route: route, } + if tc.requestSchema != "" { err = ValidateRequest(sl.Context, reqValidationInput) - } else if tc.responseSchema != "" { + + 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: httpReq.Body, + Body: io.NopCloser(strings.NewReader(tc.responseBody)), }) - } - if tc.errContains != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errContains) - } else { - require.NoError(t, err) + if tc.responseErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.responseErrContains) + } else { + require.NoError(t, err) + } } }) } From b255af9ecc1c6a97ca647d0f74c268563652b06b Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Thu, 20 Oct 2022 20:25:03 +0000 Subject: [PATCH 09/12] privatizing --- openapi3/validation_options.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 2b8f386f6..f7b160666 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -3,24 +3,23 @@ package openapi3 import "context" // ValidationOption allows the modification of how the OpenAPI document is validated. -type ValidationOption func(options *ValidationOptions) +type ValidationOption func(options *validationOptions) -// ExamplesValidation provides configuration for validating examples against their schema. -type ExamplesValidation struct { +type examplesValidation struct { Disabled bool AsReq, AsRes bool } -// ValidationOptions provides configuration for validating OpenAPI documents. -type ValidationOptions struct { +type validationOptions struct { SchemaFormatValidationEnabled bool SchemaPatternValidationDisabled bool - ExamplesValidation *ExamplesValidation + ExamplesValidation *examplesValidation } -func NewValidationOptions() *ValidationOptions { - return &ValidationOptions{ - ExamplesValidation: &ExamplesValidation{}, +// NewValidationOptions creates configuration for validating OpenAPI documents. +func NewValidationOptions() *validationOptions { + return &validationOptions{ + ExamplesValidation: &examplesValidation{}, } } @@ -28,32 +27,32 @@ 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) { + 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) { + return func(options *validationOptions) { options.SchemaPatternValidationDisabled = true } } // DisableExamplesValidation disables all example schema validation. func DisableExamplesValidation() ValidationOption { - return func(options *ValidationOptions) { + return func(options *validationOptions) { options.ExamplesValidation.Disabled = 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 { +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 { +func getValidationOptions(ctx context.Context) *validationOptions { + if options, ok := ctx.Value(validationOptionsKey{}).(*validationOptions); ok { return options } return NewValidationOptions() From f627feedd546ecccf40ac0677a99ccc06908126e Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 21 Oct 2022 13:30:32 +0000 Subject: [PATCH 10/12] address comments --- openapi3/example_validation.go | 6 +-- openapi3/media_type.go | 2 +- openapi3/openapi3.go | 2 +- openapi3/parameter.go | 2 +- openapi3/request_body.go | 4 +- openapi3/response.go | 4 +- openapi3/schema.go | 35 ++++++++------- openapi3/schema_test.go | 6 +-- openapi3/schema_validation_settings.go | 6 +-- openapi3/validation_options.go | 44 +++++++------------ ...only_test.go => validate_readonly_test.go} | 8 ++-- 11 files changed, 57 insertions(+), 62 deletions(-) rename openapi3filter/{validate_readonly_writeonly_test.go => validate_readonly_test.go} (94%) diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go index 3fb482d01..fb7a1da16 100644 --- a/openapi3/example_validation.go +++ b/openapi3/example_validation.go @@ -3,11 +3,11 @@ package openapi3 import "context" func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error { - opts := make([]SchemaValidationOption, 0, 3) + opts := make([]SchemaValidationOption, 0, 2) - if xv := getValidationOptions(ctx).ExamplesValidation; xv.AsReq { + if vo := getValidationOptions(ctx); vo.examplesValidationAsReq { opts = append(opts, VisitAsRequest()) - } else if xv.AsRes { + } else if vo.examplesValidationAsRes { opts = append(opts, VisitAsResponse()) } opts = append(opts, MultiErrors()) diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 1ed34789e..09cf2a1a6 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -88,7 +88,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { return errors.New("example and examples are mutually exclusive") } - if vo := getValidationOptions(ctx); vo.ExamplesValidation.Disabled { + if vo := getValidationOptions(ctx); vo.examplesValidationDisabled { return nil } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 11755b013..963ef722c 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -56,7 +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 := NewValidationOptions() + validationOpts := &ValidationOptions{} for _, opt := range opts { opt(validationOpts) } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index f063342f3..ea717e018 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -319,7 +319,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error { return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name) } - if vo := getValidationOptions(ctx); vo.ExamplesValidation.Disabled { + 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 6d50daad0..6b4b8c9b9 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -110,8 +110,8 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error { return errors.New("content of the request body is required") } - if xv := getValidationOptions(ctx).ExamplesValidation; !xv.Disabled { - xv.AsReq, xv.AsRes = true, false + 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 8fba46276..8f9e7dd07 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -115,8 +115,8 @@ func (response *Response) Validate(ctx context.Context) error { if response.Description == nil { return errors.New("a short description of the response is required") } - if xv := getValidationOptions(ctx).ExamplesValidation; !xv.Disabled { - xv.AsReq, xv.AsRes = false, true + if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled { + vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true } if content := response.Content; content != nil { diff --git a/openapi3/schema.go b/openapi3/schema.go index 773aaedb5..be582f984 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -668,7 +668,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) } } @@ -678,7 +678,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) } } @@ -700,12 +700,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 } @@ -767,7 +767,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } - if x := schema.Example; x != nil && !validationOpts.ExamplesValidation.Disabled { + if x := schema.Example; x != nil && !validationOpts.examplesValidationDisabled { if err := validateExampleValue(ctx, x, schema); err != nil { return fmt.Errorf("invalid example: %w", err) } @@ -930,7 +930,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val 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.asRes { + if settings.asreq || settings.asrep { tempValue = deepcopy.Copy(value) } for idx, item := range v { @@ -980,7 +980,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val return e } - if settings.asReq || settings.asRes { + if settings.asreq || settings.asrep { _ = v[matchedOneOfIdx].Value.visitJSON(settings, value) } } @@ -992,7 +992,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val 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.asRes { + if settings.asreq || settings.asrep { tempValue = deepcopy.Copy(value) } for idx, item := range v { @@ -1450,7 +1450,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value var me MultiError - if settings.asReq || settings.asRes { + if settings.asreq || settings.asrep { properties := make([]string, 0, len(schema.Properties)) for propName := range schema.Properties { properties = append(properties, propName) @@ -1458,17 +1458,22 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value sort.Strings(properties) for _, propName := range properties { propSchema := schema.Properties[propName] + if value[propName] == nil { - if dlft := propSchema.Value.Default; dlft != nil { + if dlft := propSchema.Value.Default; dlft != nil && + !(settings.asreq && propSchema.Value.ReadOnly) && + !(settings.asrep && propSchema.Value.WriteOnly) { value[propName] = dlft if f := settings.defaultsSet; f != nil { settings.onceSettingDefaults.Do(f) } } - } else { - if settings.asReq && propSchema.Value.ReadOnly { + } + + if value[propName] != nil { + if settings.asreq && propSchema.Value.ReadOnly { me = append(me, fmt.Errorf("readOnly property %q in request", propName)) - } else if settings.asRes && propSchema.Value.WriteOnly { + } else if settings.asrep && propSchema.Value.WriteOnly { me = append(me, fmt.Errorf("writeOnly property %q in response", propName)) } } @@ -1587,10 +1592,10 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value // "required" for _, k := range schema.Required { if _, ok := value[k]; !ok { - if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asReq { + if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asreq { continue } - if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asRes { + if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asrep { continue } if settings.failfast { diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 01a21077d..46de21ec2 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1028,9 +1028,9 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) - vo := NewValidationOptions() - vo.SchemaFormatValidationEnabled = true - ctx := WithValidationOptions(context.Background(), vo) + ctx := WithValidationOptions(context.Background(), &ValidationOptions{ + schemaFormatValidationEnabled: true, + }) err := schema.Validate(ctx) require.Error(t, err) } diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index c47224349..854ae8480 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -10,7 +10,7 @@ type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { failfast bool multiError bool - asReq, asRes bool // exclusive (XOR) fields + asreq, asrep bool // exclusive (XOR) fields formatValidationEnabled bool patternValidationDisabled bool @@ -28,11 +28,11 @@ func MultiErrors() SchemaValidationOption { } func VisitAsRequest() SchemaValidationOption { - return func(s *schemaValidationSettings) { s.asReq, s.asRes = true, false } + return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false } } func VisitAsResponse() SchemaValidationOption { - return func(s *schemaValidationSettings) { s.asReq, s.asRes = false, true } + 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. diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index f7b160666..98c4b2703 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -3,57 +3,47 @@ package openapi3 import "context" // ValidationOption allows the modification of how the OpenAPI document is validated. -type ValidationOption func(options *validationOptions) +type ValidationOption func(options *ValidationOptions) -type examplesValidation struct { - Disabled bool - AsReq, AsRes bool -} - -type validationOptions struct { - SchemaFormatValidationEnabled bool - SchemaPatternValidationDisabled bool - ExamplesValidation *examplesValidation -} - -// NewValidationOptions creates configuration for validating OpenAPI documents. -func NewValidationOptions() *validationOptions { - return &validationOptions{ - ExamplesValidation: &examplesValidation{}, - } +// ValidationOptions provides configuration for validating OpenAPI documents. +type ValidationOptions struct { + schemaFormatValidationEnabled bool + schemaPatternValidationDisabled bool + examplesValidationDisabled bool + examplesValidationAsReq, examplesValidationAsRes 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 + 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 + return func(options *ValidationOptions) { + options.schemaPatternValidationDisabled = true } } // DisableExamplesValidation disables all example schema validation. func DisableExamplesValidation() ValidationOption { - return func(options *validationOptions) { - options.ExamplesValidation.Disabled = true + 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 { +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 { +func getValidationOptions(ctx context.Context) *ValidationOptions { + if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok { return options } - return NewValidationOptions() + return &ValidationOptions{} } diff --git a/openapi3filter/validate_readonly_writeonly_test.go b/openapi3filter/validate_readonly_test.go similarity index 94% rename from openapi3filter/validate_readonly_writeonly_test.go rename to openapi3filter/validate_readonly_test.go index 0455d927e..bad6c961a 100644 --- a/openapi3filter/validate_readonly_writeonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -78,7 +78,7 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { }`, requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`, responseBody: `{"access_token": "abcd"}`, - requestErrContains: "readOnly property \"_id\" in request", + requestErrContains: `readOnly property "_id" in request`, }, { name: "invalid_writeonly_in_response_and_valid_writeonly_in_request", @@ -106,7 +106,7 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { }`, requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`, responseBody: `{"access_token": "abcd"}`, - responseErrContains: "writeOnly property \"access_token\" in response", + responseErrContains: `writeOnly property "access_token" in response`, }, { name: "invalid_writeonly_in_response_and_invalid_readonly_in_request", @@ -134,8 +134,8 @@ func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) { }`, requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`, responseBody: `{"access_token": "abcd"}`, - responseErrContains: "writeOnly property \"access_token\" in response", - requestErrContains: "readOnly property \"_id\" in request", + responseErrContains: `writeOnly property "access_token" in response`, + requestErrContains: `readOnly property "_id" in request`, }, } From 8a9c86e477a8a20b7a3e47d90c90139ae5a2afcf Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 21 Oct 2022 21:10:47 +0000 Subject: [PATCH 11/12] refactor and update tests --- openapi3/example_validation_test.go | 3 +++ openapi3/schema.go | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index 4cefe05d7..79288c299 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -324,12 +324,15 @@ components: 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 diff --git a/openapi3/schema.go b/openapi3/schema.go index be582f984..c7358d12b 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1458,11 +1458,11 @@ 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 && - !(settings.asreq && propSchema.Value.ReadOnly) && - !(settings.asrep && propSchema.Value.WriteOnly) { + if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO { value[propName] = dlft if f := settings.defaultsSet; f != nil { settings.onceSettingDefaults.Do(f) @@ -1471,9 +1471,9 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value } if value[propName] != nil { - if settings.asreq && propSchema.Value.ReadOnly { + if reqRO { me = append(me, fmt.Errorf("readOnly property %q in request", propName)) - } else if settings.asrep && propSchema.Value.WriteOnly { + } else if repWO { me = append(me, fmt.Errorf("writeOnly property %q in response", propName)) } } From 96d51cc553e7c0ae982e935c41968322a9a75e68 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Sun, 23 Oct 2022 10:02:55 +0000 Subject: [PATCH 12/12] address comments --- openapi3/media_type.go | 2 +- openapi3/parameter.go | 2 +- openapi3/request_body.go | 2 +- openapi3/response.go | 2 +- openapi3/schema.go | 10 +++++----- openapi3/schema_test.go | 2 +- openapi3/validation_options.go | 12 ++++++------ 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 09cf2a1a6..3500334f7 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -88,7 +88,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error { 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 ea717e018..fa07d6555 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -319,7 +319,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error { 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 6b4b8c9b9..d28133c96 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -110,7 +110,7 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error { 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 8f9e7dd07..37325bbb7 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -115,7 +115,7 @@ 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 { + if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled { vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true } diff --git a/openapi3/schema.go b/openapi3/schema.go index c7358d12b..89602fc08 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -668,7 +668,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) } } @@ -678,7 +678,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) } } @@ -700,12 +700,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 } @@ -767,7 +767,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } - 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/schema_test.go b/openapi3/schema_test.go index 46de21ec2..4c14dcb10 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1029,7 +1029,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, + SchemaFormatValidationEnabled: true, }) err := schema.Validate(ctx) require.Error(t, err) diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 98c4b2703..d8900878a 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -7,9 +7,9 @@ type ValidationOption func(options *ValidationOptions) // 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 } @@ -18,21 +18,21 @@ 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 + 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 + options.SchemaPatternValidationDisabled = true } } // DisableExamplesValidation disables all example schema validation. func DisableExamplesValidation() ValidationOption { return func(options *ValidationOptions) { - options.examplesValidationDisabled = true + options.ExamplesValidationDisabled = true } }