From 23c75a1cd0b09a5b26ef98d7a5771fc2c31387a6 Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Fri, 25 Oct 2019 18:21:57 +0200 Subject: [PATCH 1/7] Support and test for parameters and responses. --- .../gen-api-models-oas3.test.ts.snap | 18 +++++- .../__snapshots__/gen-api-models.test.ts.snap | 16 +++++ src/__tests__/api.yaml | 24 ++++++++ src/__tests__/api_oas3.yaml | 29 +++++++++ src/__tests__/gen-api-models-oas3.test.ts | 21 ++++++- src/__tests__/gen-api-models.test.ts | 20 ++++++ src/gen-api-models.ts | 61 ++++++++++++++++--- 7 files changed, 179 insertions(+), 10 deletions(-) diff --git a/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap b/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap index 6a78104b..e04b4dd0 100644 --- a/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap +++ b/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap @@ -170,7 +170,7 @@ exports[`gen-api-models should generate the operator definition 1`] = ` */ // Request type definition - export type TestAuthBearerT = r.IGetApiRequestType<{readonly bearerToken: string}, \\"Authorization\\", never, r.IResponseType<200, undefined>|r.IResponseType<403, undefined>>; + export type TestAuthBearerT = r.IGetApiRequestType<{readonly bearerToken: string,readonly qo?: string,readonly qr: string}, \\"Authorization\\", never, r.IResponseType<200, undefined>|r.IResponseType<403, undefined>>; // Decodes the success response with a custom success type export function testAuthBearerDecoder(type: t.Type) { return r.composeResponseDecoders(r.ioResponseDecoder<200, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(200, type), r.constantResponseDecoder(403, undefined)); } @@ -508,3 +508,19 @@ exports[`gen-api-models should support file uploads 1`] = ` // Decodes the success response with the type defined in the specs export const testFileUploadDefaultDecoder = () => testFileUploadDecoder(t.undefined);" `; + +exports[`gen-api-models should support generate serializers 1`] = ` +" + /**************************************************************** + * testSerializer + */ + + // Request type definition + export type TestSerializerT = r.IPostApiRequestType<{readonly qo?: string,readonly paginationRequest?: string}, \\"Content-Type\\", never, r.IResponseType<200, Message>|r.IResponseType<403, Problem>>; + + // Decodes the success response with a custom success type + export function testSerializerDecoder(type: t.Type) { return r.composeResponseDecoders(r.ioResponseDecoder<200, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(200, type), r.ioResponseDecoder<403, (typeof Problem)[\\"_A\\"], (typeof Problem)[\\"_O\\"]>(403, Problem)); } + + // Decodes the success response with the type defined in the specs + export const testSerializerDefaultDecoder = () => testSerializerDecoder(Message);" +`; diff --git a/src/__tests__/__snapshots__/gen-api-models.test.ts.snap b/src/__tests__/__snapshots__/gen-api-models.test.ts.snap index 3fa0ca2f..815dc1cd 100644 --- a/src/__tests__/__snapshots__/gen-api-models.test.ts.snap +++ b/src/__tests__/__snapshots__/gen-api-models.test.ts.snap @@ -508,3 +508,19 @@ exports[`gen-api-models should support file uploads 1`] = ` // Decodes the success response with the type defined in the specs export const testFileUploadDefaultDecoder = () => testFileUploadDecoder(t.undefined);" `; + +exports[`gen-api-models should support generate serializers 1`] = ` +" + /**************************************************************** + * testSerializer + */ + + // Request type definition + export type TestSerializerT = r.IPostApiRequestType<{readonly qo?: string,readonly paginationRequest?: string}, \\"Content-Type\\", never, r.IResponseType<200, Message>|r.IResponseType<403, Problem>>; + + // Decodes the success response with a custom success type + export function testSerializerDecoder(type: t.Type) { return r.composeResponseDecoders(r.ioResponseDecoder<200, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(200, type), r.ioResponseDecoder<403, (typeof Problem)[\\"_A\\"], (typeof Problem)[\\"_O\\"]>(403, Problem)); } + + // Decodes the success response with the type defined in the specs + export const testSerializerDefaultDecoder = () => testSerializerDecoder(Message);" +`; diff --git a/src/__tests__/api.yaml b/src/__tests__/api.yaml index fec97352..6bcf4553 100644 --- a/src/__tests__/api.yaml +++ b/src/__tests__/api.yaml @@ -45,7 +45,31 @@ paths: responses: "200": description: "File uploaded" + /test-serializers: + post: + operationId: "testSerializer" + parameters: + - name: "qo" + in: "query" + required: false + type: "string" + - $ref: "#/parameters/PaginationRequest" + responses: + "200": + description: "File uploaded" + schema: + $ref: "#/definitions/Message" + "403": + description: "Error string" + schema: + $ref: "#/definitions/Problem" definitions: + Problem: + properties: + title: + type: string + type: + type: string AllOfTest: allOf: - type: object diff --git a/src/__tests__/api_oas3.yaml b/src/__tests__/api_oas3.yaml index 9e3c66d2..3040c5a1 100644 --- a/src/__tests__/api_oas3.yaml +++ b/src/__tests__/api_oas3.yaml @@ -42,6 +42,29 @@ paths: responses: "200": description: File uploaded + /test-serializers: + post: + operationId: "testSerializer" + parameters: + - name: "qo" + in: "query" + required: false + schema: + type: "string" + - $ref: "#/components/parameters/PaginationRequest" + responses: + "200": + description: "File uploaded" + content: + application/json: + schema: + $ref: "#/components/schemas/Message" + "403": + description: "Error string" + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" servers: - url: https://localhost/api/v1 components: @@ -79,6 +102,12 @@ components: write: Grants write access admin: Grants access to admin operations schemas: + Problem: + properties: + title: + type: string + type: + type: string AllOfTest: allOf: - type: object diff --git a/src/__tests__/gen-api-models-oas3.test.ts b/src/__tests__/gen-api-models-oas3.test.ts index 146aa8b5..8b0fac8f 100644 --- a/src/__tests__/gen-api-models-oas3.test.ts +++ b/src/__tests__/gen-api-models-oas3.test.ts @@ -273,7 +273,26 @@ describe("gen-api-models", () => { "post", operation.operationId, operation, - spec.parameters, + spec.components.parameters, + spec.components.securitySchemes, + [], + {}, + "undefined", + "undefined", + true + ); + + expect(code.e1).toMatchSnapshot(); + }); + + it("should support generate serializers", async () => { + const operation = spec.paths["/test-serializers"].post; + + const code = await renderOperation( + "post", + operation.operationId, + operation, + spec.components.parameters, spec.components.securitySchemes, [], {}, diff --git a/src/__tests__/gen-api-models.test.ts b/src/__tests__/gen-api-models.test.ts index 059131e2..4d7edeb3 100644 --- a/src/__tests__/gen-api-models.test.ts +++ b/src/__tests__/gen-api-models.test.ts @@ -284,4 +284,24 @@ describe("gen-api-models", () => { expect(code.e1).toMatchSnapshot(); }); + + it("should support generate serializers", async () => { + const operation = spec.paths["/test-serializers"].post; + + const code = await renderOperation( + "post", + operation.operationId, + operation, + spec.parameters, + spec.securityDefinitions, + [], + {}, + "undefined", + "undefined", + true + ); + + expect(code.e1).toMatchSnapshot(); + }); + }); diff --git a/src/gen-api-models.ts b/src/gen-api-models.ts index 1946278c..c888b942 100644 --- a/src/gen-api-models.ts +++ b/src/gen-api-models.ts @@ -62,13 +62,25 @@ function typeFromRef( s: string ): ITuple2<"definition" | "parameter" | "other", string> | undefined { const parts = s.split("/"); + + if (!parts) { + return undefined; + } + + // If it's an OAS3, remove the "components" part. + if (parts[1] === "components") { + parts.splice(1,1); + } + if (parts && parts.length === 3) { const refType: "definition" | "parameter" | "other" = parts[1] === "definitions" ? "definition" - : parts[1] === "parameters" - ? "parameter" - : "other"; + : parts[1] === "schemas" + ? "definition" + : parts[1] === "parameters" + ? "parameter" + : "other"; return Tuple2(refType, parts[2]); } return undefined; @@ -120,11 +132,12 @@ export function renderOperation( OpenAPIV2.InBodyParameterObject | OpenAPIV3.ParameterObject >; parameters.forEach(param => { - if (param.name && (param as any).type) { + if (param.name && getParameterType(param)) { // The parameter description is inline + // and the parameter type is not undefined. const isRequired = param.required === true; params[`${param.name}${isRequired ? "" : "?"}`] = specTypeToTs( - (param as any).type + getParameterType(param)! ); return; } @@ -148,11 +161,17 @@ export function renderOperation( console.warn(`Unrecognized ref type [${refInParam}]`); return; } + // if the reference type is "definition" + // e2 contains a schema object + // otherwise it is the schema name const paramType: string | undefined = refType === "definition" ? parsedRef.e2 - : specParameters - ? specTypeToTs((specParameters as any)[parsedRef.e2].type) + // check that specParameters contain a valid declaration too! + : specParameters && getParameterType((specParameters as any)[parsedRef.e2]) + ? specTypeToTs( + getParameterType((specParameters as any)[parsedRef.e2])! + ) : undefined; if (paramType === undefined) { @@ -217,7 +236,19 @@ export function renderOperation( const responses = Object.keys(operation.responses as object).map( responseStatus => { const response = operation.responses![responseStatus]; - const typeRef = response.schema ? response.schema.$ref : undefined; + const media_type = "application/json" + const typeRef = + // get schema from Swagger... + response.schema + ? response.schema.$ref + // ... or try with OAS3 + : response.content + ? response.content[media_type] + && response.content[media_type].schema + ? response.content[media_type].schema.$ref + : undefined + // Not OAS2 or missing media-type in response.content + : undefined; const parsedRef = typeRef ? typeFromRef(typeRef) : undefined; if (parsedRef !== undefined) { importedTypes.add(parsedRef.e2); @@ -273,6 +304,20 @@ export function renderOperation( return Tuple2(code, importedTypes); } +function getParameterType( + parameter: + any | undefined +): string | undefined { + if (!parameter) { + return undefined; + } + return parameter.type + ? parameter.type + : parameter.schema + ? parameter.schema.type + : undefined; +} + function getAuthHeaders( securityDefinitions: | OpenAPIV2.SecurityDefinitionsObject From 363856fa3f1d3869741d72f7c23e35459322ce28 Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Thu, 7 Nov 2019 17:27:41 +0100 Subject: [PATCH 2/7] Test request body. --- .../__snapshots__/gen-api-models.test.ts.snap | 16 ++++++++++++++++ src/__tests__/api.yaml | 11 +++++++++++ src/__tests__/gen-api-models.test.ts | 19 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/__tests__/__snapshots__/gen-api-models.test.ts.snap b/src/__tests__/__snapshots__/gen-api-models.test.ts.snap index 815dc1cd..629ee252 100644 --- a/src/__tests__/__snapshots__/gen-api-models.test.ts.snap +++ b/src/__tests__/__snapshots__/gen-api-models.test.ts.snap @@ -509,6 +509,22 @@ exports[`gen-api-models should support file uploads 1`] = ` export const testFileUploadDefaultDecoder = () => testFileUploadDecoder(t.undefined);" `; +exports[`gen-api-models should support generate requestbody 1`] = ` +" + /**************************************************************** + * testRequestBody + */ + + // Request type definition + export type TestRequestBodyT = r.IPostApiRequestType<{readonly body?: #/definitions/Problem}, \\"Content-Type\\", never, r.IResponseType<204, undefined>>; + + // Decodes the success response with a custom success type + export function testRequestBodyDecoder(type: t.Type) { return r.ioResponseDecoder<204, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(204, type); } + + // Decodes the success response with the type defined in the specs + export const testRequestBodyDefaultDecoder = () => testRequestBodyDecoder(t.undefined);" +`; + exports[`gen-api-models should support generate serializers 1`] = ` " /**************************************************************** diff --git a/src/__tests__/api.yaml b/src/__tests__/api.yaml index 6bcf4553..6d560753 100644 --- a/src/__tests__/api.yaml +++ b/src/__tests__/api.yaml @@ -63,6 +63,17 @@ paths: description: "Error string" schema: $ref: "#/definitions/Problem" + /test-requestbody: + post: + operationId: "testRequestBody" + parameters: + - in: body + name: body + schema: + type: "#/definitions/Problem" + responses: + "204": + description: No content definitions: Problem: properties: diff --git a/src/__tests__/gen-api-models.test.ts b/src/__tests__/gen-api-models.test.ts index 4d7edeb3..a1fc0d74 100644 --- a/src/__tests__/gen-api-models.test.ts +++ b/src/__tests__/gen-api-models.test.ts @@ -304,4 +304,23 @@ describe("gen-api-models", () => { expect(code.e1).toMatchSnapshot(); }); + it("should support generate requestbody", async () => { + const operation = spec.paths["/test-requestbody"].post; + + const code = await renderOperation( + "post", + operation.operationId, + operation, + spec.parameters, + spec.securityDefinitions, + [], + {}, + "undefined", + "undefined", + true + ); + + expect(code.e1).toMatchSnapshot(); + }); + }); From 92f572c11e9093a5609b6673f39ae7f11dee8b3f Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Thu, 7 Nov 2019 17:37:06 +0100 Subject: [PATCH 3/7] Fix tests. Related to #141. --- src/__tests__/__snapshots__/gen-api-models.test.ts.snap | 2 +- src/__tests__/api.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/__snapshots__/gen-api-models.test.ts.snap b/src/__tests__/__snapshots__/gen-api-models.test.ts.snap index 629ee252..5558f836 100644 --- a/src/__tests__/__snapshots__/gen-api-models.test.ts.snap +++ b/src/__tests__/__snapshots__/gen-api-models.test.ts.snap @@ -516,7 +516,7 @@ exports[`gen-api-models should support generate requestbody 1`] = ` */ // Request type definition - export type TestRequestBodyT = r.IPostApiRequestType<{readonly body?: #/definitions/Problem}, \\"Content-Type\\", never, r.IResponseType<204, undefined>>; + export type TestRequestBodyT = r.IPostApiRequestType<{readonly problem?: Problem}, \\"Content-Type\\", never, r.IResponseType<204, undefined>>; // Decodes the success response with a custom success type export function testRequestBodyDecoder(type: t.Type) { return r.ioResponseDecoder<204, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(204, type); } diff --git a/src/__tests__/api.yaml b/src/__tests__/api.yaml index 6d560753..e114fa46 100644 --- a/src/__tests__/api.yaml +++ b/src/__tests__/api.yaml @@ -70,7 +70,7 @@ paths: - in: body name: body schema: - type: "#/definitions/Problem" + "$ref": "#/definitions/Problem" responses: "204": description: No content From 38e5aa105ff2adf978cbdd926d917c0f7690e895 Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Thu, 7 Nov 2019 17:48:25 +0100 Subject: [PATCH 4/7] Add test for request body. Related to #141. --- .../gen-api-models-oas3.test.ts.snap | 17 ++++++++++++++++ src/__tests__/api_oas3.yaml | 11 ++++++++++ src/__tests__/gen-api-models-oas3.test.ts | 20 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap b/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap index e04b4dd0..d81ed65c 100644 --- a/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap +++ b/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap @@ -509,6 +509,23 @@ exports[`gen-api-models should support file uploads 1`] = ` export const testFileUploadDefaultDecoder = () => testFileUploadDecoder(t.undefined);" `; +exports[`gen-api-models should support generate requestbody 1`] = ` +" + /**************************************************************** + * testRequestBody + */ + + // Request type definition + export type TestRequestBodyT = r.IPostApiRequestType<{readonly problem?: Problem}, \\"Content-Type\\", never, r.IResponseType<204, undefined>>; + + // Decodes the success response with a custom success type + export function testRequestBodyDecoder(type: t.Type) { return r.ioResponseDecoder<204, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(204, type); } + + // Decodes the success response with the type defined in the specs + export const testRequestBodyDefaultDecoder = () => testRequestBodyDecoder(t.undefined);" + +`; + exports[`gen-api-models should support generate serializers 1`] = ` " /**************************************************************** diff --git a/src/__tests__/api_oas3.yaml b/src/__tests__/api_oas3.yaml index 3040c5a1..df1eb811 100644 --- a/src/__tests__/api_oas3.yaml +++ b/src/__tests__/api_oas3.yaml @@ -65,6 +65,17 @@ paths: application/json: schema: $ref: "#/components/schemas/Problem" + /test-requestbody: + post: + operationId: "testRequestBody" + requestBody: + content: + application/json: + schema: + "$ref": "#/components/schemas/Problem" + responses: + "204": + description: No content servers: - url: https://localhost/api/v1 components: diff --git a/src/__tests__/gen-api-models-oas3.test.ts b/src/__tests__/gen-api-models-oas3.test.ts index 8b0fac8f..7149f590 100644 --- a/src/__tests__/gen-api-models-oas3.test.ts +++ b/src/__tests__/gen-api-models-oas3.test.ts @@ -303,4 +303,24 @@ describe("gen-api-models", () => { expect(code.e1).toMatchSnapshot(); }); + + it("should support generate requestbody", async () => { + const operation = spec.paths["/test-requestbody"].post; + + const code = await renderOperation( + "post", + operation.operationId, + operation, + spec.components.parameters, + spec.components.securitySchemes, + [], + {}, + "undefined", + "undefined", + true + ); + + expect(code.e1).toMatchSnapshot(); + }); + }); From 59e32e7a80be20caaed84065b7ab435ab17880d9 Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Thu, 7 Nov 2019 19:17:27 +0100 Subject: [PATCH 5/7] Ongoing work on requestBody --- .../gen-api-models-oas3.test.ts.snap | 4 +- src/__tests__/api_oas3.yaml | 1 - src/gen-api-models.ts | 59 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap b/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap index d81ed65c..12e60a58 100644 --- a/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap +++ b/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap @@ -500,8 +500,8 @@ exports[`gen-api-models should support file uploads 1`] = ` */ // Request type definition - export type TestFileUploadT = r.IPostApiRequestType<{}, never, never, r.IResponseType<200, undefined>>; - + export type TestFileUploadT = r.IPostApiRequestType<{readonly file: { uri: string, name: string, type: string }}, \\"Content-Type\\", never, r.IResponseType<200, undefined>>; + // Decodes the success response with a custom success type export function testFileUploadDecoder(type: t.Type) { return r.ioResponseDecoder<200, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(200, type); } diff --git a/src/__tests__/api_oas3.yaml b/src/__tests__/api_oas3.yaml index df1eb811..5682fa79 100644 --- a/src/__tests__/api_oas3.yaml +++ b/src/__tests__/api_oas3.yaml @@ -304,4 +304,3 @@ components: - is_email_set - name - version - diff --git a/src/gen-api-models.ts b/src/gen-api-models.ts index c888b942..9b9e5074 100644 --- a/src/gen-api-models.ts +++ b/src/gen-api-models.ts @@ -108,6 +108,29 @@ function getDecoderForResponse(status: string, type: string): string { } } +// Get a schema from a request or response body +function getSchemaFromBody( +item: any, // OpenAPIV3.RequestBodyObject | +preferred_media_type: string | undefined +): string | undefined { + + try { + const content = item.content; + if (preferred_media_type in content) { + return item.content[preferred_media_type].schema.$ref; + } + // FIXME if there's more than one property then console.warn + const first_content = content[Object.keys(content)[0]] + if ($ref in first_content.schema) { + return first_content.schema.$ref; + } + return first_content.schema; + } catch { + console.warn(`f ${media_type } ${ JSON.stringify(item)}`); + return undefined; + } +} + export function renderOperation( method: string, operationId: string, @@ -127,6 +150,42 @@ export function renderOperation( const requestType = `r.I${capitalize(method)}ApiRequestType`; const params: { [key: string]: string } = {}; const importedTypes = new Set(); + + // Eventually process requestBody + if ((operation as any).requestBody !== undefined) { + const item = (operation as any).requestBody; + const application_json = "application/json" + + console.warn(`requestBody ${ JSON.stringify(item) }`); + const typeRef = getSchemaFromBody(item, application_json); + + const parsedRef = typeRef ? typeFromRef(typeRef) : undefined; + console.warn(`requestBody.typeRef ${ JSON.stringify({'1': parsedRef, '2': typeRef}) }`); + if (parsedRef) { + const refType = parsedRef.e1; // "definition" + + // TODO implement if required... + const isParamRequired = false; + const paramName = `${uncapitalize(parsedRef.e2)}${ + isParamRequired ? "" : "?" + }`; + + console.warn(`requestBody.paramName ${ JSON.stringify(paramName) }`); + + params[paramName] = parsedRef.e2; + if (refType === "definition") { + importedTypes.add(parsedRef.e2); + } + + + } else { + console.warn(`Cannot extract type from ref [${typeRef}]`); + } + + } + + + // Process ordinary parameters if (operation.parameters !== undefined) { const parameters = operation.parameters as Array< OpenAPIV2.InBodyParameterObject | OpenAPIV3.ParameterObject From d9205d7ae5966a3f80566d54dbb0c6607eb3de63 Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Sun, 10 Nov 2019 16:45:32 +0100 Subject: [PATCH 6/7] completed implementation for file uploads --- .../gen-api-models-oas3.test.ts.snap | 5 +- src/__tests__/api_oas3.yaml | 1 + src/gen-api-models.ts | 133 ++++++++++++------ 3 files changed, 95 insertions(+), 44 deletions(-) diff --git a/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap b/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap index 12e60a58..5558f836 100644 --- a/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap +++ b/src/__tests__/__snapshots__/gen-api-models-oas3.test.ts.snap @@ -501,7 +501,7 @@ exports[`gen-api-models should support file uploads 1`] = ` // Request type definition export type TestFileUploadT = r.IPostApiRequestType<{readonly file: { uri: string, name: string, type: string }}, \\"Content-Type\\", never, r.IResponseType<200, undefined>>; - + // Decodes the success response with a custom success type export function testFileUploadDecoder(type: t.Type) { return r.ioResponseDecoder<200, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(200, type); } @@ -517,13 +517,12 @@ exports[`gen-api-models should support generate requestbody 1`] = ` // Request type definition export type TestRequestBodyT = r.IPostApiRequestType<{readonly problem?: Problem}, \\"Content-Type\\", never, r.IResponseType<204, undefined>>; - + // Decodes the success response with a custom success type export function testRequestBodyDecoder(type: t.Type) { return r.ioResponseDecoder<204, (typeof type)[\\"_A\\"], (typeof type)[\\"_O\\"]>(204, type); } // Decodes the success response with the type defined in the specs export const testRequestBodyDefaultDecoder = () => testRequestBodyDecoder(t.undefined);" - `; exports[`gen-api-models should support generate serializers 1`] = ` diff --git a/src/__tests__/api_oas3.yaml b/src/__tests__/api_oas3.yaml index 5682fa79..524c0a39 100644 --- a/src/__tests__/api_oas3.yaml +++ b/src/__tests__/api_oas3.yaml @@ -29,6 +29,7 @@ paths: post: operationId: testFileUpload requestBody: + required: true content: multipart/form-data: schema: diff --git a/src/gen-api-models.ts b/src/gen-api-models.ts index 9b9e5074..00530bb7 100644 --- a/src/gen-api-models.ts +++ b/src/gen-api-models.ts @@ -108,29 +108,74 @@ function getDecoderForResponse(status: string, type: string): string { } } -// Get a schema from a request or response body +/** Get the first schema from an OAS3 request or response body + */ function getSchemaFromBody( -item: any, // OpenAPIV3.RequestBodyObject | -preferred_media_type: string | undefined -): string | undefined { + item: any, +): OpenAPIV3.BaseSchemaObject | string | undefined { try { const content = item.content; - if (preferred_media_type in content) { - return item.content[preferred_media_type].schema.$ref; + const media_types = Object.keys(content); + + if (media_types.length == 0) { + console.warn(`Missing media-type in ${JSON.stringify(item)}`); + return undefined; } - // FIXME if there's more than one property then console.warn - const first_content = content[Object.keys(content)[0]] - if ($ref in first_content.schema) { - return first_content.schema.$ref; + + if (media_types.length > 1) { + console.warn(`Multiple media-types in ${JSON.stringify(item)}`); + return undefined; } - return first_content.schema; + + const media_type = content[media_types[0]]; + return "$ref" in media_type.schema ? media_type.schema.$ref : media_type.schema; + } catch { - console.warn(`f ${media_type } ${ JSON.stringify(item)}`); + console.warn(`Cannot get schema from body: ${JSON.stringify(item)}`); return undefined; } } +/** + * Convert an OAS3 Object to typescript. + * This function supports only one level of schema properties: + * for nested schemas define a schema object. + */ +function specObjectToTs( + item: any +) : string | undefined { + + if ((item.properties || item.type) == false) { + return undefined; + } + + if (item.properties) { + item = item.properties; + + // File upload implementation used in io-utils. + const file_upload_schema = {"file": {"type": "string", "format": "binary"}} + if (JSON.stringify(item) == JSON.stringify(file_upload_schema)) { + console.log(`Found file upload pattern`); + return specTypeToTs("file"); + } + + for (let p in item) { + item[p] = item[p].type; + } + return JSON.stringify(item).replace(/"/g, " "); + } + + if (item.type) { + // Support for generic OAS3 binary file upload + // see https://swagger.io/docs/specification/describing-request-body/file-upload/ + if (item.type == "string" && item.format == "binary") { + return specTypeToTs("file"); + } + return specTypeToTs(item.type); + } +} + export function renderOperation( method: string, operationId: string, @@ -152,39 +197,44 @@ export function renderOperation( const importedTypes = new Set(); // Eventually process requestBody - if ((operation as any).requestBody !== undefined) { - const item = (operation as any).requestBody; - const application_json = "application/json" - - console.warn(`requestBody ${ JSON.stringify(item) }`); - const typeRef = getSchemaFromBody(item, application_json); - - const parsedRef = typeRef ? typeFromRef(typeRef) : undefined; - console.warn(`requestBody.typeRef ${ JSON.stringify({'1': parsedRef, '2': typeRef}) }`); - if (parsedRef) { - const refType = parsedRef.e1; // "definition" - - // TODO implement if required... - const isParamRequired = false; + if (isV3OperationWithBody(operation) && operation.requestBody !== undefined) { + const item = operation.requestBody; + const typeRefOrSchema = getSchemaFromBody(item); + const isParamRequired = (item as OpenAPIV3.RequestBodyObject).required === true; + if (typeRefOrSchema) { + const inlineRequestBody = specObjectToTs(typeRefOrSchema); + + // parameter is defined inline + if (inlineRequestBody){ + const schema = (typeRefOrSchema as OpenAPIV3.BaseSchemaObject); + const parameterName = schema.properties + ? schema.properties.file + ? "file" + : "body" + : "body"; + params[`${parameterName}${isParamRequired ? "" : "?"}`] = inlineRequestBody; + } else { // parameter is in $ref + const schema = (typeRefOrSchema as string); + const parsedRef = typeRefOrSchema ? typeFromRef(schema) : undefined; + console.debug(`requestBody.typeRef ${ JSON.stringify({'1': parsedRef, '2': typeRefOrSchema}) }`); + + if (parsedRef) { + const refType = parsedRef.e1; const paramName = `${uncapitalize(parsedRef.e2)}${ isParamRequired ? "" : "?" }`; - console.warn(`requestBody.paramName ${ JSON.stringify(paramName) }`); - params[paramName] = parsedRef.e2; if (refType === "definition") { importedTypes.add(parsedRef.e2); } - - - } else { - console.warn(`Cannot extract type from ref [${typeRef}]`); + } } - + } else { + console.warn(`Cannot extract type from ref [${typeRefOrSchema}]`); + } } - // Process ordinary parameters if (operation.parameters !== undefined) { const parameters = operation.parameters as Array< @@ -301,13 +351,8 @@ export function renderOperation( response.schema ? response.schema.$ref // ... or try with OAS3 - : response.content - ? response.content[media_type] - && response.content[media_type].schema - ? response.content[media_type].schema.$ref - : undefined - // Not OAS2 or missing media-type in response.content - : undefined; + : getSchemaFromBody(response); + const parsedRef = typeRef ? typeFromRef(typeRef) : undefined; if (parsedRef !== undefined) { importedTypes.add(parsedRef.e2); @@ -453,6 +498,12 @@ export function isOpenAPIV3( return specs.hasOwnProperty("openapi"); } +export function isV3OperationWithBody( + item: any +): item is OpenAPIV3.OperationObject { + return item.hasOwnProperty("requestBody"); +} + export async function generateApi( env: nunjucks.Environment, specFilePath: string, From ab5e2ae2981906398ddb41e2087f20de692411ad Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Sun, 10 Nov 2019 23:06:24 +0100 Subject: [PATCH 7/7] Removed unused variable. --- src/gen-api-models.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gen-api-models.ts b/src/gen-api-models.ts index 00530bb7..a536675f 100644 --- a/src/gen-api-models.ts +++ b/src/gen-api-models.ts @@ -345,7 +345,6 @@ export function renderOperation( const responses = Object.keys(operation.responses as object).map( responseStatus => { const response = operation.responses![responseStatus]; - const media_type = "application/json" const typeRef = // get schema from Swagger... response.schema