Skip to content

Commit

Permalink
Merge pull request #1127 from hey-api/feat/zod-plugin-models
Browse files Browse the repository at this point in the history
feat: zod plugin handles models
  • Loading branch information
mrlubos authored Oct 19, 2024
2 parents 41e4259 + 49b6e6e commit 4ff2429
Show file tree
Hide file tree
Showing 97 changed files with 9,117 additions and 1,280 deletions.
3 changes: 2 additions & 1 deletion packages/openapi-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"rxjs": "7.8.1",
"ts-node": "10.9.2",
"tslib": "2.6.3",
"typescript": "5.5.3"
"typescript": "5.5.3",
"zod": "3.23.8"
}
}
157 changes: 2 additions & 155 deletions packages/openapi-ts/src/compiler/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import { type PathLike, rmSync, writeFileSync } from 'node:fs';
import path from 'node:path';

import type ts from 'typescript';

import { ensureDirSync } from '../generate/utils';
import * as classes from './classes';
import * as convert from './convert';
import * as module from './module';
Expand All @@ -18,155 +12,6 @@ export type { FunctionParameter } from './types';
export type { Comments } from './utils';
export type { ClassElement, Node, TypeNode } from 'typescript';

const splitNameAndExtension = (fileName: string) => {
const match = fileName.match(/\.[0-9a-z]+$/i);
const extension = match ? match[0].slice(1) : '';
const name = fileName.slice(
0,
fileName.length - (extension ? extension.length + 1 : 0),
);
return { extension, name };
};

export class TypeScriptFile {
private _headers: Array<string> = [];
private _imports = new Map<
string,
Map<string, utils.ImportExportItemObject>
>();
private _items: Array<ts.Node | string> = [];
private _name: string;
private _path: PathLike;

public constructor({
dir,
name,
header = true,
}: {
dir: string;
header?: boolean;
name: string;
}) {
this._name = this._setName(name);
this._path = path.resolve(dir, this.getName());

if (header) {
this._headers = [
...this._headers,
'// This file is auto-generated by @hey-api/openapi-ts',
];
}
}

public add(...nodes: Array<ts.Node | string>) {
this._items = [...this._items, ...nodes];
}

/**
* Adds an import to the provided module. Handles duplication, returns added import.
*/
public import({
module,
...importedItem
}: utils.ImportExportItemObject & {
module: string;
}): utils.ImportExportItemObject {
let moduleMap = this._imports.get(module);

if (!moduleMap) {
moduleMap = new Map<string, utils.ImportExportItemObject>();
this._imports.set(module, moduleMap);
}

const match = moduleMap.get(importedItem.name);
if (match) {
return match;
}

moduleMap.set(importedItem.name, importedItem);
return importedItem;
}

public getName(withExtension = true) {
if (withExtension) {
return this._name;
}

const { name } = splitNameAndExtension(this._name);
return name;
}

public isEmpty() {
return !this._items.length;
}

public remove(options?: Parameters<typeof rmSync>[1]) {
rmSync(this._path, options);
}

/**
* Removes last node form the stack. Works as undo.
*/
public removeNode() {
this._items = this._items.slice(0, this._items.length - 1);
}

private _setName(fileName: string) {
if (fileName.includes('index')) {
return fileName;
}

const { extension, name } = splitNameAndExtension(fileName);
return [name, 'gen', extension].filter(Boolean).join('.');
}

public toString(separator: string = '\n') {
let output: string[] = [];
if (this._headers.length) {
output = [...output, this._headers.join('\n')];
}
let importsStringArray: string[] = [];
for (const [_module, moduleMap] of this._imports.entries()) {
const imports = Array.from(moduleMap.values());
const node = compiler.namedImportDeclarations({
imports,
module: _module,
});
importsStringArray = [
...importsStringArray,
utils.tsNodeToString({ node }),
];
}
if (importsStringArray.length) {
output = [...output, importsStringArray.join('\n')];
}
output = [
...output,
...this._items.map((node) =>
typeof node === 'string'
? node
: utils.tsNodeToString({ node, unescape: true }),
),
];
return output.join(separator);
}

public write(separator = '\n') {
if (this.isEmpty()) {
this.remove({ force: true });
return;
}

let dir = this._path;
if (typeof this._path === 'string') {
const parts = this._path.split(path.sep);
dir = parts.slice(0, parts.length - 1).join(path.sep);
}
ensureDirSync(dir);
writeFileSync(this._path, this.toString(separator));
}
}

export const compiler = {
anonymousFunction: types.createAnonymousFunction,
arrayLiteralExpression: types.createArrayLiteralExpression,
Expand All @@ -187,10 +32,12 @@ export const compiler = {
indexedAccessTypeNode: types.createIndexedAccessTypeNode,
isTsNode: utils.isTsNode,
keywordTypeNode: types.createKeywordTypeNode,
literalTypeNode: types.createLiteralTypeNode,
methodDeclaration: classes.createMethodDeclaration,
namedImportDeclarations: module.createNamedImportDeclarations,
namespaceDeclaration: types.createNamespaceDeclaration,
nodeToString: utils.tsNodeToString,
null: types.createNull,
objectExpression: types.createObjectType,
ots: utils.ots,
propertyAccessExpression: types.createPropertyAccessExpression,
Expand Down
113 changes: 86 additions & 27 deletions packages/openapi-ts/src/compiler/typedef.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import ts from 'typescript';

import { createTypeNode, createTypeReferenceNode } from './types';
import { addLeadingComments, type Comments, tsNodeToString } from './utils';
import { validTypescriptIdentifierRegExp } from '../utils/regexp';
import {
createKeywordTypeNode,
createStringLiteral,
createTypeNode,
createTypeReferenceNode,
} from './types';
import {
addLeadingComments,
type Comments,
createIdentifier,
tsNodeToString,
} from './utils';

const nullNode = createTypeReferenceNode({ typeName: 'null' });

Expand Down Expand Up @@ -38,40 +49,83 @@ const maybeNullable = ({
* @returns ts.TypeLiteralNode | ts.TypeUnionNode
*/
export const createTypeInterfaceNode = ({
indexProperty,
isNullable,
properties,
useLegacyResolution,
}: {
/**
* Adds an index signature if defined.
* @example
* ```ts
* type IndexProperty = {
* [key: string]: string
* }
* ```
*/
indexProperty?: Property;
isNullable?: boolean;
properties: Property[];
useLegacyResolution: boolean;
}) => {
const node = ts.factory.createTypeLiteralNode(
properties.map((property) => {
const modifiers: readonly ts.Modifier[] | undefined = property.isReadOnly
? [ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)]
: undefined;
const propertyTypes: Array<ts.TypeNode> = [];

const members: Array<ts.TypeElement> = properties.map((property) => {
const modifiers: readonly ts.Modifier[] | undefined = property.isReadOnly
? [ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)]
: undefined;

const questionToken: ts.QuestionToken | undefined =
property.isRequired !== false
? undefined
: ts.factory.createToken(ts.SyntaxKind.QuestionToken);

const type: ts.TypeNode | undefined = createTypeNode(property.type);
propertyTypes.push(type);

const questionToken: ts.QuestionToken | undefined =
property.isRequired !== false
? undefined
: ts.factory.createToken(ts.SyntaxKind.QuestionToken);
const signature = ts.factory.createPropertySignature(
modifiers,
useLegacyResolution ||
property.name.match(validTypescriptIdentifierRegExp)
? property.name
: createStringLiteral({ text: property.name }),
questionToken,
type,
);

const type: ts.TypeNode | undefined = createTypeNode(property.type);
addLeadingComments({
comments: property.comment,
node: signature,
});

const signature = ts.factory.createPropertySignature(
modifiers,
property.name,
questionToken,
type,
);
return signature;
});

addLeadingComments({
comments: property.comment,
node: signature,
});
if (indexProperty) {
const modifiers: readonly ts.Modifier[] | undefined =
indexProperty.isReadOnly
? [ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)]
: undefined;
const indexSignature = ts.factory.createIndexSignature(
modifiers,
[
ts.factory.createParameterDeclaration(
undefined,
undefined,
createIdentifier({ text: indexProperty.name }),
undefined,
createKeywordTypeNode({
keyword: 'string',
}),
undefined,
),
],
createTypeNode(indexProperty.type),
);
members.push(indexSignature);
}

return signature;
}),
);
const node = ts.factory.createTypeLiteralNode(members);
return maybeNullable({ isNullable, node });
};

Expand Down Expand Up @@ -140,6 +194,7 @@ export const createTypeRecordNode = (
keys: (any | ts.TypeNode)[],
values: (any | ts.TypeNode)[],
isNullable: boolean = false,
useLegacyResolution: boolean = true,
) => {
const keyNode = createTypeUnionNode({
types: keys,
Expand All @@ -157,6 +212,7 @@ export const createTypeRecordNode = (
type: valueNode,
},
],
useLegacyResolution,
});
return maybeNullable({ isNullable, node });
};
Expand All @@ -168,11 +224,14 @@ export const createTypeRecordNode = (
* @returns ts.TypeReferenceNode | ts.UnionTypeNode
*/
export const createTypeArrayNode = (
types: (any | ts.TypeNode)[],
types: (any | ts.TypeNode)[] | ts.TypeNode | string,
isNullable: boolean = false,
) => {
const node = createTypeReferenceNode({
typeArguments: [createTypeUnionNode({ types })],
typeArguments: [
// @ts-ignore
Array.isArray(types) ? createTypeUnionNode({ types }) : types,
],
typeName: 'Array',
});
return maybeNullable({ isNullable, node });
Expand Down
Loading

0 comments on commit 4ff2429

Please sign in to comment.