Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.

Commit eb8f356

Browse files
authored
Merge pull request #177 from apiaryio/honzajavorek/apib-multipart
Work around an APIB parser bug (multipart/form-data)
2 parents 3c11b70 + 2d72a26 commit eb8f356

File tree

5 files changed

+165
-10
lines changed

5 files changed

+165
-10
lines changed

src/compile.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ function findRelevantTransactions(mediaType, apiElements) {
1616
//
1717
// This is very specific to API Blueprint and to backwards compatibility
1818
// of Dredd. There's a plan to migrate to so-called "transaction paths"
19-
// in the future (apiaryio/dredd#227), which won't use the concept
20-
// of transaction examples anymore.
19+
// in the future (https://github.com/apiaryio/dredd/issues/227), which
20+
// won't use the concept of transaction examples anymore.
2121
const transactionExampleNumbers = detectTransactionExampleNumbers(transitionElement);
2222
const hasMoreExamples = Math.max(...Array.from(transactionExampleNumbers || [])) > 1;
2323

@@ -91,6 +91,24 @@ function compileOrigin(mediaType, filename, httpTransactionElement, exampleNo) {
9191
};
9292
}
9393

94+
function hasMultipartBody(headers) {
95+
return !!headers.filter(({ name, value }) =>
96+
name.toLowerCase() === 'content-type'
97+
&& value.toLowerCase().includes('multipart')
98+
).length;
99+
}
100+
101+
function compileBody(messageBodyElement, isMultipart) {
102+
if (!messageBodyElement) { return ''; }
103+
104+
const body = messageBodyElement.toValue() || '';
105+
if (!isMultipart) { return body; }
106+
107+
// Fixing manually written 'multipart/form-data' bodies (API Blueprint
108+
// issue: https://github.com/apiaryio/api-blueprint/issues/401)
109+
return body.replace(/\r?\n/g, '\r\n');
110+
}
111+
94112
function compileRequest(httpRequestElement) {
95113
let request;
96114
const { uri, annotations } = compileUri(httpRequestElement);
@@ -106,11 +124,12 @@ function compileRequest(httpRequestElement) {
106124
});
107125

108126
if (uri) {
127+
const headers = compileHeaders(httpRequestElement.headers);
109128
request = {
110129
method: httpRequestElement.method.toValue(),
111130
uri,
112-
headers: compileHeaders(httpRequestElement.headers),
113-
body: (httpRequestElement.messageBody ? httpRequestElement.messageBody.toValue() : undefined) || ''
131+
headers,
132+
body: compileBody(httpRequestElement.messageBody, hasMultipartBody(headers))
114133
};
115134
} else {
116135
request = null;
@@ -120,13 +139,11 @@ function compileRequest(httpRequestElement) {
120139
}
121140

122141
function compileResponse(httpResponseElement) {
123-
const response = {
124-
status: (httpResponseElement.statusCode ? httpResponseElement.statusCode.toValue() : undefined) || '200',
125-
headers: compileHeaders(httpResponseElement.headers)
126-
};
142+
const status = (httpResponseElement.statusCode ? httpResponseElement.statusCode.toValue() : undefined) || '200';
143+
const headers = compileHeaders(httpResponseElement.headers);
144+
const response = { status, headers };
127145

128-
const body = httpResponseElement.messageBody ?
129-
httpResponseElement.messageBody.toValue() : undefined;
146+
const body = compileBody(httpResponseElement.messageBody, hasMultipartBody(headers));
130147
if (body) { response.body = body; }
131148

132149
const schema = httpResponseElement.messageBodySchema ?
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
FORMAT: 1A
2+
3+
# Sanbox 'multipart/form-data' API
4+
5+
# POST /data
6+
7+
+ Request (multipart/form-data; boundary=CUSTOM-BOUNDARY)
8+
9+
+ Body
10+
11+
--CUSTOM-BOUNDARY
12+
Content-Disposition: form-data; name="text"
13+
Content-Type: text/plain
14+
15+
test equals to 42
16+
--CUSTOM-BOUNDARY
17+
Content-Disposition: form-data; name="json"
18+
Content-Type: application/json
19+
20+
{"test": 42}
21+
22+
--CUSTOM-BOUNDARY--
23+
24+
+ Response 200 (multipart/form-data; boundary=CUSTOM-BOUNDARY)
25+
26+
+ Body
27+
28+
--CUSTOM-BOUNDARY
29+
Content-Disposition: form-data; name="text"
30+
Content-Type: text/plain
31+
32+
test equals to 42
33+
--CUSTOM-BOUNDARY
34+
Content-Disposition: form-data; name="json"
35+
Content-Type: application/json
36+
37+
{"test": 42}
38+
39+
--CUSTOM-BOUNDARY--

test/fixtures/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ const fixtures = {
136136
apiBlueprint: fromFile('./api-blueprint/no-schema.apib'),
137137
swagger: fromFile('./swagger/no-schema.yml')
138138
}),
139+
multipartFormData: fixture({
140+
apiBlueprint: fromFile('./api-blueprint/multipart-form-data.apib'),
141+
swagger: fromFile('./swagger/multipart-form-data.yml')
142+
}),
139143

140144
// Specific to API Blueprint
141145
unrecognizable: fixture({
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
swagger: '2.0'
2+
info:
3+
title: "Sanbox 'multipart/form-data' API"
4+
version: '1.0'
5+
consumes:
6+
- multipart/form-data; boundary=CUSTOM-BOUNDARY
7+
produces:
8+
- multipart/form-data; boundary=CUSTOM-BOUNDARY
9+
paths:
10+
'/data':
11+
post:
12+
parameters:
13+
- name: text
14+
in: formData
15+
type: string
16+
required: true
17+
x-example: "test equals to 42"
18+
- name: json
19+
in: formData
20+
type: string
21+
required: true
22+
x-example: '{"test": 42}'
23+
responses:
24+
200:
25+
description: 'Test'
26+
examples:
27+
multipart/form-data; boundary=CUSTOM-BOUNDARY: |
28+
--CUSTOM-BOUNDARY
29+
Content-Disposition: form-data; name="text"
30+
Content-Type: text/plain
31+
32+
test equals to 42
33+
--CUSTOM-BOUNDARY
34+
Content-Disposition: form-data; name="json"
35+
Content-Type: application/json
36+
37+
{"test": 42}
38+
39+
--CUSTOM-BOUNDARY--

test/integration/compile-test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,4 +641,60 @@ describe('compile() · all API description formats', () => {
641641
);
642642
})
643643
);
644+
645+
describe('with \'multipart/form-data\' message bodies', () => {
646+
fixtures.multipartFormData.forEachDescribe(({ source, format }) => {
647+
let compilationResult;
648+
const expectedBody = [
649+
'--CUSTOM-BOUNDARY',
650+
'Content-Disposition: form-data; name="text"',
651+
'Content-Type: text/plain',
652+
'',
653+
'test equals to 42',
654+
'--CUSTOM-BOUNDARY',
655+
'Content-Disposition: form-data; name="json"',
656+
'Content-Type: application/json',
657+
'',
658+
'{"test": 42}',
659+
'',
660+
'--CUSTOM-BOUNDARY--',
661+
''
662+
].join('\r\n');
663+
664+
before(done =>
665+
compileFixture(source, (...args) => {
666+
compilationResult = args[1];
667+
done(args[0]);
668+
})
669+
);
670+
671+
it('produces no annotations and 1 transaction', () =>
672+
assert.jsonSchema(compilationResult, createCompilationResultSchema({
673+
annotations: 0,
674+
transactions: 1
675+
}))
676+
);
677+
678+
context('the transaction', () => {
679+
it('has the expected request body', () => {
680+
// Remove the lines with Content-Type headers as Swagger doesn't
681+
// support generating them for 'multipart/form-data' request bodies
682+
const expectedRequestBody = format === 'Swagger'
683+
? expectedBody.split('\r\n').filter(line => !line.match(/Content-Type/)).join('\r\n')
684+
: expectedBody;
685+
686+
assert.deepEqual(
687+
compilationResult.transactions[0].request.body,
688+
expectedRequestBody
689+
);
690+
});
691+
it('has the expected response body', () =>
692+
assert.deepEqual(
693+
compilationResult.transactions[0].response.body,
694+
expectedBody
695+
)
696+
);
697+
});
698+
});
699+
});
644700
});

0 commit comments

Comments
 (0)