From 9778695faaa12daddf0a0bd24336a42f23859c3f Mon Sep 17 00:00:00 2001 From: Artem Derevnjuk Date: Tue, 28 Feb 2023 17:45:37 +0400 Subject: [PATCH] fix(oas): add support for multipart subtypes closes #182 --- .../converter/parts/postdata/BodyConverter.ts | 89 ++++++++---- .../tests/fixtures/multipart.oas.result.json | 41 ++++-- .../oas/tests/fixtures/multipart.oas.yaml | 128 +++++------------- 3 files changed, 126 insertions(+), 132 deletions(-) diff --git a/packages/oas/src/converter/parts/postdata/BodyConverter.ts b/packages/oas/src/converter/parts/postdata/BodyConverter.ts index 15cd8064..76523740 100644 --- a/packages/oas/src/converter/parts/postdata/BodyConverter.ts +++ b/packages/oas/src/converter/parts/postdata/BodyConverter.ts @@ -50,6 +50,8 @@ export abstract class BodyConverter }; } + // 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, @@ -72,7 +74,19 @@ export abstract class BodyConverter 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': @@ -101,43 +115,72 @@ export abstract class BodyConverter : 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, - 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; + 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 diff --git a/packages/oas/tests/fixtures/multipart.oas.result.json b/packages/oas/tests/fixtures/multipart.oas.result.json index c0d41b93..97137f6c 100644 --- a/packages/oas/tests/fixtures/multipart.oas.result.json +++ b/packages/oas/tests/fixtures/multipart.oas.result.json @@ -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" } ] diff --git a/packages/oas/tests/fixtures/multipart.oas.yaml b/packages/oas/tests/fixtures/multipart.oas.yaml index 8f772a5c..a3954097 100644 --- a/packages/oas/tests/fixtures/multipart.oas.yaml +++ b/packages/oas/tests/fixtures/multipart.oas.yaml @@ -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