Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

readOnly writeOnly validation #599

Merged
merged 15 commits into from Oct 27, 2022
15 changes: 13 additions & 2 deletions openapi3/example_validation.go
@@ -1,5 +1,16 @@
package openapi3

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

func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error {
opts := make([]SchemaValidationOption, 0, 3)
danicc097 marked this conversation as resolved.
Show resolved Hide resolved

if xv := getValidationOptions(ctx).ExamplesValidation; xv.AsReq {
opts = append(opts, VisitAsRequest())
} else if xv.AsRes {
opts = append(opts, VisitAsResponse())
}
opts = append(opts, MultiErrors())

return schema.VisitJSON(input, opts...)
}
119 changes: 109 additions & 10 deletions openapi3/example_validation_test.go
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 Down Expand Up @@ -135,7 +138,64 @@ func TestExamplesSchemaValidation(t *testing.T) {
example:
user_id: 1
access_token: "abcd"
`,
`,
},
{
name: "valid_readonly_writeonly_examples",
readWriteOnlyMediaTypeRequestExample: `
examples:
ReadWriteOnlyRequest:
$ref: '#/components/examples/ReadWriteOnlyRequestData'
`,
readWriteOnlyMediaTypeResponseExample: `
examples:
ReadWriteOnlyResponse:
$ref: '#/components/examples/ReadWriteOnlyResponseData'
`,
componentExamples: `
examples:
ReadWriteOnlyRequestData:
value:
username: user
password: password
ReadWriteOnlyResponseData:
value:
user_id: 4321
`,
},
{
name: "invalid_readonly_request_examples",
readWriteOnlyMediaTypeRequestExample: `
examples:
ReadWriteOnlyRequest:
$ref: '#/components/examples/ReadWriteOnlyRequestData'
`,
componentExamples: `
examples:
ReadWriteOnlyRequestData:
value:
username: user
password: password
user_id: 4321
`,
errContains: "ReadWriteOnlyRequest: readOnly property \"user_id\" in request",
},
{
name: "invalid_writeonly_response_examples",
readWriteOnlyMediaTypeResponseExample: `
examples:
ReadWriteOnlyResponse:
$ref: '#/components/examples/ReadWriteOnlyResponseData'
`,
componentExamples: `
examples:
ReadWriteOnlyResponseData:
value:
password: password
user_id: 4321
`,

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

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,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)

Expand Down
6 changes: 3 additions & 3 deletions openapi3/media_type.go
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.ExamplesValidation.Disabled {
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 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("%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
2 changes: 1 addition & 1 deletion openapi3/openapi3.go
Expand Up @@ -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()
danicc097 marked this conversation as resolved.
Show resolved Hide resolved
for _, opt := range opts {
opt(validationOpts)
}
Expand Down
7 changes: 4 additions & 3 deletions openapi3/parameter.go
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.ExamplesValidation.Disabled {
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 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
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 xv := getValidationOptions(ctx).ExamplesValidation; !xv.Disabled {
xv.AsReq, xv.AsRes = true, false
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}

return requestBody.Content.Validate(ctx)
}
3 changes: 3 additions & 0 deletions openapi3/response.go
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 xv := getValidationOptions(ctx).ExamplesValidation; !xv.Disabled {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
xv.AsReq, xv.AsRes = false, true
}

if content := response.Content; content != nil {
if err := content.Validate(ctx); err != nil {
Expand Down
26 changes: 16 additions & 10 deletions openapi3/schema.go
Expand Up @@ -767,8 +767,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(ctx, x, schema); err != nil {
return fmt.Errorf("invalid schema example: %w", err)
}
}
Expand Down Expand Up @@ -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.asrep {
if settings.asReq || settings.asRes {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
tempValue = deepcopy.Copy(value)
}
for idx, item := range v {
Expand Down Expand Up @@ -980,7 +980,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
return e
}

if settings.asreq || settings.asrep {
if settings.asReq || settings.asRes {
_ = v[matchedOneOfIdx].Value.visitJSON(settings, value)
}
}
Expand All @@ -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.asrep {
if settings.asReq || settings.asRes {
tempValue = deepcopy.Copy(value)
}
for idx, item := range v {
Expand Down Expand Up @@ -1448,7 +1448,9 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
return schema.expectedType(settings, TypeObject)
}

if settings.asreq || settings.asrep {
var me MultiError

if settings.asReq || settings.asRes {
properties := make([]string, 0, len(schema.Properties))
for propName := range schema.Properties {
properties = append(properties, propName)
Expand All @@ -1463,12 +1465,16 @@ 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", propName))
} else if settings.asRes && propSchema.Value.WriteOnly {
me = append(me, fmt.Errorf("writeOnly property %q in response", propName))
}
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

var me MultiError

// "properties"
properties := schema.Properties
lenValue := int64(len(value))
Expand Down Expand Up @@ -1581,10 +1587,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 {
Expand Down
6 changes: 3 additions & 3 deletions openapi3/schema_test.go
Expand Up @@ -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)
fenollp marked this conversation as resolved.
Show resolved Hide resolved
require.Error(t, err)
}
Expand Down
6 changes: 3 additions & 3 deletions openapi3/schema_validation_settings.go
Expand Up @@ -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

Expand All @@ -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.
Expand Down