Skip to content

Commit 19556cf

Browse files
authored
openapi3filter: support for allOf request schema in multipart/form-data (#729)
fix #722
1 parent 92d47ad commit 19556cf

2 files changed

Lines changed: 186 additions & 25 deletions

File tree

openapi3filter/issue722_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package openapi3filter_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"io"
7+
"mime/multipart"
8+
"net/http"
9+
"net/textproto"
10+
"strings"
11+
"testing"
12+
13+
"github.com/getkin/kin-openapi/openapi3"
14+
"github.com/getkin/kin-openapi/openapi3filter"
15+
"github.com/getkin/kin-openapi/routers/gorillamux"
16+
)
17+
18+
func TestValidateMultipartFormDataContainingAllOf(t *testing.T) {
19+
const spec = `
20+
openapi: 3.0.0
21+
info:
22+
title: 'Validator'
23+
version: 0.0.1
24+
paths:
25+
/test:
26+
post:
27+
requestBody:
28+
required: true
29+
content:
30+
multipart/form-data:
31+
schema:
32+
type: object
33+
required:
34+
- file
35+
allOf:
36+
- $ref: '#/components/schemas/Category'
37+
- properties:
38+
file:
39+
type: string
40+
format: binary
41+
description:
42+
type: string
43+
responses:
44+
'200':
45+
description: Created
46+
47+
components:
48+
schemas:
49+
Category:
50+
type: object
51+
properties:
52+
name:
53+
type: string
54+
required:
55+
- name
56+
`
57+
58+
loader := openapi3.NewLoader()
59+
doc, err := loader.LoadFromData([]byte(spec))
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
if err = doc.Validate(loader.Context); err != nil {
64+
t.Fatal(err)
65+
}
66+
67+
router, err := gorillamux.NewRouter(doc)
68+
if err != nil {
69+
t.Fatal(err)
70+
}
71+
72+
body := &bytes.Buffer{}
73+
writer := multipart.NewWriter(body)
74+
75+
{ // Add file data
76+
fw, err := writer.CreateFormFile("file", "hello.txt")
77+
if err != nil {
78+
t.Fatal(err)
79+
}
80+
if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil {
81+
t.Fatal(err)
82+
}
83+
}
84+
85+
{ // Add a single "name" item as part data
86+
h := make(textproto.MIMEHeader)
87+
h.Set("Content-Disposition", `form-data; name="name"`)
88+
fw, err := writer.CreatePart(h)
89+
if err != nil {
90+
t.Fatal(err)
91+
}
92+
if _, err = io.Copy(fw, strings.NewReader(`foo`)); err != nil {
93+
t.Fatal(err)
94+
}
95+
}
96+
97+
{ // Add a single "discription" item as part data
98+
h := make(textproto.MIMEHeader)
99+
h.Set("Content-Disposition", `form-data; name="description"`)
100+
fw, err := writer.CreatePart(h)
101+
if err != nil {
102+
t.Fatal(err)
103+
}
104+
if _, err = io.Copy(fw, strings.NewReader(`description note`)); err != nil {
105+
t.Fatal(err)
106+
}
107+
}
108+
109+
writer.Close()
110+
111+
req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes()))
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
req.Header.Set("Content-Type", writer.FormDataContentType())
116+
117+
route, pathParams, err := router.FindRoute(req)
118+
if err != nil {
119+
t.Fatal(err)
120+
}
121+
122+
if err = openapi3filter.ValidateRequestBody(
123+
context.Background(),
124+
&openapi3filter.RequestValidationInput{
125+
Request: req,
126+
PathParams: pathParams,
127+
Route: route,
128+
},
129+
route.Operation.RequestBody.Value,
130+
); err != nil {
131+
t.Error(err)
132+
}
133+
}

openapi3filter/req_resp_decoder.go

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,33 +1120,47 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S
11201120
enc = encFn(name)
11211121
}
11221122
subEncFn := func(string) *openapi3.Encoding { return enc }
1123-
// If the property's schema has type "array" it is means that the form contains a few parts with the same name.
1124-
// Every such part has a type that is defined by an items schema in the property's schema.
1123+
11251124
var valueSchema *openapi3.SchemaRef
1126-
var exists bool
1127-
valueSchema, exists = schema.Value.Properties[name]
1128-
if !exists {
1129-
anyProperties := schema.Value.AdditionalPropertiesAllowed
1130-
if anyProperties != nil {
1131-
switch *anyProperties {
1132-
case true:
1133-
//additionalProperties: true
1134-
continue
1135-
default:
1136-
//additionalProperties: false
1137-
return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
1125+
if len(schema.Value.AllOf) > 0 {
1126+
var exists bool
1127+
for _, sr := range schema.Value.AllOf {
1128+
valueSchema, exists = sr.Value.Properties[name]
1129+
if exists {
1130+
break
11381131
}
11391132
}
1140-
if schema.Value.AdditionalProperties == nil {
1133+
if !exists {
11411134
return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
11421135
}
1143-
valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name]
1136+
} else {
1137+
// If the property's schema has type "array" it is means that the form contains a few parts with the same name.
1138+
// Every such part has a type that is defined by an items schema in the property's schema.
1139+
var exists bool
1140+
valueSchema, exists = schema.Value.Properties[name]
11441141
if !exists {
1145-
return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
1142+
anyProperties := schema.Value.AdditionalPropertiesAllowed
1143+
if anyProperties != nil {
1144+
switch *anyProperties {
1145+
case true:
1146+
//additionalProperties: true
1147+
continue
1148+
default:
1149+
//additionalProperties: false
1150+
return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
1151+
}
1152+
}
1153+
if schema.Value.AdditionalProperties == nil {
1154+
return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
1155+
}
1156+
valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name]
1157+
if !exists {
1158+
return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
1159+
}
1160+
}
1161+
if valueSchema.Value.Type == "array" {
1162+
valueSchema = valueSchema.Value.Items
11461163
}
1147-
}
1148-
if valueSchema.Value.Type == "array" {
1149-
valueSchema = valueSchema.Value.Items
11501164
}
11511165

11521166
var value interface{}
@@ -1160,14 +1174,28 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S
11601174
}
11611175

11621176
allTheProperties := make(map[string]*openapi3.SchemaRef)
1163-
for k, v := range schema.Value.Properties {
1164-
allTheProperties[k] = v
1165-
}
1166-
if schema.Value.AdditionalProperties != nil {
1167-
for k, v := range schema.Value.AdditionalProperties.Value.Properties {
1177+
if len(schema.Value.AllOf) > 0 {
1178+
for _, sr := range schema.Value.AllOf {
1179+
for k, v := range sr.Value.Properties {
1180+
allTheProperties[k] = v
1181+
}
1182+
if sr.Value.AdditionalProperties != nil {
1183+
for k, v := range sr.Value.AdditionalProperties.Value.Properties {
1184+
allTheProperties[k] = v
1185+
}
1186+
}
1187+
}
1188+
} else {
1189+
for k, v := range schema.Value.Properties {
11681190
allTheProperties[k] = v
11691191
}
1192+
if schema.Value.AdditionalProperties != nil {
1193+
for k, v := range schema.Value.AdditionalProperties.Value.Properties {
1194+
allTheProperties[k] = v
1195+
}
1196+
}
11701197
}
1198+
11711199
// Make an object value from form values.
11721200
obj := make(map[string]interface{})
11731201
for name, prop := range allTheProperties {

0 commit comments

Comments
 (0)