Skip to content

Commit

Permalink
Add content-type wildcard support to validation (#93)
Browse files Browse the repository at this point in the history
* Add content-type wildcard support to validation

This implements the cascading wildcard behavior described in the OpenAPI
specification for request body and response body validation.

https://swagger.io/docs/specification/describing-request-body/

* Return nil as content type for invalid mimes

Rather than allow accidental fallthrough of invalid mime types we will
return nil.
  • Loading branch information
kconwayatlassian authored and fenollp committed May 10, 2019
1 parent 2a7fbb6 commit 90a7df0
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 20 deletions.
28 changes: 27 additions & 1 deletion openapi3/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,40 @@ func NewContentWithJSONSchemaRef(schema *SchemaRef) Content {
}

func (content Content) Get(mime string) *MediaType {
// Start by making the most specific match possible
// by using the mime type in full.
if v := content[mime]; v != nil {
return v
}
// If an exact match is not found then we strip all
// metadata from the mime type and only use the x/y
// portion.
i := strings.IndexByte(mime, ';')
if i < 0 {
// If there is no metadata then preserve the full mime type
// string for later wildcard searches.
i = len(mime)
}
mime = mime[:i]
if v := content[mime]; v != nil {
return v
}
// If the x/y pattern has no specific match then we
// try the x/* pattern.
i = strings.IndexByte(mime, '/')
if i < 0 {
// In the case that the given mime type is not valid because it is
// missing the subtype we return nil so that this does not accidentally
// resolve with the wildcard.
return nil
}
return content[mime[:i]]
mime = mime[:i] + "/*"
if v := content[mime]; v != nil {
return v
}
// Finally, the most generic match of */* is returned
// as a catch-all.
return content["*/*"]
}

func (content Content) Validate(c context.Context) error {
Expand Down
107 changes: 107 additions & 0 deletions openapi3/content_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package openapi3

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestContent_Get(t *testing.T) {
fallback := NewMediaType()
wildcard := NewMediaType()
stripped := NewMediaType()
fullMatch := NewMediaType()
content := Content{
"*/*": fallback,
"application/*": wildcard,
"application/json": stripped,
"application/json;encoding=utf-8": fullMatch,
}
contentWithoutWildcards := Content{
"application/json": stripped,
"application/json;encoding=utf-8": fullMatch,
}
tests := []struct {
name string
content Content
mime string
want *MediaType
}{
{
name: "missing",
content: contentWithoutWildcards,
mime: "text/plain;encoding=utf-8",
want: nil,
},
{
name: "full match",
content: content,
mime: "application/json;encoding=utf-8",
want: fullMatch,
},
{
name: "stripped match",
content: content,
mime: "application/json;encoding=utf-16",
want: stripped,
},
{
name: "wildcard match",
content: content,
mime: "application/yaml;encoding=utf-16",
want: wildcard,
},
{
name: "fallback match",
content: content,
mime: "text/plain;encoding=utf-16",
want: fallback,
},
{
name: "invalid mime type",
content: content,
mime: "text;encoding=utf16",
want: nil,
},
{
name: "missing no encoding",
content: contentWithoutWildcards,
mime: "text/plain",
want: nil,
},
{
name: "stripped match no encoding",
content: content,
mime: "application/json",
want: stripped,
},
{
name: "wildcard match no encoding",
content: content,
mime: "application/yaml",
want: wildcard,
},
{
name: "fallback match no encoding",
content: content,
mime: "text/plain",
want: fallback,
},
{
name: "invalid mime type no encoding",
content: content,
mime: "text",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Using require.True here because require.Same is not yet released.
// We're comparing pointer values and the require.Equal will
// dereference and compare the pointed to values rather than check
// if the memory addresses are the same. Once require.Same is released
// this test should convert to using that.
require.True(t, tt.want == tt.content.Get(tt.mime))
})
}
}
11 changes: 1 addition & 10 deletions openapi3filter/validate_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,7 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque
}

inputMIME := req.Header.Get("Content-Type")
mediaType := parseMediaType(inputMIME)
if mediaType == "" {
return &RequestError{
Input: input,
RequestBody: requestBody,
Reason: "content type is missed",
}
}

contentType := requestBody.Content[mediaType]
contentType := requestBody.Content.Get(inputMIME)
if contentType == nil {
return &RequestError{
Input: input,
Expand Down
10 changes: 1 addition & 9 deletions openapi3filter/validate_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,7 @@ func ValidateResponse(c context.Context, input *ResponseValidationInput) error {
}

inputMIME := input.Header.Get("Content-Type")
mediaType := parseMediaType(inputMIME)
if mediaType == "" {
return &ResponseError{
Input: input,
Reason: "content type of response body is missed",
}
}

contentType := content[mediaType]
contentType := content.Get(inputMIME)
if contentType == nil {
return &ResponseError{
Input: input,
Expand Down

0 comments on commit 90a7df0

Please sign in to comment.