Skip to content

Commit

Permalink
chore(compiler): export javascript enums
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlubos committed Apr 7, 2024
1 parent 6828ef5 commit ed7a51b
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 45 deletions.
2 changes: 1 addition & 1 deletion packages/openapi-ts/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div align="center">
<img width="150" height="150" src="./docs/public/logo.png" alt="Logo">
<img width="150" height="150" src="https://heyapi.vercel.app/logo.png" alt="Logo">
<h1 align="center"><b>OpenAPI Typescript</b></h1>
<p align="center">✨ Turn your OpenAPI specification into a beautiful TypeScript client.</p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts/src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 7 additions & 2 deletions packages/openapi-ts/src/utils/escape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '\\${');
162 changes: 139 additions & 23 deletions packages/openapi-ts/src/utils/write/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import ots.
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<ts.JSDoc | ts.TypeAliasDeclaration | ts.Identifier | ts.VariableStatement> = [];
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<ts.PropertyAssignment>),
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<ts.JSDoc | ts.TypeAliasDeclaration | ts.Identifier | ts.VariableStatement> = [];
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`),
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
36 changes: 18 additions & 18 deletions packages/openapi-ts/src/utils/write/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {
type: 'array',
};

if (model.link) {
properties.contains = modelToObj(config, model.link);
properties.contains = processModel(config, model.link);
} else {
properties.contains = {
type: model.base,
Expand All @@ -41,15 +41,15 @@ const arrayObj = (config: Config, model: Model) => {
return properties;
};

const compositionObj = (config: Config, model: Model) => {
const processComposition = (config: Config, model: Model) => {
const properties: Record<string, unknown> = {
type: model.export,
};
if (model.description) {
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;
Expand All @@ -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<string, unknown> = {
type: 'dictionary',
};

if (model.link) {
properties.contains = modelToObj(config, model.link);
properties.contains = processModel(config, model.link);
} else {
properties.contains = {
type: model.base,
Expand All @@ -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<string, unknown> = {
type: 'Enum',
};
Expand All @@ -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<string, unknown> = {};
if (model.type) {
properties.type = model.type;
Expand Down Expand Up @@ -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<string, unknown> = {};

if (model.description) {
Expand All @@ -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;

Expand All @@ -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);
};
Expand Down

0 comments on commit ed7a51b

Please sign in to comment.