Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(oas): add support for multipart subtypes #201

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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