From ed7a51b762f8b703305d4517ce39a69976af29d1 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 7 Apr 2024 04:55:00 +0100 Subject: [PATCH] chore(compiler): export javascript enums --- packages/openapi-ts/README.md | 2 +- packages/openapi-ts/src/compiler/types.ts | 2 +- packages/openapi-ts/src/utils/escape.ts | 9 +- packages/openapi-ts/src/utils/write/models.ts | 162 +++++++++++++++--- .../openapi-ts/src/utils/write/schemas.ts | 36 ++-- 5 files changed, 166 insertions(+), 45 deletions(-) diff --git a/packages/openapi-ts/README.md b/packages/openapi-ts/README.md index 2d34e9ee5..2f13e509c 100644 --- a/packages/openapi-ts/README.md +++ b/packages/openapi-ts/README.md @@ -1,5 +1,5 @@
- Logo + Logo

OpenAPI Typescript

✨ Turn your OpenAPI specification into a beautiful TypeScript client.

diff --git a/packages/openapi-ts/src/compiler/types.ts b/packages/openapi-ts/src/compiler/types.ts index 3e01cac4e..572b18066 100644 --- a/packages/openapi-ts/src/compiler/types.ts +++ b/packages/openapi-ts/src/compiler/types.ts @@ -7,7 +7,7 @@ import { isType, ots } from './utils'; * @param value - the unknown value. * @returns ts.Expression */ -const toExpression = (value: unknown): ts.Expression | undefined => { +export const toExpression = (value: unknown): ts.Expression | undefined => { if (Array.isArray(value)) { return createArrayType(value); } else if (typeof value === 'object' && value !== null) { diff --git a/packages/openapi-ts/src/utils/escape.ts b/packages/openapi-ts/src/utils/escape.ts index 8dc598534..03aae7794 100644 --- a/packages/openapi-ts/src/utils/escape.ts +++ b/packages/openapi-ts/src/utils/escape.ts @@ -23,11 +23,16 @@ export const unescapeName = (value: string): string => { return value; }; -export const escapeComment = (value: string) => +export const escapeComment = (value: string, insertAsterisk = true) => value .replace(/\*\//g, '*') .replace(/\/\*/g, '*') - .replace(/\r?\n(.*)/g, (_, w) => `${EOL} * ${w.trim()}`); + .replace(/\r?\n(.*)/g, (_, w) => { + if (insertAsterisk) { + return `${EOL} * ${w.trim()}`; + } + return EOL + w.trim(); + }); export const escapeDescription = (value: string) => value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '\\${'); diff --git a/packages/openapi-ts/src/utils/write/models.ts b/packages/openapi-ts/src/utils/write/models.ts index 282ff42c7..de0566233 100644 --- a/packages/openapi-ts/src/utils/write/models.ts +++ b/packages/openapi-ts/src/utils/write/models.ts @@ -3,25 +3,145 @@ import path from 'node:path'; import ts from 'typescript'; -import { tsNodeToString } from '../../compiler/utils'; +import compiler from '../../compiler'; +import { toExpression } from '../../compiler/types'; +import { isType, ots, tsNodeToString } from '../../compiler/utils'; import type { Model } from '../../openApi'; import type { Client } from '../../types/client'; import type { Config } from '../../types/config'; +import { enumKey, enumName, enumValue } from '../enum'; +import { escapeComment } from '../escape'; import type { Templates } from '../handlebars'; +import { toType } from './type'; + +const processEnum = (config: Config, client: Client, model: Model, exportType: boolean) => { + let nodes: Array = []; + if (exportType) { + // {{#if exportType}} + // {{#ifdef description deprecated}} + // /** + // {{#if description}} + // * {{{escapeComment description}}} + // {{/if}} + // {{#if deprecated}} + // * @deprecated + // {{/if}} + // */ + // {{/ifdef}} + // {{#equals @root.$config.enums 'typescript'}} + // export enum {{{name}}} { + // {{#each enum}} + // {{#if x-enum-description}} + // /** + // * {{{escapeComment x-enum-description}}} + // */ + // {{else if description}} + // /** + // * {{{escapeComment description}}} + // */ + // {{/if}} + // {{{enumKey value x-enum-varname}}} = {{{enumValue value}}}, + // {{/each}} + // } + // {{else}} + // export type {{{name}}} = {{{enumUnionType enum}}}; + // {{/equals}} + // {{/if}} + if (model.description || model.deprecated) { + // TODO: figure out deprecated + // {{#if deprecated}} + // * @deprecated + // {{/if}} + const comment = ts.factory.createJSDocComment( + model.description + ? ts.factory.createJSDocText(escapeComment(model.description, false)).text + : undefined, + [ + // ts.factory.createJSDocTypedefTag( + // undefined, + // undefined, + // ts.factory.createIdentifier('{Object}'), + // 'key2 - description or deprecated' + // ), + // ots.string('foo'), + // ts.factory.createJSDocText('bar').text, + // ts.factory.createIdentifier('fooo'), + // ts.factory.createJSDocDeprecatedTag(ts.SyntaxKind.JSDoc), + ] + ); + const commentTrailingNewLine = ts.factory.createIdentifier('\n'); + nodes = [...nodes, comment, commentTrailingNewLine]; + } + } + + if (config.enums === 'javascript') { + const expression = ts.factory.createObjectLiteralExpression( + model.enum + .map(enumerator => { + const key = enumKey(enumerator.value, enumerator['x-enum-varname']); + const initializer = toExpression(enumValue(enumerator.value)); + if (!initializer) { + return undefined; + } + const assignment = ts.factory.createPropertyAssignment(key, initializer); + const comment = enumerator['x-enum-description'] || enumerator.description; + if (!comment) { + return assignment; + } + return ts.addSyntheticLeadingComment( + assignment, + ts.SyntaxKind.MultiLineCommentTrivia, + `*\n * ${escapeComment(comment)}\n `, + true + ); + }) + .filter(isType), + true + ); + nodes = [...nodes, compiler.export.asConst(enumName(config, client, model.name)!, expression)]; + } + + return nodes; +}; + +const processType = (config: Config, client: Client, model: Model) => { + let nodes: Array = []; + if (model.description || model.deprecated) { + // TODO: figure out deprecated + // {{#if deprecated}} + // * @deprecated + // {{/if}} + const comment = ts.factory.createJSDocComment( + model.description ? ts.factory.createJSDocText(escapeComment(model.description, false)).text : undefined, + [ + // ts.factory.createJSDocTypedefTag( + // undefined, + // undefined, + // ts.factory.createIdentifier('{Object}'), + // 'key2 - description or deprecated' + // ), + // ots.string('foo'), + // ts.factory.createJSDocText('bar').text, + // ts.factory.createIdentifier('fooo'), + // ts.factory.createJSDocDeprecatedTag(ts.SyntaxKind.JSDoc), + ] + ); + const commentTrailingNewLine = ts.factory.createIdentifier('\n'); + nodes = [...nodes, comment, commentTrailingNewLine]; + } -// {{#ifdef description deprecated}} -// /** -// {{#if description}} -// * {{{escapeComment description}}} -// {{/if}} -// {{#if deprecated}} -// * @deprecated -// {{/if}} -// */ -// {{/ifdef}} -// export type {{{name}}} = {{>type}}; - -const modelToTypeScriptInterface = (config: Config, model: Model) => { + const typeDeclaration = ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(model.name), + undefined, + ts.factory.createTypeReferenceNode(toType(model, config)!) + ); + nodes = [...nodes, typeDeclaration]; + + return nodes; +}; + +const processModel = (config: Config, client: Client, model: Model) => { const typeDeclaration = ts.factory.createTypeAliasDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(`${model.name}_WIP`), @@ -47,22 +167,18 @@ const modelToTypeScriptInterface = (config: Config, model: Model) => { // return compositionSchema(config, model); return [comment, commentTrailingNewLine, typeDeclaration]; case 'enum': - // {{>exportEnum exportType="true"}} - // return enumSchema(config, model); - return [comment, commentTrailingNewLine, typeDeclaration]; + return processEnum(config, client, model, true); case 'interface': // {{>exportInterface}} // return interfaceSchema(config, model); return [comment, commentTrailingNewLine, typeDeclaration]; default: - // {{>exportType}} - // return genericSchema(config, model); - return [comment, commentTrailingNewLine, typeDeclaration]; + return processType(config, client, model); } }; -const exportModel = (config: Config, model: Model) => { - const expression = modelToTypeScriptInterface(config, model); +const exportModel = (config: Config, client: Client, model: Model) => { + const expression = processModel(config, client, model); let result: string = ''; for (const e of expression) { result += tsNodeToString(e); @@ -92,7 +208,7 @@ export const writeClientModels = async ( let results: string[] = []; for (const model of client.models) { - const result = exportModel(config, model); + const result = exportModel(config, client, model); const resultOld = templates.exports.model({ $config: config, ...model, diff --git a/packages/openapi-ts/src/utils/write/schemas.ts b/packages/openapi-ts/src/utils/write/schemas.ts index 4caafe6a7..82fbbfdc3 100644 --- a/packages/openapi-ts/src/utils/write/schemas.ts +++ b/packages/openapi-ts/src/utils/write/schemas.ts @@ -9,13 +9,13 @@ import type { Templates } from '../handlebars'; const escapeNewline = (value: string) => value.replace(/\n/g, '\\n'); -const arrayObj = (config: Config, model: Model) => { +const processArray = (config: Config, model: Model) => { const properties: Record = { type: 'array', }; if (model.link) { - properties.contains = modelToObj(config, model.link); + properties.contains = processModel(config, model.link); } else { properties.contains = { type: model.base, @@ -41,7 +41,7 @@ const arrayObj = (config: Config, model: Model) => { return properties; }; -const compositionObj = (config: Config, model: Model) => { +const processComposition = (config: Config, model: Model) => { const properties: Record = { type: model.export, }; @@ -49,7 +49,7 @@ const compositionObj = (config: Config, model: Model) => { properties.description = `\`${escapeDescription(model.description)}\``; } - properties.contains = model.properties.map(property => modelToObj(config, property)); + properties.contains = model.properties.map(property => processModel(config, property)); if (model.default !== undefined) { properties.default = model.default; @@ -70,13 +70,13 @@ const compositionObj = (config: Config, model: Model) => { return properties; }; -const dictObj = (config: Config, model: Model) => { +const processDict = (config: Config, model: Model) => { const properties: Record = { type: 'dictionary', }; if (model.link) { - properties.contains = modelToObj(config, model.link); + properties.contains = processModel(config, model.link); } else { properties.contains = { type: model.base, @@ -102,7 +102,7 @@ const dictObj = (config: Config, model: Model) => { return properties; }; -const enumObj = (config: Config, model: Model) => { +const processEnum = (config: Config, model: Model) => { const properties: Record = { type: 'Enum', }; @@ -129,7 +129,7 @@ const enumObj = (config: Config, model: Model) => { return properties; }; -const genericObj = (config: Config, model: Model) => { +const processGeneric = (config: Config, model: Model) => { const properties: Record = {}; if (model.type) { properties.type = model.type; @@ -214,7 +214,7 @@ const genericObj = (config: Config, model: Model) => { return properties; }; -const interfaceObj = (config: Config, model: Model) => { +const processInterface = (config: Config, model: Model) => { const properties: Record = {}; if (model.description) { @@ -225,7 +225,7 @@ const interfaceObj = (config: Config, model: Model) => { model.properties .filter(property => property.name !== '[key: string]') .forEach(property => { - props[property.name] = modelToObj(config, property); + props[property.name] = processModel(config, property); }); properties.properties = props; @@ -248,27 +248,27 @@ const interfaceObj = (config: Config, model: Model) => { return properties; }; -const modelToObj = (config: Config, model: Model) => { +const processModel = (config: Config, model: Model) => { switch (model.export) { case 'all-of': case 'any-of': case 'one-of': - return compositionObj(config, model); + return processComposition(config, model); case 'array': - return arrayObj(config, model); + return processArray(config, model); case 'dictionary': - return dictObj(config, model); + return processDict(config, model); case 'enum': - return enumObj(config, model); + return processEnum(config, model); case 'interface': - return interfaceObj(config, model); + return processInterface(config, model); default: - return genericObj(config, model); + return processGeneric(config, model); } }; const exportSchema = (config: Config, model: Model) => { - const obj = modelToObj(config, model); + const obj = processModel(config, model); const expression = compiler.types.object(obj); return compiler.export.asConst(`$${model.name}`, expression); };