Skip to content

[Feature] --union-enums CLI option #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
});
});