Skip to content

Commit

Permalink
readOnly writeOnly validation (#599)
Browse files Browse the repository at this point in the history
  • Loading branch information
danicc097 authored Oct 27, 2022
1 parent e56a195 commit 285135d
Show file tree
Hide file tree
Showing 11 changed files with 379 additions and 117 deletions.
15 changes: 13 additions & 2 deletions openapi3/example_validation.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
package openapi3

func validateExampleValue(input interface{}, schema *Schema) error {
return schema.VisitJSON(input, MultiErrors())
import "context"

func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error {
opts := make([]SchemaValidationOption, 0, 2)

if vo := getValidationOptions(ctx); vo.examplesValidationAsReq {
opts = append(opts, VisitAsRequest())
} else if vo.examplesValidationAsRes {
opts = append(opts, VisitAsResponse())
}
opts = append(opts, MultiErrors())

return schema.VisitJSON(input, opts...)
}
140 changes: 121 additions & 19 deletions openapi3/example_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -26,7 +29,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
param1example:
value: abcd
`,
errContains: "invalid paths: invalid path /user: invalid operation POST: param1example",
errContains: `invalid paths: invalid path /user: invalid operation POST: param1example`,
},
{
name: "valid_parameter_examples",
Expand All @@ -41,7 +44,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
parametersExample: `
example: abcd
`,
errContains: "invalid path /user: invalid operation POST: invalid example",
errContains: `invalid path /user: invalid operation POST: invalid example`,
},
{
name: "valid_parameter_example",
Expand All @@ -64,7 +67,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
email: bad
password: short
`,
errContains: "invalid paths: invalid path /user: invalid operation POST: example BadUser",
errContains: `invalid paths: invalid path /user: invalid operation POST: example BadUser`,
},
{
name: "valid_component_examples",
Expand All @@ -90,7 +93,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
email: bad
password: short
`,
errContains: "invalid path /user: invalid operation POST: invalid example",
errContains: `invalid path /user: invalid operation POST: invalid example`,
},
{
name: "valid_mediatype_examples",
Expand All @@ -109,7 +112,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
email: [email protected]
# missing password
`,
errContains: "schema \"CreateUserRequest\": invalid example",
errContains: `schema "CreateUserRequest": invalid example`,
},
{
name: "valid_schema_request_example",
Expand All @@ -127,15 +130,72 @@ func TestExamplesSchemaValidation(t *testing.T) {
user_id: 1
# missing access_token
`,
errContains: "schema \"CreateUserResponse\": invalid example",
errContains: `schema "CreateUserResponse": invalid example`,
},
{
name: "valid_schema_response_example",
responseSchemaExample: `
example:
user_id: 1
access_token: "abcd"
`,
`,
},
{
name: "valid_readonly_writeonly_examples",
readWriteOnlyMediaTypeRequestExample: `
examples:
ReadWriteOnlyRequest:
$ref: '#/components/examples/ReadWriteOnlyRequestData'
`,
readWriteOnlyMediaTypeResponseExample: `
examples:
ReadWriteOnlyResponse:
$ref: '#/components/examples/ReadWriteOnlyResponseData'
`,
componentExamples: `
examples:
ReadWriteOnlyRequestData:
value:
username: user
password: password
ReadWriteOnlyResponseData:
value:
user_id: 4321
`,
},
{
name: "invalid_readonly_request_examples",
readWriteOnlyMediaTypeRequestExample: `
examples:
ReadWriteOnlyRequest:
$ref: '#/components/examples/ReadWriteOnlyRequestData'
`,
componentExamples: `
examples:
ReadWriteOnlyRequestData:
value:
username: user
password: password
user_id: 4321
`,
errContains: `ReadWriteOnlyRequest: readOnly property "user_id" in request`,
},
{
name: "invalid_writeonly_response_examples",
readWriteOnlyMediaTypeResponseExample: `
examples:
ReadWriteOnlyResponse:
$ref: '#/components/examples/ReadWriteOnlyResponseData'
`,
componentExamples: `
examples:
ReadWriteOnlyResponseData:
value:
password: password
user_id: 4321
`,

errContains: `ReadWriteOnlyResponse: writeOnly property "password" in response`,
},
}

Expand Down Expand Up @@ -198,7 +258,28 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserResponse"
$ref: "#/components/schemas/CreateUserResponse"`)
spec.WriteString(tc.mediaTypeResponseExample)
spec.WriteString(`
/readWriteOnly:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/ReadWriteOnlyData"
required: true`)
spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample)
spec.WriteString(`
responses:
'201':
description: a response
content:
application/json:
schema:
$ref: "#/components/schemas/ReadWriteOnlyData"`)
spec.WriteString(tc.readWriteOnlyMediaTypeResponseExample)
spec.WriteString(`
components:
schemas:
CreateUserRequest:`)
Expand All @@ -223,7 +304,6 @@ components:
CreateUserResponse:`)
spec.WriteString(tc.responseSchemaExample)
spec.WriteString(`
description: represents the response to a User creation
required:
- access_token
- user_id
Expand All @@ -234,6 +314,28 @@ components:
format: int64
type: integer
type: object
ReadWriteOnlyData:
required:
# only required in request
- username
- password
# only required in response
- user_id
properties:
username:
type: string
default: default
writeOnly: true # only sent in a request
password:
type: string
default: default
writeOnly: true # only sent in a request
user_id:
format: int64
default: 1
type: integer
readOnly: true # only returned in a response
type: object
`)
spec.WriteString(tc.componentExamples)

Expand Down Expand Up @@ -278,7 +380,7 @@ func TestExampleObjectValidation(t *testing.T) {
email: [email protected]
password: validpassword
`,
errContains: "invalid path /user: invalid operation POST: example and examples are mutually exclusive",
errContains: `invalid path /user: invalid operation POST: example and examples are mutually exclusive`,
componentExamples: `
examples:
BadUser:
Expand All @@ -295,7 +397,7 @@ func TestExampleObjectValidation(t *testing.T) {
BadUser:
description: empty user example
`,
errContains: "invalid components: example \"BadUser\": no value or externalValue field",
errContains: `invalid components: example "BadUser": no value or externalValue field`,
},
{
name: "value_externalValue_mutual_exclusion",
Expand All @@ -308,7 +410,7 @@ func TestExampleObjectValidation(t *testing.T) {
password: validpassword
externalValue: 'http://example.com/examples/example'
`,
errContains: "invalid components: example \"BadUser\": value and externalValue are mutually exclusive",
errContains: `invalid components: example "BadUser": value and externalValue are mutually exclusive`,
},
}

Expand Down
6 changes: 3 additions & 3 deletions openapi3/media_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
return errors.New("example and examples are mutually exclusive")
}

if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled {
return nil
}

if example := mediaType.Example; example != nil {
if err := validateExampleValue(example, schema.Value); err != nil {
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
return fmt.Errorf("invalid example: %w", err)
}
}
Expand All @@ -109,7 +109,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("example %s: %w", k, err)
}
if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
return fmt.Errorf("example %s: %w", k, err)
}
}
Expand Down
7 changes: 4 additions & 3 deletions openapi3/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,11 +318,12 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
if parameter.Example != nil && parameter.Examples != nil {
return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name)
}
if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {

if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled {
return nil
}
if example := parameter.Example; example != nil {
if err := validateExampleValue(example, schema.Value); err != nil {
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
return fmt.Errorf("invalid example: %w", err)
}
} else if examples := parameter.Examples; examples != nil {
Expand All @@ -336,7 +337,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("%s: %w", k, err)
}
if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
return fmt.Errorf("%s: %w", k, err)
}
}
Expand Down
5 changes: 5 additions & 0 deletions openapi3/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,10 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error {
if requestBody.Content == nil {
return errors.New("content of the request body is required")
}

if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled {
vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false
}

return requestBody.Content.Validate(ctx)
}
3 changes: 3 additions & 0 deletions openapi3/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ func (response *Response) Validate(ctx context.Context) error {
if response.Description == nil {
return errors.New("a short description of the response is required")
}
if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled {
vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true
}

if content := response.Content; content != nil {
if err := content.Validate(ctx); err != nil {
Expand Down
19 changes: 15 additions & 4 deletions openapi3/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}

if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled {
if err := validateExampleValue(x, schema); err != nil {
if err := validateExampleValue(ctx, x, schema); err != nil {
return fmt.Errorf("invalid example: %w", err)
}
}
Expand Down Expand Up @@ -1449,6 +1449,8 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
return schema.expectedType(settings, TypeObject)
}

var me MultiError

if settings.asreq || settings.asrep {
properties := make([]string, 0, len(schema.Properties))
for propName := range schema.Properties {
Expand All @@ -1457,19 +1459,28 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
sort.Strings(properties)
for _, propName := range properties {
propSchema := schema.Properties[propName]
reqRO := settings.asreq && propSchema.Value.ReadOnly
repWO := settings.asrep && propSchema.Value.WriteOnly

if value[propName] == nil {
if dlft := propSchema.Value.Default; dlft != nil {
if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO {
value[propName] = dlft
if f := settings.defaultsSet; f != nil {
settings.onceSettingDefaults.Do(f)
}
}
}

if value[propName] != nil {
if reqRO {
me = append(me, fmt.Errorf("readOnly property %q in request", propName))
} else if repWO {
me = append(me, fmt.Errorf("writeOnly property %q in response", propName))
}
}
}
}

var me MultiError

// "properties"
properties := schema.Properties
lenValue := int64(len(value))
Expand Down
9 changes: 5 additions & 4 deletions openapi3/validation_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import "context"
// ValidationOption allows the modification of how the OpenAPI document is validated.
type ValidationOption func(options *ValidationOptions)

// ValidationOptions provide configuration for validating OpenAPI documents.
// ValidationOptions provides configuration for validating OpenAPI documents.
type ValidationOptions struct {
SchemaFormatValidationEnabled bool
SchemaPatternValidationDisabled bool
ExamplesValidationDisabled bool
SchemaFormatValidationEnabled bool
SchemaPatternValidationDisabled bool
ExamplesValidationDisabled bool
examplesValidationAsReq, examplesValidationAsRes bool
}

type validationOptionsKey struct{}
Expand Down
Loading

0 comments on commit 285135d

Please sign in to comment.