Skip to content

Commit

Permalink
[Feature] --union-enums CLI option (#61)
Browse files Browse the repository at this point in the history
* feat: add --union-enums cli option which generate all enums as union types

* docs: update README, CHANGELOG

* chore(fix): manual test for union enums option
  • Loading branch information
js2me committed Jun 26, 2020
1 parent 353526d commit c1e360f
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 19 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# next release

Features:
- `--union-enums` CLI option. Allows to generate all enums as union types.
For example, schema part:
```
"StringEnum": {
"enum": ["String1", "String2", "String3", "String4"],
"type": "string"
}
```
will be converted into:
```
export type StringEnum = "String1" | "String2" | "String3" | "String4";
```


# 1.8.4

Fixes:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Options:
as success response type by default. (default: false)
-r, --responses generate additional information about request responses
also add typings for bad responses
--union-enums generate all "enum" types as union types (T1 | T2 | TN) (default: false)
--route-types generate type definitions for API routes (default: false)
--no-client do not generate an API class
-h, --help output usage information
Expand Down
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,21 @@ program
"also add typings for bad responses",
false,
)
.option("--union-enums", 'generate all "enum" types as union types (T1 | T2 | TN)', false)
.option("--route-types", "generate type definitions for API routes", false)
.option("--no-client", "do not generate an API class", false);

program.parse(process.argv);

const { path, output, name, templates, routeTypes, client, defaultAsSuccess, responses } = program;
const { path, output, name, templates, unionEnums, routeTypes, client, defaultAsSuccess, responses } = program;

generateApi({
name,
url: path,
generateRouteTypes: routeTypes,
generateClient: client,
defaultResponseAsSuccess: defaultAsSuccess,
generateUnionEnums: unionEnums,
generateResponses: responses,
input: resolve(process.cwd(), path),
output: resolve(process.cwd(), output || "."),
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"scripts": {
"cli:json": "node index.js -r -d -p ./swagger-test-cli.json -n swagger-test-cli.ts",
"cli:yaml": "node index.js -r -d -p ./swagger-test-cli.yaml -n swagger-test-cli.ts",
"cli:debug:json": "node --nolazy --inspect-brk=9229 index.js -p ./swagger-test-cli.json -n swagger-test-cli.ts",
"cli:debug:json": "node --nolazy --inspect-brk=9229 index.js -p ./swagger-test-cli.json -n swagger-test-cli.ts --union-enums",
"cli:debug:yaml": "node --nolazy --inspect-brk=9229 index.js -p ./swagger-test-cli.yaml -n swagger-test-cli.ts",
"cli:help": "node index.js -h",
"test:all": "npm-run-all generate validate test:routeTypes test:noClient test:defaultAsSuccess test:responses test:templates --continue-on-error",
"test:all": "npm-run-all generate validate test:routeTypes test:noClient test:defaultAsSuccess test:responses test:templates test:unionEnums --continue-on-error",
"generate": "node tests/generate.js",
"generate:debug": "node --nolazy --inspect-brk=9229 tests/generate.js",
"validate": "node tests/validate.js",
Expand All @@ -17,6 +17,7 @@
"test:noClient": "node tests/spec/noClient/test.js",
"test:defaultAsSuccess": "node tests/spec/defaultAsSuccess/test.js",
"test:templates": "node tests/spec/templates/test.js",
"test:unionEnums": "node tests/spec/unionEnums/test.js",
"test:responses": "node tests/spec/responses/test.js"
},
"author": "acacode",
Expand Down
2 changes: 2 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const config = {
generateRouteTypes: false,
/** CLI flag */
generateClient: true,
/** CLI flag */
generateUnionEnums: false,
/** parsed swagger schema from getSwaggerObject() */

/** parsed swagger schema ref */
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ module.exports = {
defaultResponseAsSuccess = config.defaultResponseAsSuccess,
generateRouteTypes = config.generateRouteTypes,
generateClient = config.generateClient,
generateUnionEnums = config.generateUnionEnums,
}) =>
new Promise((resolve, reject) => {
addToConfig({
Expand All @@ -48,6 +49,7 @@ module.exports = {
generateClient,
generateResponses,
templates,
generateUnionEnums,
});
getSwaggerObject(input, url)
.then(({ usageSchema, originalSchema }) => {
Expand Down
39 changes: 24 additions & 15 deletions src/modelTypes.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
const _ = require('lodash');
const _ = require("lodash");
const { formatters } = require("./typeFormatters");
const { checkAndRenameModelName } = require("./modelNames");
const { formatDescription } = require("./common")
const { getTypeData } = require('./components');
const { formatDescription } = require("./common");
const { config } = require("./config");
const { getTypeData } = require("./components");

const CONTENT_KEYWORD = '__CONTENT__';
const CONTENT_KEYWORD = "__CONTENT__";

const contentWrapersByTypeIdentifier = {
'enum': `{\r\n${CONTENT_KEYWORD} \r\n }`,
'interface': `{\r\n${CONTENT_KEYWORD}}`,
'type': `= ${CONTENT_KEYWORD}`,
}
enum: `{\r\n${CONTENT_KEYWORD} \r\n }`,
interface: `{\r\n${CONTENT_KEYWORD}}`,
type: `= ${CONTENT_KEYWORD}`,
};

const getModelType = typeInfo => {
const { typeIdentifier, name: originalName, content, type, description } = getTypeData(typeInfo);
const getModelType = (typeInfo) => {
let { typeIdentifier, name: originalName, content, type, description } = getTypeData(typeInfo);

if (config.generateUnionEnums && typeIdentifier === "enum") {
typeIdentifier = "type";
}

if (!contentWrapersByTypeIdentifier[typeIdentifier]) {
throw new Error(`${typeIdentifier} - type identifier is unknown for this utility`)
throw new Error(`${typeIdentifier} - type identifier is unknown for this utility`);
}

const resultContent = formatters[type] ? formatters[type](content) : content;
Expand All @@ -27,11 +32,15 @@ const getModelType = typeInfo => {
name,
rawContent: resultContent,
description: formatDescription(description),
content: _.replace(contentWrapersByTypeIdentifier[typeIdentifier], CONTENT_KEYWORD, resultContent)
}
}
content: _.replace(
contentWrapersByTypeIdentifier[typeIdentifier],
CONTENT_KEYWORD,
resultContent,
),
};
};

module.exports = {
getModelType,
checkAndRenameModelName,
}
};
6 changes: 5 additions & 1 deletion src/typeFormatters.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
const _ = require("lodash");
const { config } = require("./config");
const { checkAndRenameModelName } = require("./modelNames");

const formatters = {
enum: (content) => _.map(content, ({ key, value }) => ` ${key} = ${value}`).join(",\n"),
enum: (content) =>
_.map(content, ({ key, value }) =>
config.generateUnionEnums ? value : ` ${key} = ${value}`,
).join(config.generateUnionEnums ? " | " : ",\n"),
intEnum: (content) => _.map(content, ({ value }) => value).join(" | "),
object: (content) =>
_.map(content, (part) => {
Expand Down
30 changes: 30 additions & 0 deletions tests/spec/unionEnums/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"components": {
"examples": {},
"headers": {},
"parameters": {},
"requestBodies": {},
"responses": {},
"schemas": {
"StringEnum": {
"enum": ["String1", "String2", "String3", "String4"],
"type": "string"
},
"NumberEnum": {
"enum": [1, 2, 3, 4],
"type": "number"
}
},
"securitySchemes": {}
},
"info": {
"title": ""
},
"openapi": "3.0.0",
"paths": {},
"servers": [
{
"url": "http://localhost:8080/api/v1"
}
]
}
102 changes: 102 additions & 0 deletions tests/spec/unionEnums/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* tslint:disable */
/* eslint-disable */

/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/

export type StringEnum = "String1" | "String2" | "String3" | "String4";

export type NumberEnum = 1 | 2 | 3 | 4;

export type RequestParams = Omit<RequestInit, "body" | "method"> & {
secure?: boolean;
};

type ApiConfig<SecurityDataType> = {
baseUrl?: string;
baseApiParams?: RequestParams;
securityWorker?: (securityData: SecurityDataType) => RequestParams;
};

const enum BodyType {
Json,
}

class HttpClient<SecurityDataType> {
public baseUrl: string = "http://localhost:8080/api/v1";
private securityData: SecurityDataType = null as any;
private securityWorker: ApiConfig<SecurityDataType>["securityWorker"] = (() => {}) as any;

private baseApiParams: RequestParams = {
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
},
redirect: "follow",
referrerPolicy: "no-referrer",
};

constructor({ baseUrl, baseApiParams, securityWorker }: ApiConfig<SecurityDataType> = {}) {
this.baseUrl = baseUrl || this.baseUrl;
this.baseApiParams = baseApiParams || this.baseApiParams;
this.securityWorker = securityWorker || this.securityWorker;
}

public setSecurityData = (data: SecurityDataType) => {
this.securityData = data;
};

private bodyFormatters: Record<BodyType, (input: any) => any> = {
[BodyType.Json]: JSON.stringify,
};

private mergeRequestOptions(params: RequestParams, securityParams?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params,
...(securityParams || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params.headers || {}),
...((securityParams && securityParams.headers) || {}),
},
};
}

private safeParseResponse = <T = any, E = any>(response: Response): Promise<T> =>
response
.json()
.then((data) => data)
.catch((e) => response.text);

public request = <T = any, E = any>(
path: string,
method: string,
{ secure, ...params }: RequestParams = {},
body?: any,
bodyType?: BodyType,
secureByDefault?: boolean,
): Promise<T> =>
fetch(`${this.baseUrl}${path}`, {
// @ts-ignore
...this.mergeRequestOptions(params, (secureByDefault || secure) && this.securityWorker(this.securityData)),
method,
body: body ? this.bodyFormatters[bodyType || BodyType.Json](body) : null,
}).then(async (response) => {
const data = await this.safeParseResponse<T, E>(response);
if (!response.ok) throw data;
return data;
});
}

/**
* @title Api
* @baseUrl http://localhost:8080/api/v1
*/
export class Api<SecurityDataType = any> extends HttpClient<SecurityDataType> {}
25 changes: 25 additions & 0 deletions tests/spec/unionEnums/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { generateApi } = require("../../../src");
const { resolve } = require("path");
const validateGeneratedModule = require("../../helpers/validateGeneratedModule");
const createSchemasInfos = require("../../helpers/createSchemaInfos");

const schemas = createSchemasInfos({ absolutePathToSchemas: resolve(__dirname, "./") });

schemas.forEach(({ absolutePath, apiFileName }) => {
generateApi({
name: apiFileName,
input: absolutePath,
output: resolve(__dirname, "./"),
generateUnionEnums: true,
})
.then(() => {
const diagnostics = validateGeneratedModule({
pathToFile: resolve(__dirname, `./${apiFileName}`),
});
if (diagnostics.length) throw "Failed";
})
.catch((e) => {
console.error("unionEnums option test failed.");
throw e;
});
});

0 comments on commit c1e360f

Please sign in to comment.