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..5558f836 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)); }
@@ -500,7 +500,7 @@ 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); }
@@ -508,3 +508,35 @@ 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 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`] = `
+"
+ /****************************************************************
+ * 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..5558f836 100644
--- a/src/__tests__/__snapshots__/gen-api-models.test.ts.snap
+++ b/src/__tests__/__snapshots__/gen-api-models.test.ts.snap
@@ -508,3 +508,35 @@ 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 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`] = `
+"
+ /****************************************************************
+ * 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..e114fa46 100644
--- a/src/__tests__/api.yaml
+++ b/src/__tests__/api.yaml
@@ -45,7 +45,42 @@ 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"
+ /test-requestbody:
+ post:
+ operationId: "testRequestBody"
+ parameters:
+ - in: body
+ name: body
+ schema:
+ "$ref": "#/definitions/Problem"
+ responses:
+ "204":
+ description: No content
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..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:
@@ -42,6 +43,40 @@ 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"
+ /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:
@@ -79,6 +114,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
@@ -264,4 +305,3 @@ components:
- is_email_set
- name
- version
-
diff --git a/src/__tests__/gen-api-models-oas3.test.ts b/src/__tests__/gen-api-models-oas3.test.ts
index 146aa8b5..7149f590 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,
[],
{},
@@ -284,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();
+ });
+
});
diff --git a/src/__tests__/gen-api-models.test.ts b/src/__tests__/gen-api-models.test.ts
index 059131e2..a1fc0d74 100644
--- a/src/__tests__/gen-api-models.test.ts
+++ b/src/__tests__/gen-api-models.test.ts
@@ -284,4 +284,43 @@ 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();
+ });
+
+ 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();
+ });
+
});
diff --git a/src/gen-api-models.ts b/src/gen-api-models.ts
index 1946278c..a536675f 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;
@@ -96,6 +108,74 @@ function getDecoderForResponse(status: string, type: string): string {
}
}
+/** Get the first schema from an OAS3 request or response body
+ */
+function getSchemaFromBody(
+ item: any,
+): OpenAPIV3.BaseSchemaObject | string | undefined {
+
+ try {
+ const content = item.content;
+ const media_types = Object.keys(content);
+
+ if (media_types.length == 0) {
+ console.warn(`Missing media-type in ${JSON.stringify(item)}`);
+ return undefined;
+ }
+
+ if (media_types.length > 1) {
+ console.warn(`Multiple media-types in ${JSON.stringify(item)}`);
+ return undefined;
+ }
+
+ const media_type = content[media_types[0]];
+ return "$ref" in media_type.schema ? media_type.schema.$ref : media_type.schema;
+
+ } catch {
+ 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,
@@ -115,16 +195,58 @@ export function renderOperation(
const requestType = `r.I${capitalize(method)}ApiRequestType`;
const params: { [key: string]: string } = {};
const importedTypes = new Set();
+
+ // Eventually process requestBody
+ 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 ? "" : "?"
+ }`;
+
+ params[paramName] = parsedRef.e2;
+ if (refType === "definition") {
+ importedTypes.add(parsedRef.e2);
+ }
+ }
+ }
+ } else {
+ console.warn(`Cannot extract type from ref [${typeRefOrSchema}]`);
+ }
+ }
+
+ // Process ordinary parameters
if (operation.parameters !== undefined) {
const parameters = operation.parameters as Array<
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 +270,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 +345,13 @@ 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 typeRef =
+ // get schema from Swagger...
+ response.schema
+ ? response.schema.$ref
+ // ... or try with OAS3
+ : getSchemaFromBody(response);
+
const parsedRef = typeRef ? typeFromRef(typeRef) : undefined;
if (parsedRef !== undefined) {
importedTypes.add(parsedRef.e2);
@@ -273,6 +407,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
@@ -349,6 +497,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,