Skip to content

Commit

Permalink
fix(oas): add support for multipart subtypes
Browse files Browse the repository at this point in the history
closes #182
  • Loading branch information
derevnjuk committed Feb 28, 2023
1 parent 88621b7 commit 9778695
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 132 deletions.
89 changes: 66 additions & 23 deletions packages/oas/src/converter/parts/postdata/BodyConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
};
}

// TODO: move the logic that receives the content type from the encoding object
// to the {@link Oas3RequestBodyConverter} class.
// eslint-disable-next-line complexity
protected encodeValue({
value,
Expand All @@ -72,7 +74,19 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
return this.encodeXml(value, schema);
case 'multipart/form-data':
case 'multipart/mixed':
return this.encodeMultipartFormData(value, fields, schema);
case 'multipart/alternative':
case 'multipart/related':
return this.encodeMultipart(
Object.entries(value || {}).map(([key, val]: [string, unknown]) =>
this.createPart({
key,
schema,
fields,
value: val,
formData: mime.startsWith('multipart/form-data')
})
)
);
case 'image/x-icon':
case 'image/ico':
case 'image/vnd.microsoft.icon':
Expand Down Expand Up @@ -101,43 +115,72 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
: encoded;
}

// TODO: move the logic that receives the content type from the encoding object
// to the {@link Oas3RequestBodyConverter} class.
private encodeMultipartFormData(
value: unknown,
fields?: Record<string, OpenAPIV3.EncodingObject>,
schema?: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject
private encodeMultipart(
parts: {
content: unknown;
contentType?: string;
base64Encoded?: boolean;
rawHeaders?: string[];
}[]
): string {
const EOL = '\r\n';

return Object.entries(value || {})
.map(([key, val]: [string, unknown]) => {
const propertySchema = this.getPropertySchema(key, schema);
const contentType =
fields?.[key]?.contentType ??
this.inferContentType(val, propertySchema);

const headers = [
`Content-Disposition: form-data; name="${key}"${
this.filenameRequired(contentType) ? `; filename="${key}"` : ''
}`,
return parts
.map(({ content, contentType, base64Encoded, rawHeaders }) => {
const headers: string[] = [
...(rawHeaders ?? []),
...(contentType !== 'text/plain'
? [`Content-Type: ${contentType}`]
: []),
...(this.BASE64_FORMATS.includes(propertySchema?.format)
? ['Content-Transfer-Encoding: base64']
: [])
...(base64Encoded ? ['Content-Transfer-Encoding: base64'] : [])
];
const body = this.encodeOther(val);

return `--${this.BOUNDARY}${EOL}${headers.join(
EOL
)}${EOL}${EOL}${body}`;
)}${EOL}${EOL}${content}`;
})
.join(EOL)
.concat(`${EOL}--${this.BOUNDARY}--`);
}

private createPart({
key,
value,
schema,
fields,
formData
}: {
key: string;
value: unknown;
schema?: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject;
fields?: Record<string, OpenAPIV3.EncodingObject>;
formData?: boolean;
}): {
content: string;
rawHeaders?: string[];
base64Encoded?: boolean;
contentType?: string;
} {
const propertySchema = this.getPropertySchema(key, schema);
const contentType =
fields?.[key]?.contentType ??
this.inferContentType(value, propertySchema);
const content = this.encodeOther(value);

return {
content,
contentType,
rawHeaders: formData
? [
`Content-Disposition: form-data; name="${key}"${
this.filenameRequired(contentType) ? `; filename="${key}"` : ''
}`
]
: [],
base64Encoded: this.BASE64_FORMATS.includes(propertySchema?.format)
};
}

private getPropertySchema(
key: string,
schema?: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject
Expand Down
41 changes: 28 additions & 13 deletions packages/oas/tests/fixtures/multipart.oas.result.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
[
{
"queryString": [],
"url": "https://petstore.swagger.io/v2/pet",
"method": "PUT",
"bodySize": 0,
"cookies": [],
"headers": [
{
"value": "multipart/form-data",
"name": "content-type"
},
{
"name": "authorization",
"value": "Bearer ZHVtbXkgYmluYXJ5IHNhbXBsZQA="
"name": "content-type",
"value": "multipart/form-data"
}
],
"headersSize": 0,
"httpVersion": "HTTP/1.1",
"method": "PUT",
"postData": {
"mimeType": "multipart/form-data; boundary=956888039105887155673143",
"text": "--956888039105887155673143\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\nfbdf5a53-161e-4460-98ad-0e39408d8689\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"address\"\r\nContent-Type: application/json\r\n\r\n{\"street\":\"lorem\",\"city\":\"lorem\"}\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"profileImage\"\r\nContent-Type: image/png, image/jpeg\r\nContent-Transfer-Encoding: base64\r\n\r\niVBORw0KGgo=\r\n--956888039105887155673143--"
},
"queryString": [],
"url": "https://petstore.swagger.io/v2/pet"
},
{
"bodySize": 0,
"cookies": [],
"headers": [
{
"name": "content-type",
"value": "multipart/alternative"
}
],
"headersSize": 0,
"bodySize": 0,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "multipart/form-data; boundary=956888039105887155673143",
"text": "--956888039105887155673143\r\nContent-Disposition: form-data; name=\"required\"\r\nContent-Type: application/json\r\n\r\nnull\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"type\"\r\nContent-Type: application/json\r\n\r\nnull\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"properties\"\r\nContent-Type: application/json\r\n\r\nnull\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"xml\"\r\nContent-Type: application/json\r\n\r\nnull\r\n--956888039105887155673143--"
}
"mimeType": "multipart/alternative; boundary=956888039105887155673143",
"text": "--956888039105887155673143\r\nContent-Type: application/octet-stream\r\n\r\n\u0001\u0002\u0003\u0004\u0005\r\n--956888039105887155673143--"
},
"queryString": [],
"url": "https://petstore.swagger.io/v2/pet"
}
]
128 changes: 32 additions & 96 deletions packages/oas/tests/fixtures/multipart.oas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,110 +12,46 @@ paths:
/pet:
put:
summary: Update an existing pet
operationId: updatePet
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
$ref: '#/components/schemas/Pet'
id:
type: string
format: uuid
address:
type: object
properties:
street:
type: string
city:
type: string
profileImage:
type: string
format: base64
encoding:
profileImage:
contentType: image/png, image/jpeg
responses:
400:
description: Invalid ID supplied
content: {}
404:
description: Pet not found
content: {}
405:
description: Validation exception
post:
summary: Create pets
requestBody:
content:
multipart/alternative:
schema:
type: object
properties:
filename:
type: array
items:
type: string
format: binary
responses:
400:
description: Invalid ID supplied
content: {}
security:
- petstore_auth:
- write:pets
- read:pets
components:
schemas:
Category:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: Category
Tag:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: Tag
Pet:
required:
- name
- photoUrls
type: object
properties:
id:
type: integer
format: int64
category:
$ref: '#/components/schemas/Category'
name:
type: string
example: doggie
photoUrls:
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
tags:
type: array
xml:
name: tag
wrapped: true
items:
$ref: '#/components/schemas/Tag'
status:
type: string
description: pet status in the store
enum:
- available
- pending
- sold
profileImage:
type: string
format: binary
xml:
name: Pet
ApiResponse:
type: object
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: http://petstore.swagger.io/oauth/dialog
scopes:
write:pets: modify pets in your account
read:pets: read your pets
api_key:
type: apiKey
name: api_key
in: header

0 comments on commit 9778695

Please sign in to comment.