Skip to content

Commit 99883a7

Browse files
committed
completed implementation for file uploads
1 parent 59e32e7 commit 99883a7

3 files changed

Lines changed: 94 additions & 35 deletions

File tree

src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ exports[`gen-api-models should support file uploads 1`] = `
501501
502502
// Request type definition
503503
export type TestFileUploadT = r.IPostApiRequestType<{readonly file: { uri: string, name: string, type: string }}, \\"Content-Type\\", never, r.IResponseType<200, undefined>>;
504-
504+
505505
// Decodes the success response with a custom success type
506506
export function testFileUploadDecoder<A, O>(type: t.Type<A, O>) { return r.ioResponseDecoder<200, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(200, type); }
507507

src/__tests__/api_oas3.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ paths:
2929
post:
3030
operationId: testFileUpload
3131
requestBody:
32+
required: true
3233
content:
3334
multipart/form-data:
3435
schema:

src/gen-api-models.ts

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -108,29 +108,74 @@ function getDecoderForResponse(status: string, type: string): string {
108108
}
109109
}
110110

111-
// Get a schema from a request or response body
111+
/** Get the first schema from an OAS3 request or response body
112+
*/
112113
function getSchemaFromBody(
113-
item: any, // OpenAPIV3.RequestBodyObject |
114-
preferred_media_type: string | undefined
115-
): string | undefined {
114+
item: any,
115+
): OpenAPIV3.BaseSchemaObject | string | undefined {
116116

117117
try {
118118
const content = item.content;
119-
if (preferred_media_type in content) {
120-
return item.content[preferred_media_type].schema.$ref;
119+
const media_types = Object.keys(content);
120+
121+
if (media_types.length == 0) {
122+
console.warn(`Missing media-type in ${JSON.stringify(item)}`);
123+
return undefined;
121124
}
122-
// FIXME if there's more than one property then console.warn
123-
const first_content = content[Object.keys(content)[0]]
124-
if ($ref in first_content.schema) {
125-
return first_content.schema.$ref;
125+
126+
if (media_types.length > 1) {
127+
console.warn(`Multiple media-types in ${JSON.stringify(item)}`);
128+
return undefined;
126129
}
127-
return first_content.schema;
130+
131+
const media_type = content[media_types[0]];
132+
return "$ref" in media_type.schema ? media_type.schema.$ref : media_type.schema;
133+
128134
} catch {
129-
console.warn(`f ${media_type } ${ JSON.stringify(item)}`);
135+
console.warn(`Cannot get schema from body: ${JSON.stringify(item)}`);
130136
return undefined;
131137
}
132138
}
133139

140+
/**
141+
* Convert an OAS3 Object to typescript.
142+
* This function supports only one level of schema properties:
143+
* for nested schemas define a schema object.
144+
*/
145+
function specObjectToTs(
146+
item: any
147+
) : string | undefined {
148+
149+
if ((item.properties || item.type) == false) {
150+
return undefined;
151+
}
152+
153+
if (item.properties) {
154+
item = item.properties;
155+
156+
// File upload implementation used in io-utils.
157+
const file_upload_schema = {"file": {"type": "string", "format": "binary"}}
158+
if (JSON.stringify(item) == JSON.stringify(file_upload_schema)) {
159+
console.warn(`Found file upload pattern`);
160+
return specTypeToTs("file");
161+
}
162+
163+
for (let p in item) {
164+
item[p] = item[p].type;
165+
}
166+
return JSON.stringify(item).replace(/"/g, " ");
167+
}
168+
169+
if (item.type) {
170+
// Support for generic OAS3 binary file upload
171+
// see https://swagger.io/docs/specification/describing-request-body/file-upload/
172+
if (item.type == "string" && item.format == "binary") {
173+
return specTypeToTs("file");
174+
}
175+
return specTypeToTs(item.type);
176+
}
177+
}
178+
134179
export function renderOperation(
135180
method: string,
136181
operationId: string,
@@ -152,39 +197,46 @@ export function renderOperation(
152197
const importedTypes = new Set<string>();
153198

154199
// Eventually process requestBody
155-
if ((operation as any).requestBody !== undefined) {
156-
const item = (operation as any).requestBody;
157-
const application_json = "application/json"
158-
159-
console.warn(`requestBody ${ JSON.stringify(item) }`);
160-
const typeRef = getSchemaFromBody(item, application_json);
161-
162-
const parsedRef = typeRef ? typeFromRef(typeRef) : undefined;
163-
console.warn(`requestBody.typeRef ${ JSON.stringify({'1': parsedRef, '2': typeRef}) }`);
164-
if (parsedRef) {
165-
const refType = parsedRef.e1; // "definition"
166-
167-
// TODO implement if required...
168-
const isParamRequired = false;
200+
if (isV3OperationWithBody(operation) && operation.requestBody !== undefined) {
201+
const item = operation.requestBody;
202+
const typeRefOrSchema = getSchemaFromBody(item);
203+
const isParamRequired = (item as OpenAPIV3.RequestBodyObject).required === true;
204+
console.warn(`Required: ${isParamRequired}`);
205+
if (typeRefOrSchema) {
206+
const inlineRequestBody = specObjectToTs(typeRefOrSchema);
207+
208+
// inline parameter definition
209+
if (inlineRequestBody){
210+
const schema = (typeRefOrSchema as OpenAPIV3.BaseSchemaObject);
211+
const parameterName = schema.properties
212+
? schema.properties.file
213+
? "file"
214+
: "body"
215+
: "body";
216+
params[`${parameterName}${isParamRequired ? "" : "?"}`] = inlineRequestBody;
217+
} else { // referenced parameter
218+
const schema = (typeRefOrSchema as string);
219+
const parsedRef = typeRefOrSchema ? typeFromRef(schema) : undefined;
220+
console.debug(`requestBody.typeRef ${ JSON.stringify({'1': parsedRef, '2': typeRefOrSchema}) }`);
221+
222+
if (parsedRef) {
223+
const refType = parsedRef.e1;
169224
const paramName = `${uncapitalize(parsedRef.e2)}${
170225
isParamRequired ? "" : "?"
171226
}`;
172227

173-
console.warn(`requestBody.paramName ${ JSON.stringify(paramName) }`);
174-
228+
console.debug(`requestBody.paramName ${ JSON.stringify(paramName) }`);
175229
params[paramName] = parsedRef.e2;
176230
if (refType === "definition") {
177231
importedTypes.add(parsedRef.e2);
178232
}
179-
180-
181-
} else {
182-
console.warn(`Cannot extract type from ref [${typeRef}]`);
233+
}
183234
}
184-
235+
} else {
236+
console.warn(`Cannot extract type from ref [${typeRefOrSchema}]`);
237+
}
185238
}
186239

187-
188240
// Process ordinary parameters
189241
if (operation.parameters !== undefined) {
190242
const parameters = operation.parameters as Array<
@@ -453,6 +505,12 @@ export function isOpenAPIV3(
453505
return specs.hasOwnProperty("openapi");
454506
}
455507

508+
export function isV3OperationWithBody(
509+
item: any
510+
): item is OpenAPIV3.OperationObject {
511+
return item.hasOwnProperty("requestBody");
512+
}
513+
456514
export async function generateApi(
457515
env: nunjucks.Environment,
458516
specFilePath: string,

0 commit comments

Comments
 (0)