diff --git a/CHANGELOG.md b/CHANGELOG.md index f2ed5f70..ebd30d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/README.md b/README.md index 915e9f7a..5ae3cf2b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/index.js b/index.js index 78c00697..03727de2 100755 --- a/index.js +++ b/index.js @@ -30,12 +30,13 @@ 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, @@ -43,6 +44,7 @@ generateApi({ generateRouteTypes: routeTypes, generateClient: client, defaultResponseAsSuccess: defaultAsSuccess, + generateUnionEnums: unionEnums, generateResponses: responses, input: resolve(process.cwd(), path), output: resolve(process.cwd(), output || "."), diff --git a/package.json b/package.json index e70a0a90..ca321edf 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/config.js b/src/config.js index 0c5c130b..c916a7c5 100644 --- a/src/config.js +++ b/src/config.js @@ -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 */ diff --git a/src/index.js b/src/index.js index a29bbb5d..792cc63c 100644 --- a/src/index.js +++ b/src/index.js @@ -40,6 +40,7 @@ module.exports = { defaultResponseAsSuccess = config.defaultResponseAsSuccess, generateRouteTypes = config.generateRouteTypes, generateClient = config.generateClient, + generateUnionEnums = config.generateUnionEnums, }) => new Promise((resolve, reject) => { addToConfig({ @@ -48,6 +49,7 @@ module.exports = { generateClient, generateResponses, templates, + generateUnionEnums, }); getSwaggerObject(input, url) .then(({ usageSchema, originalSchema }) => { diff --git a/src/modelTypes.js b/src/modelTypes.js index b6b6f2d6..2458a844 100644 --- a/src/modelTypes.js +++ b/src/modelTypes.js @@ -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; @@ -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, -} +}; diff --git a/src/typeFormatters.js b/src/typeFormatters.js index 667d20fb..af345491 100644 --- a/src/typeFormatters.js +++ b/src/typeFormatters.js @@ -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) => { diff --git a/tests/spec/unionEnums/schema.json b/tests/spec/unionEnums/schema.json new file mode 100644 index 00000000..adabec99 --- /dev/null +++ b/tests/spec/unionEnums/schema.json @@ -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" + } + ] +} diff --git a/tests/spec/unionEnums/schema.ts b/tests/spec/unionEnums/schema.ts new file mode 100644 index 00000000..6106b211 --- /dev/null +++ b/tests/spec/unionEnums/schema.ts @@ -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 & { + secure?: boolean; +}; + +type ApiConfig = { + baseUrl?: string; + baseApiParams?: RequestParams; + securityWorker?: (securityData: SecurityDataType) => RequestParams; +}; + +const enum BodyType { + Json, +} + +class HttpClient { + public baseUrl: string = "http://localhost:8080/api/v1"; + private securityData: SecurityDataType = null as any; + private securityWorker: ApiConfig["securityWorker"] = (() => {}) as any; + + private baseApiParams: RequestParams = { + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + referrerPolicy: "no-referrer", + }; + + constructor({ baseUrl, baseApiParams, securityWorker }: ApiConfig = {}) { + 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 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 = (response: Response): Promise => + response + .json() + .then((data) => data) + .catch((e) => response.text); + + public request = ( + path: string, + method: string, + { secure, ...params }: RequestParams = {}, + body?: any, + bodyType?: BodyType, + secureByDefault?: boolean, + ): Promise => + 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(response); + if (!response.ok) throw data; + return data; + }); +} + +/** + * @title Api + * @baseUrl http://localhost:8080/api/v1 + */ +export class Api extends HttpClient {} diff --git a/tests/spec/unionEnums/test.js b/tests/spec/unionEnums/test.js new file mode 100644 index 00000000..78031ede --- /dev/null +++ b/tests/spec/unionEnums/test.js @@ -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; + }); +});