Skip to content

Commit f0dcc53

Browse files
authored
openapi3filter: RegisterBodyDecoder for text/csv (#734)
fix #696
1 parent e7d649f commit f0dcc53

4 files changed

Lines changed: 154 additions & 6 deletions

File tree

.github/docs/openapi3filter.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er
1717
func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, ...) error
1818
func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
1919
func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, ...) error
20-
func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, ...) (interface{}, error)
2120
type AuthenticationFunc func(context.Context, *AuthenticationInput) error
2221
type AuthenticationInput struct{ ... }
2322
type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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/stretchr/testify/require"
14+
15+
"github.com/getkin/kin-openapi/openapi3"
16+
"github.com/getkin/kin-openapi/openapi3filter"
17+
"github.com/getkin/kin-openapi/routers/gorillamux"
18+
)
19+
20+
func TestValidateCsvFileUpload(t *testing.T) {
21+
const spec = `
22+
openapi: 3.0.0
23+
info:
24+
title: 'Validator'
25+
version: 0.0.1
26+
paths:
27+
/test:
28+
post:
29+
requestBody:
30+
required: true
31+
content:
32+
multipart/form-data:
33+
schema:
34+
type: object
35+
required:
36+
- file
37+
properties:
38+
file:
39+
type: string
40+
format: string
41+
responses:
42+
'200':
43+
description: Created
44+
`
45+
46+
loader := openapi3.NewLoader()
47+
doc, err := loader.LoadFromData([]byte(spec))
48+
require.NoError(t, err)
49+
50+
err = doc.Validate(loader.Context)
51+
require.NoError(t, err)
52+
53+
router, err := gorillamux.NewRouter(doc)
54+
require.NoError(t, err)
55+
56+
tests := []struct {
57+
csvData string
58+
wantErr bool
59+
}{
60+
{
61+
`foo,bar`,
62+
false,
63+
},
64+
{
65+
`"foo","bar"`,
66+
false,
67+
},
68+
{
69+
`foo,bar
70+
baz,qux`,
71+
false,
72+
},
73+
{
74+
`foo,bar
75+
baz,qux,quux`,
76+
true,
77+
},
78+
{
79+
`"""`,
80+
true,
81+
},
82+
}
83+
for _, tt := range tests {
84+
body := &bytes.Buffer{}
85+
writer := multipart.NewWriter(body)
86+
87+
{ // Add file data
88+
h := make(textproto.MIMEHeader)
89+
h.Set("Content-Disposition", `form-data; name="file"; filename="hello.csv"`)
90+
h.Set("Content-Type", "text/csv")
91+
92+
fw, err := writer.CreatePart(h)
93+
require.NoError(t, err)
94+
_, err = io.Copy(fw, strings.NewReader(tt.csvData))
95+
96+
require.NoError(t, err)
97+
}
98+
99+
writer.Close()
100+
101+
req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes()))
102+
require.NoError(t, err)
103+
104+
req.Header.Set("Content-Type", writer.FormDataContentType())
105+
106+
route, pathParams, err := router.FindRoute(req)
107+
require.NoError(t, err)
108+
109+
if err = openapi3filter.ValidateRequestBody(
110+
context.Background(),
111+
&openapi3filter.RequestValidationInput{
112+
Request: req,
113+
PathParams: pathParams,
114+
Route: route,
115+
},
116+
route.Operation.RequestBody.Value,
117+
); err != nil {
118+
if !tt.wantErr {
119+
t.Errorf("got %v", err)
120+
}
121+
continue
122+
}
123+
if tt.wantErr {
124+
t.Errorf("want err")
125+
}
126+
}
127+
}

openapi3filter/req_resp_decoder.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package openapi3filter
33
import (
44
"archive/zip"
55
"bytes"
6+
"encoding/csv"
67
"encoding/json"
78
"errors"
89
"fmt"
@@ -1013,8 +1014,9 @@ func init() {
10131014
RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder)
10141015
RegisterBodyDecoder("application/x-yaml", yamlBodyDecoder)
10151016
RegisterBodyDecoder("application/yaml", yamlBodyDecoder)
1016-
RegisterBodyDecoder("application/zip", ZipFileBodyDecoder)
1017+
RegisterBodyDecoder("application/zip", zipFileBodyDecoder)
10171018
RegisterBodyDecoder("multipart/form-data", multipartBodyDecoder)
1019+
RegisterBodyDecoder("text/csv", csvBodyDecoder)
10181020
RegisterBodyDecoder("text/plain", plainBodyDecoder)
10191021
}
10201022

@@ -1221,8 +1223,8 @@ func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema
12211223
return string(data), nil
12221224
}
12231225

1224-
// ZipFileBodyDecoder is a body decoder that decodes a zip file body to a string.
1225-
func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
1226+
// zipFileBodyDecoder is a body decoder that decodes a zip file body to a string.
1227+
func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
12261228
buff := bytes.NewBuffer([]byte{})
12271229
size, err := io.Copy(buff, body)
12281230
if err != nil {
@@ -1271,3 +1273,23 @@ func ZipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Sch
12711273

12721274
return string(content), nil
12731275
}
1276+
1277+
// csvBodyDecoder is a body decoder that decodes a csv body to a string.
1278+
func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
1279+
r := csv.NewReader(body)
1280+
1281+
var content string
1282+
for {
1283+
record, err := r.Read()
1284+
if err == io.EOF {
1285+
break
1286+
}
1287+
if err != nil {
1288+
return nil, err
1289+
}
1290+
1291+
content += strings.Join(record, ",") + "\n"
1292+
}
1293+
1294+
return content, nil
1295+
}

openapi3filter/req_resp_decoder_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,7 +1345,7 @@ func TestRegisterAndUnregisterBodyDecoder(t *testing.T) {
13451345
}
13461346
return strings.Split(string(data), ","), nil
13471347
}
1348-
contentType := "text/csv"
1348+
contentType := "application/csv"
13491349
h := make(http.Header)
13501350
h.Set(headerCT, contentType)
13511351

@@ -1371,7 +1371,7 @@ func TestRegisterAndUnregisterBodyDecoder(t *testing.T) {
13711371
_, _, err = decodeBody(body, h, schema, encFn)
13721372
require.Equal(t, &ParseError{
13731373
Kind: KindUnsupportedFormat,
1374-
Reason: prefixUnsupportedCT + ` "text/csv"`,
1374+
Reason: prefixUnsupportedCT + ` "application/csv"`,
13751375
}, err)
13761376
}
13771377

0 commit comments

Comments
 (0)