zod-codegen 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +93 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +70 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +87 -0
- package/.github/dependabot.yml +76 -0
- package/.github/workflows/ci.yml +143 -0
- package/.github/workflows/release.yml +65 -0
- package/.husky/commit-msg +2 -0
- package/.husky/pre-commit +5 -0
- package/.lintstagedrc.json +4 -0
- package/.nvmrc +1 -0
- package/.prettierrc.json +7 -0
- package/.releaserc.json +159 -0
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +274 -0
- package/LICENCE +201 -0
- package/README.md +263 -0
- package/SECURITY.md +108 -0
- package/codecov.yml +29 -0
- package/commitlint.config.mjs +28 -0
- package/dist/scripts/update-manifest.js +31 -0
- package/dist/src/assets/manifest.json +5 -0
- package/dist/src/cli.js +60 -0
- package/dist/src/generator.js +55 -0
- package/dist/src/http/fetch-client.js +141 -0
- package/dist/src/interfaces/code-generator.js +1 -0
- package/dist/src/interfaces/file-reader.js +1 -0
- package/dist/src/polyfills/fetch.js +18 -0
- package/dist/src/services/code-generator.service.js +419 -0
- package/dist/src/services/file-reader.service.js +25 -0
- package/dist/src/services/file-writer.service.js +32 -0
- package/dist/src/services/import-builder.service.js +45 -0
- package/dist/src/services/type-builder.service.js +42 -0
- package/dist/src/types/http.js +10 -0
- package/dist/src/types/openapi.js +173 -0
- package/dist/src/utils/error-handler.js +11 -0
- package/dist/src/utils/execution-time.js +3 -0
- package/dist/src/utils/manifest.js +9 -0
- package/dist/src/utils/reporter.js +15 -0
- package/dist/src/utils/signal-handler.js +12 -0
- package/dist/src/utils/tty.js +3 -0
- package/dist/tests/integration/cli.test.js +25 -0
- package/dist/tests/unit/generator.test.js +29 -0
- package/dist/vitest.config.js +38 -0
- package/eslint.config.mjs +33 -0
- package/package.json +102 -0
- package/samples/openapi.json +1 -0
- package/samples/saris-openapi.json +7122 -0
- package/samples/swagger-petstore.yaml +802 -0
- package/samples/swagger-saris.yaml +3585 -0
- package/samples/test-logical.yaml +50 -0
- package/scripts/update-manifest.js +31 -0
- package/scripts/update-manifest.ts +47 -0
- package/src/assets/manifest.json +5 -0
- package/src/cli.ts +68 -0
- package/src/generator.ts +61 -0
- package/src/http/fetch-client.ts +181 -0
- package/src/interfaces/code-generator.ts +25 -0
- package/src/interfaces/file-reader.ts +15 -0
- package/src/polyfills/fetch.ts +26 -0
- package/src/services/code-generator.service.ts +775 -0
- package/src/services/file-reader.service.ts +29 -0
- package/src/services/file-writer.service.ts +36 -0
- package/src/services/import-builder.service.ts +64 -0
- package/src/services/type-builder.service.ts +77 -0
- package/src/types/http.ts +35 -0
- package/src/types/openapi.ts +202 -0
- package/src/utils/error-handler.ts +13 -0
- package/src/utils/execution-time.ts +3 -0
- package/src/utils/manifest.ts +17 -0
- package/src/utils/reporter.ts +16 -0
- package/src/utils/signal-handler.ts +14 -0
- package/src/utils/tty.ts +3 -0
- package/tests/integration/cli.test.ts +29 -0
- package/tests/unit/generator.test.ts +36 -0
- package/tsconfig.json +44 -0
- package/vitest.config.ts +39 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { HttpError } from '../types/http.js';
|
|
2
|
+
export class FetchHttpClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
defaultHeaders;
|
|
5
|
+
fetch;
|
|
6
|
+
Headers;
|
|
7
|
+
constructor(baseUrl = '', defaultHeaders = {}) {
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
this.defaultHeaders = defaultHeaders;
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
11
|
+
if (typeof globalThis.fetch === 'function' && globalThis.Headers) {
|
|
12
|
+
this.fetch = globalThis.fetch.bind(globalThis);
|
|
13
|
+
this.Headers = globalThis.Headers;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (typeof window !== 'undefined' && typeof window.fetch === 'function') {
|
|
17
|
+
this.fetch = window.fetch.bind(window);
|
|
18
|
+
this.Headers = window.Headers;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw new Error("Fetch API is not available. Please ensure you're running in a compatible environment or polyfill fetch.");
|
|
22
|
+
}
|
|
23
|
+
async request(config) {
|
|
24
|
+
const url = this.buildUrl(config.url, config.params);
|
|
25
|
+
const headers = this.buildHeaders(config.headers);
|
|
26
|
+
const body = this.buildBody(config.data, headers);
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timeoutId = config.timeout
|
|
29
|
+
? setTimeout(() => {
|
|
30
|
+
controller.abort();
|
|
31
|
+
}, config.timeout)
|
|
32
|
+
: undefined;
|
|
33
|
+
try {
|
|
34
|
+
const response = await this.fetch(url, {
|
|
35
|
+
method: config.method,
|
|
36
|
+
headers,
|
|
37
|
+
body,
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
});
|
|
40
|
+
const responseHeaders = this.extractHeaders(response.headers);
|
|
41
|
+
const data = await this.parseResponse(response);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new HttpError(`HTTP ${String(response.status)}: ${response.statusText}`, response.status, {
|
|
44
|
+
data,
|
|
45
|
+
status: response.status,
|
|
46
|
+
statusText: response.statusText,
|
|
47
|
+
headers: responseHeaders,
|
|
48
|
+
url: response.url,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
data,
|
|
53
|
+
status: response.status,
|
|
54
|
+
statusText: response.statusText,
|
|
55
|
+
headers: responseHeaders,
|
|
56
|
+
url: response.url,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof HttpError) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
if (error instanceof Error) {
|
|
64
|
+
if (error.name === 'AbortError') {
|
|
65
|
+
throw new HttpError('Request timeout', 408);
|
|
66
|
+
}
|
|
67
|
+
throw new HttpError(`Network error: ${error.message}`, 0);
|
|
68
|
+
}
|
|
69
|
+
throw new HttpError('Unknown network error', 0);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (timeoutId) {
|
|
73
|
+
clearTimeout(timeoutId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
buildUrl(path, params) {
|
|
78
|
+
const url = new URL(path, this.baseUrl || 'http://localhost');
|
|
79
|
+
if (params) {
|
|
80
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
82
|
+
if (value !== undefined && value !== null) {
|
|
83
|
+
url.searchParams.set(key, String(value));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return url.toString();
|
|
88
|
+
}
|
|
89
|
+
buildHeaders(customHeaders) {
|
|
90
|
+
const headers = new this.Headers();
|
|
91
|
+
Object.entries(this.defaultHeaders).forEach(([key, value]) => {
|
|
92
|
+
headers.set(key, value);
|
|
93
|
+
});
|
|
94
|
+
if (customHeaders) {
|
|
95
|
+
Object.entries(customHeaders).forEach(([key, value]) => {
|
|
96
|
+
headers.set(key, value);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return headers;
|
|
100
|
+
}
|
|
101
|
+
buildBody(data, headers) {
|
|
102
|
+
if (!data) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const contentType = headers?.get('content-type') ?? 'application/json';
|
|
106
|
+
if (contentType.includes('application/json')) {
|
|
107
|
+
if (!headers?.has('content-type')) {
|
|
108
|
+
headers?.set('content-type', 'application/json');
|
|
109
|
+
}
|
|
110
|
+
return JSON.stringify(data);
|
|
111
|
+
}
|
|
112
|
+
if (typeof data === 'string') {
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
return JSON.stringify(data);
|
|
116
|
+
}
|
|
117
|
+
extractHeaders(headers) {
|
|
118
|
+
const result = {};
|
|
119
|
+
headers.forEach((value, key) => {
|
|
120
|
+
result[key.toLowerCase()] = value;
|
|
121
|
+
});
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
async parseResponse(response) {
|
|
125
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
126
|
+
if (contentType.includes('application/json')) {
|
|
127
|
+
return (await response.json());
|
|
128
|
+
}
|
|
129
|
+
if (contentType.includes('text/')) {
|
|
130
|
+
return (await response.text());
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const text = await response.text();
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
135
|
+
return text ? JSON.parse(text) : {};
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function setupFetchPolyfill(options = {}) {
|
|
2
|
+
if (typeof fetch === 'function') {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
if (typeof window !== 'undefined' && typeof window.fetch === 'function') {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (options.enableNodejsPolyfill && typeof process !== 'undefined' && process.versions.node) {
|
|
9
|
+
try {
|
|
10
|
+
await import('undici');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Fall through to error
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
throw new Error('Fetch API is not available. ' + 'For Node.js environments, please install undici: npm install undici');
|
|
18
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import jp from 'jsonpath';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { MethodSchema, Reference, SchemaProperties } from '../types/openapi.js';
|
|
5
|
+
import { TypeScriptImportBuilderService } from './import-builder.service.js';
|
|
6
|
+
import { TypeScriptTypeBuilderService } from './type-builder.service.js';
|
|
7
|
+
export class TypeScriptCodeGeneratorService {
|
|
8
|
+
typeBuilder = new TypeScriptTypeBuilderService();
|
|
9
|
+
importBuilder = new TypeScriptImportBuilderService();
|
|
10
|
+
printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
11
|
+
ZodAST = z.object({
|
|
12
|
+
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'unknown', 'record']),
|
|
13
|
+
args: z.array(z.unknown()).optional(),
|
|
14
|
+
});
|
|
15
|
+
generate(spec) {
|
|
16
|
+
const file = ts.createSourceFile('generated.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
17
|
+
const nodes = this.buildAST(spec);
|
|
18
|
+
return this.printer.printList(ts.ListFormat.MultiLine, ts.factory.createNodeArray(nodes), file);
|
|
19
|
+
}
|
|
20
|
+
buildSchema(schema, required = true) {
|
|
21
|
+
const safeCategorySchema = SchemaProperties.safeParse(schema);
|
|
22
|
+
if (safeCategorySchema.success) {
|
|
23
|
+
const safeCategory = safeCategorySchema.data;
|
|
24
|
+
if (safeCategory.anyOf && Array.isArray(safeCategory.anyOf) && safeCategory.anyOf.length > 0) {
|
|
25
|
+
return this.handleLogicalOperator('anyOf', safeCategory.anyOf, required);
|
|
26
|
+
}
|
|
27
|
+
if (safeCategory.oneOf && Array.isArray(safeCategory.oneOf) && safeCategory.oneOf.length > 0) {
|
|
28
|
+
return this.handleLogicalOperator('oneOf', safeCategory.oneOf, required);
|
|
29
|
+
}
|
|
30
|
+
if (safeCategory.allOf && Array.isArray(safeCategory.allOf) && safeCategory.allOf.length > 0) {
|
|
31
|
+
return this.handleLogicalOperator('allOf', safeCategory.allOf, required);
|
|
32
|
+
}
|
|
33
|
+
if (safeCategory.not) {
|
|
34
|
+
return this.handleLogicalOperator('not', [safeCategory.not], required);
|
|
35
|
+
}
|
|
36
|
+
return this.buildProperty(safeCategory, required);
|
|
37
|
+
}
|
|
38
|
+
throw safeCategorySchema.error;
|
|
39
|
+
}
|
|
40
|
+
buildAST(openapi) {
|
|
41
|
+
const imports = this.importBuilder.buildImports();
|
|
42
|
+
const schemas = this.buildSchemas(openapi);
|
|
43
|
+
const clientClass = this.buildClientClass(openapi, schemas);
|
|
44
|
+
const baseUrlConstant = this.buildBaseUrlConstant(openapi);
|
|
45
|
+
return [
|
|
46
|
+
this.createComment('Imports'),
|
|
47
|
+
...imports,
|
|
48
|
+
this.createComment('Components schemas'),
|
|
49
|
+
...Object.values(schemas),
|
|
50
|
+
...baseUrlConstant,
|
|
51
|
+
this.createComment('Client class'),
|
|
52
|
+
clientClass,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
buildSchemas(openapi) {
|
|
56
|
+
const schemasEntries = Object.entries(openapi.components?.schemas ?? {});
|
|
57
|
+
const sortedSchemaNames = this.topologicalSort(Object.fromEntries(schemasEntries));
|
|
58
|
+
return sortedSchemaNames.reduce((schemaRegistered, name) => {
|
|
59
|
+
const schema = openapi.components?.schemas?.[name];
|
|
60
|
+
if (!schema)
|
|
61
|
+
return schemaRegistered;
|
|
62
|
+
const variableStatement = ts.factory.createVariableStatement([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
|
|
63
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(name)), undefined, undefined, this.buildSchema(schema)),
|
|
64
|
+
], ts.NodeFlags.Const));
|
|
65
|
+
return {
|
|
66
|
+
...schemaRegistered,
|
|
67
|
+
[name]: variableStatement,
|
|
68
|
+
};
|
|
69
|
+
}, {});
|
|
70
|
+
}
|
|
71
|
+
buildClientClass(openapi, schemas) {
|
|
72
|
+
const clientName = this.generateClientName(openapi.info.title);
|
|
73
|
+
const methods = this.buildClientMethods(openapi, schemas);
|
|
74
|
+
return ts.factory.createClassDeclaration([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(clientName), undefined, undefined, [
|
|
75
|
+
this.typeBuilder.createProperty('#baseUrl', 'string', true),
|
|
76
|
+
this.buildConstructor(),
|
|
77
|
+
this.buildHttpRequestMethod(),
|
|
78
|
+
...methods,
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
buildConstructor() {
|
|
82
|
+
return ts.factory.createConstructorDeclaration(undefined, [
|
|
83
|
+
this.typeBuilder.createParameter('baseUrl', 'string', ts.factory.createIdentifier('defaultBaseUrl')),
|
|
84
|
+
this.typeBuilder.createParameter('_', 'unknown', undefined, true),
|
|
85
|
+
], ts.factory.createBlock([
|
|
86
|
+
ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createPrivateIdentifier('#baseUrl')), ts.factory.createToken(ts.SyntaxKind.EqualsToken), ts.factory.createIdentifier('baseUrl'))),
|
|
87
|
+
], true));
|
|
88
|
+
}
|
|
89
|
+
buildHttpRequestMethod() {
|
|
90
|
+
return ts.factory.createMethodDeclaration([ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)], undefined, ts.factory.createPrivateIdentifier('#makeRequest'), undefined, [this.typeBuilder.createGenericType('T')], [
|
|
91
|
+
this.typeBuilder.createParameter('method', 'string'),
|
|
92
|
+
this.typeBuilder.createParameter('path', 'string'),
|
|
93
|
+
this.typeBuilder.createParameter('options', 'unknown', ts.factory.createObjectLiteralExpression([], false), true),
|
|
94
|
+
], ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
|
|
95
|
+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('T'), undefined),
|
|
96
|
+
]), ts.factory.createBlock([
|
|
97
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
98
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('url'), undefined, undefined, ts.factory.createTemplateExpression(ts.factory.createTemplateHead('', ''), [
|
|
99
|
+
ts.factory.createTemplateSpan(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createPrivateIdentifier('#baseUrl')), ts.factory.createTemplateMiddle('', '')),
|
|
100
|
+
ts.factory.createTemplateSpan(ts.factory.createIdentifier('path'), ts.factory.createTemplateTail('', '')),
|
|
101
|
+
])),
|
|
102
|
+
], ts.NodeFlags.Const)),
|
|
103
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
104
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('response'), undefined, undefined, ts.factory.createAwaitExpression(ts.factory.createCallExpression(ts.factory.createIdentifier('fetch'), undefined, [
|
|
105
|
+
ts.factory.createIdentifier('url'),
|
|
106
|
+
ts.factory.createObjectLiteralExpression([
|
|
107
|
+
ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier('method'), undefined),
|
|
108
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('headers'), ts.factory.createObjectLiteralExpression([
|
|
109
|
+
ts.factory.createPropertyAssignment(ts.factory.createStringLiteral('Content-Type', true), ts.factory.createStringLiteral('application/json', true)),
|
|
110
|
+
], true)),
|
|
111
|
+
], true),
|
|
112
|
+
]))),
|
|
113
|
+
], ts.NodeFlags.Const)),
|
|
114
|
+
ts.factory.createIfStatement(ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('response'), ts.factory.createIdentifier('ok'))), ts.factory.createThrowStatement(ts.factory.createNewExpression(ts.factory.createIdentifier('Error'), undefined, [
|
|
115
|
+
ts.factory.createTemplateExpression(ts.factory.createTemplateHead('HTTP ', 'HTTP '), [
|
|
116
|
+
ts.factory.createTemplateSpan(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('response'), ts.factory.createIdentifier('status')), ts.factory.createTemplateMiddle(': ', ': ')),
|
|
117
|
+
ts.factory.createTemplateSpan(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('response'), ts.factory.createIdentifier('statusText')), ts.factory.createTemplateTail('', '')),
|
|
118
|
+
]),
|
|
119
|
+
])), undefined),
|
|
120
|
+
ts.factory.createReturnStatement(ts.factory.createAwaitExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('response'), ts.factory.createIdentifier('json')), undefined, []))),
|
|
121
|
+
], true));
|
|
122
|
+
}
|
|
123
|
+
buildClientMethods(openapi, schemas) {
|
|
124
|
+
return Object.entries(openapi.paths).reduce((endpoints, [path, pathItem]) => {
|
|
125
|
+
const methods = Object.entries(pathItem)
|
|
126
|
+
.filter(([method]) => ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method))
|
|
127
|
+
.map(([method, methodSchema]) => {
|
|
128
|
+
const safeMethodSchema = MethodSchema.parse(methodSchema);
|
|
129
|
+
if (!safeMethodSchema.operationId) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return this.buildEndpointMethod(method, path, safeMethodSchema, schemas);
|
|
133
|
+
})
|
|
134
|
+
.filter((method) => method !== null);
|
|
135
|
+
return [...endpoints, ...methods];
|
|
136
|
+
}, []);
|
|
137
|
+
}
|
|
138
|
+
buildEndpointMethod(method, path, schema, schemas) {
|
|
139
|
+
const parameters = this.buildMethodParameters(schema, schemas);
|
|
140
|
+
const responseType = this.getResponseType(schema, schemas);
|
|
141
|
+
return ts.factory.createMethodDeclaration([ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)], undefined, ts.factory.createIdentifier(String(schema.operationId)), undefined, undefined, parameters, responseType, ts.factory.createBlock([
|
|
142
|
+
ts.factory.createReturnStatement(ts.factory.createAwaitExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createPrivateIdentifier('#makeRequest')), undefined, [
|
|
143
|
+
ts.factory.createStringLiteral(method.toUpperCase(), true),
|
|
144
|
+
ts.factory.createStringLiteral(path, true),
|
|
145
|
+
]))),
|
|
146
|
+
], true));
|
|
147
|
+
}
|
|
148
|
+
buildMethodParameters(schema, schemas) {
|
|
149
|
+
void schemas; // Mark as intentionally unused
|
|
150
|
+
const parameters = [];
|
|
151
|
+
if (schema.parameters) {
|
|
152
|
+
schema.parameters.forEach((param) => {
|
|
153
|
+
if (param.in === 'path' && param.required) {
|
|
154
|
+
parameters.push(this.typeBuilder.createParameter(this.typeBuilder.sanitizeIdentifier(param.name), 'string', undefined, false));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
parameters.push(this.typeBuilder.createParameter('_', 'unknown', undefined, true));
|
|
159
|
+
return parameters;
|
|
160
|
+
}
|
|
161
|
+
getResponseType(schema, schemas) {
|
|
162
|
+
void schemas; // Mark as intentionally unused
|
|
163
|
+
const response200 = schema.responses?.['200'];
|
|
164
|
+
if (!response200?.content?.['application/json']?.schema) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
|
|
168
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
buildBaseUrlConstant(openapi) {
|
|
172
|
+
const baseUrl = openapi.servers?.[0]?.url;
|
|
173
|
+
if (!baseUrl) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
return [
|
|
177
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
178
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('defaultBaseUrl'), undefined, undefined, ts.factory.createStringLiteral(baseUrl, true)),
|
|
179
|
+
], ts.NodeFlags.Const)),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
generateClientName(title) {
|
|
183
|
+
return title
|
|
184
|
+
.split(/[^a-zA-Z0-9]/g)
|
|
185
|
+
.map((word) => this.typeBuilder.toPascalCase(word))
|
|
186
|
+
.join('');
|
|
187
|
+
}
|
|
188
|
+
createComment(text) {
|
|
189
|
+
const commentNode = ts.factory.createIdentifier('\n');
|
|
190
|
+
ts.addSyntheticTrailingComment(commentNode, ts.SyntaxKind.SingleLineCommentTrivia, ` ${text}`, true);
|
|
191
|
+
return ts.factory.createExpressionStatement(commentNode);
|
|
192
|
+
}
|
|
193
|
+
buildZodAST(input) {
|
|
194
|
+
const [initial, ...rest] = input;
|
|
195
|
+
const safeInitial = this.ZodAST.safeParse(initial);
|
|
196
|
+
const initialExpression = !safeInitial.success
|
|
197
|
+
? ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier(this.ZodAST.shape.type.parse(initial))), undefined, [])
|
|
198
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier(safeInitial.data.type)), undefined, (safeInitial.data.args ?? []));
|
|
199
|
+
return rest.reduce((expression, exp) => {
|
|
200
|
+
const safeExp = this.ZodAST.safeParse(exp);
|
|
201
|
+
return !safeExp.success
|
|
202
|
+
? ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(expression, ts.factory.createIdentifier(typeof exp === 'string' ? exp : String(exp))), undefined, [])
|
|
203
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(expression, ts.factory.createIdentifier(safeExp.data.type)), undefined, (safeExp.data.args ?? []));
|
|
204
|
+
}, initialExpression);
|
|
205
|
+
}
|
|
206
|
+
buildProperty(property, required = false) {
|
|
207
|
+
const safeProperty = SchemaProperties.safeParse(property);
|
|
208
|
+
if (!safeProperty.success) {
|
|
209
|
+
return this.buildZodAST(['unknown']);
|
|
210
|
+
}
|
|
211
|
+
const prop = safeProperty.data;
|
|
212
|
+
if (this.isReference(prop)) {
|
|
213
|
+
return this.buildFromReference(prop);
|
|
214
|
+
}
|
|
215
|
+
const methodsToApply = [];
|
|
216
|
+
if (prop.anyOf && Array.isArray(prop.anyOf) && prop.anyOf.length > 0) {
|
|
217
|
+
return this.handleLogicalOperator('anyOf', prop.anyOf, required);
|
|
218
|
+
}
|
|
219
|
+
if (prop.oneOf && Array.isArray(prop.oneOf) && prop.oneOf.length > 0) {
|
|
220
|
+
return this.handleLogicalOperator('oneOf', prop.oneOf, required);
|
|
221
|
+
}
|
|
222
|
+
if (prop.allOf && Array.isArray(prop.allOf) && prop.allOf.length > 0) {
|
|
223
|
+
return this.handleLogicalOperator('allOf', prop.allOf, required);
|
|
224
|
+
}
|
|
225
|
+
if (prop.not) {
|
|
226
|
+
return this.handleLogicalOperator('not', [prop.not], required);
|
|
227
|
+
}
|
|
228
|
+
switch (prop.type) {
|
|
229
|
+
case 'array':
|
|
230
|
+
return this.buildZodAST([
|
|
231
|
+
{
|
|
232
|
+
type: 'array',
|
|
233
|
+
args: prop.items ? [this.buildProperty(prop.items, true)] : [],
|
|
234
|
+
},
|
|
235
|
+
...(!required ? ['optional'] : []),
|
|
236
|
+
]);
|
|
237
|
+
case 'object': {
|
|
238
|
+
const { properties = {}, required: propRequired = [] } = prop;
|
|
239
|
+
const propertiesEntries = Object.entries(properties);
|
|
240
|
+
if (propertiesEntries.length > 0) {
|
|
241
|
+
return this.buildZodAST([
|
|
242
|
+
{
|
|
243
|
+
type: 'object',
|
|
244
|
+
args: [
|
|
245
|
+
ts.factory.createObjectLiteralExpression(propertiesEntries.map(([name, propValue]) => {
|
|
246
|
+
return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(name), this.buildProperty(propValue, propRequired.includes(name)));
|
|
247
|
+
}), true),
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
...(!required ? ['optional'] : []),
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
return this.buildZodAST([
|
|
254
|
+
{
|
|
255
|
+
type: 'record',
|
|
256
|
+
args: [this.buildZodAST(['string']), this.buildZodAST(['unknown'])],
|
|
257
|
+
},
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
case 'integer':
|
|
261
|
+
methodsToApply.push('int');
|
|
262
|
+
return this.buildZodAST(['number', ...methodsToApply, ...(!required ? ['optional'] : [])]);
|
|
263
|
+
case 'number':
|
|
264
|
+
return this.buildZodAST(['number', ...(!required ? ['optional'] : [])]);
|
|
265
|
+
case 'string':
|
|
266
|
+
return this.buildZodAST(['string', ...(!required ? ['optional'] : [])]);
|
|
267
|
+
case 'boolean':
|
|
268
|
+
return this.buildZodAST(['boolean', ...(!required ? ['optional'] : [])]);
|
|
269
|
+
case 'unknown':
|
|
270
|
+
default:
|
|
271
|
+
return this.buildZodAST(['unknown', ...(!required ? ['optional'] : [])]);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
handleLogicalOperator(operator, schemas, required) {
|
|
275
|
+
const logicalExpression = this.buildLogicalOperator(operator, schemas);
|
|
276
|
+
return required
|
|
277
|
+
? logicalExpression
|
|
278
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(logicalExpression, ts.factory.createIdentifier('optional')), undefined, []);
|
|
279
|
+
}
|
|
280
|
+
buildLogicalOperator(operator, schemas) {
|
|
281
|
+
switch (operator) {
|
|
282
|
+
case 'anyOf':
|
|
283
|
+
case 'oneOf': {
|
|
284
|
+
const unionSchemas = schemas.map((schema) => this.buildSchemaFromLogicalOperator(schema));
|
|
285
|
+
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('union')), undefined, [ts.factory.createArrayLiteralExpression(unionSchemas, false)]);
|
|
286
|
+
}
|
|
287
|
+
case 'allOf': {
|
|
288
|
+
if (schemas.length === 0) {
|
|
289
|
+
throw new Error('allOf requires at least one schema');
|
|
290
|
+
}
|
|
291
|
+
const firstSchema = this.buildSchemaFromLogicalOperator(schemas[0]);
|
|
292
|
+
return schemas.slice(1).reduce((acc, schema) => {
|
|
293
|
+
const schemaExpression = this.buildSchemaFromLogicalOperator(schema);
|
|
294
|
+
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('intersection')), undefined, [acc, schemaExpression]);
|
|
295
|
+
}, firstSchema);
|
|
296
|
+
}
|
|
297
|
+
case 'not': {
|
|
298
|
+
const notSchema = this.buildSchemaFromLogicalOperator(schemas[0]);
|
|
299
|
+
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('any')), ts.factory.createIdentifier('refine')), undefined, [
|
|
300
|
+
ts.factory.createArrowFunction(undefined, undefined, [ts.factory.createParameterDeclaration(undefined, undefined, 'val', undefined, undefined, undefined)], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(notSchema, ts.factory.createIdentifier('safeParse')), undefined, [ts.factory.createIdentifier('val')]))),
|
|
301
|
+
ts.factory.createObjectLiteralExpression([
|
|
302
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('message'), ts.factory.createStringLiteral('Value must not match the excluded schema')),
|
|
303
|
+
]),
|
|
304
|
+
]);
|
|
305
|
+
}
|
|
306
|
+
default:
|
|
307
|
+
throw new Error(`Unsupported logical operator: ${String(operator)}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
buildSchemaFromLogicalOperator(schema) {
|
|
311
|
+
if (this.isReference(schema)) {
|
|
312
|
+
return this.buildFromReference(schema);
|
|
313
|
+
}
|
|
314
|
+
const safeSchema = SchemaProperties.safeParse(schema);
|
|
315
|
+
if (safeSchema.success) {
|
|
316
|
+
return this.buildProperty(safeSchema.data, true);
|
|
317
|
+
}
|
|
318
|
+
return this.buildBasicTypeFromSchema(schema);
|
|
319
|
+
}
|
|
320
|
+
buildBasicTypeFromSchema(schema) {
|
|
321
|
+
if (typeof schema === 'object' && schema !== null && 'type' in schema) {
|
|
322
|
+
const schemaObj = schema;
|
|
323
|
+
switch (schemaObj.type) {
|
|
324
|
+
case 'string':
|
|
325
|
+
return this.buildZodAST(['string']);
|
|
326
|
+
case 'number':
|
|
327
|
+
return this.buildZodAST(['number']);
|
|
328
|
+
case 'integer':
|
|
329
|
+
return this.buildZodAST(['number', 'int']);
|
|
330
|
+
case 'boolean':
|
|
331
|
+
return this.buildZodAST(['boolean']);
|
|
332
|
+
case 'object':
|
|
333
|
+
return this.buildObjectTypeFromSchema(schemaObj);
|
|
334
|
+
case 'array':
|
|
335
|
+
return this.buildArrayTypeFromSchema(schemaObj);
|
|
336
|
+
default:
|
|
337
|
+
return this.buildZodAST(['unknown']);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return this.buildZodAST(['unknown']);
|
|
341
|
+
}
|
|
342
|
+
buildObjectTypeFromSchema(schemaObj) {
|
|
343
|
+
const properties = Object.entries(schemaObj.properties ?? {});
|
|
344
|
+
if (properties.length > 0) {
|
|
345
|
+
return this.buildZodAST([
|
|
346
|
+
{
|
|
347
|
+
type: 'object',
|
|
348
|
+
args: [
|
|
349
|
+
ts.factory.createObjectLiteralExpression(properties.map(([name, property]) => {
|
|
350
|
+
return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(name), this.buildSchemaFromLogicalOperator(property));
|
|
351
|
+
}), true),
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
]);
|
|
355
|
+
}
|
|
356
|
+
return this.buildZodAST([
|
|
357
|
+
{
|
|
358
|
+
type: 'record',
|
|
359
|
+
args: [this.buildZodAST(['string']), this.buildZodAST(['unknown'])],
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
}
|
|
363
|
+
buildArrayTypeFromSchema(schemaObj) {
|
|
364
|
+
if (schemaObj.items) {
|
|
365
|
+
return this.buildZodAST([
|
|
366
|
+
{
|
|
367
|
+
type: 'array',
|
|
368
|
+
args: [this.buildSchemaFromLogicalOperator(schemaObj.items)],
|
|
369
|
+
},
|
|
370
|
+
]);
|
|
371
|
+
}
|
|
372
|
+
return this.buildZodAST(['array']);
|
|
373
|
+
}
|
|
374
|
+
isReference(reference) {
|
|
375
|
+
if (typeof reference === 'object' && reference !== null && '$ref' in reference) {
|
|
376
|
+
const ref = reference;
|
|
377
|
+
return typeof ref.$ref === 'string' && ref.$ref.length > 0;
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
buildFromReference(reference) {
|
|
382
|
+
const { $ref = '' } = Reference.parse(reference);
|
|
383
|
+
const refName = $ref.split('/').pop() ?? 'never';
|
|
384
|
+
return ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(refName));
|
|
385
|
+
}
|
|
386
|
+
topologicalSort(schemas) {
|
|
387
|
+
const visited = new Set();
|
|
388
|
+
const visiting = new Set();
|
|
389
|
+
const result = [];
|
|
390
|
+
const visit = (name) => {
|
|
391
|
+
if (visiting.has(name)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (visited.has(name)) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
visiting.add(name);
|
|
398
|
+
const schema = schemas[name];
|
|
399
|
+
const dependencies = jp
|
|
400
|
+
.query(schema, '$..["$ref"]')
|
|
401
|
+
.filter((ref) => ref.startsWith('#/components/schemas/'))
|
|
402
|
+
.map((ref) => ref.replace('#/components/schemas/', ''));
|
|
403
|
+
for (const dep of dependencies) {
|
|
404
|
+
if (schemas[dep]) {
|
|
405
|
+
visit(dep);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
visiting.delete(name);
|
|
409
|
+
visited.add(name);
|
|
410
|
+
result.push(name);
|
|
411
|
+
};
|
|
412
|
+
for (const name of Object.keys(schemas)) {
|
|
413
|
+
if (!visited.has(name)) {
|
|
414
|
+
visit(name);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { load } from 'js-yaml';
|
|
3
|
+
import { OpenApiSpec } from '../types/openapi.js';
|
|
4
|
+
export class SyncFileReaderService {
|
|
5
|
+
readFile(path) {
|
|
6
|
+
return readFileSync(path, 'utf8');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class OpenApiFileParserService {
|
|
10
|
+
parse(input) {
|
|
11
|
+
let parsedInput;
|
|
12
|
+
if (typeof input === 'string') {
|
|
13
|
+
try {
|
|
14
|
+
parsedInput = JSON.parse(input);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
parsedInput = load(input);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
parsedInput = input;
|
|
22
|
+
}
|
|
23
|
+
return OpenApiSpec.parse(parsedInput);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
export class SyncFileWriterService {
|
|
4
|
+
name;
|
|
5
|
+
version;
|
|
6
|
+
inputPath;
|
|
7
|
+
constructor(name, version, inputPath) {
|
|
8
|
+
this.name = name;
|
|
9
|
+
this.version = version;
|
|
10
|
+
this.inputPath = inputPath;
|
|
11
|
+
}
|
|
12
|
+
writeFile(filePath, content) {
|
|
13
|
+
const generatedContent = [
|
|
14
|
+
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
|
|
15
|
+
`// Built with ${this.name}@${this.version}`,
|
|
16
|
+
`// Latest edit: ${new Date().toUTCString()}`,
|
|
17
|
+
`// Source file: ${this.inputPath}`,
|
|
18
|
+
'/* eslint-disable */',
|
|
19
|
+
'// @ts-nocheck',
|
|
20
|
+
'',
|
|
21
|
+
content,
|
|
22
|
+
].join('\n');
|
|
23
|
+
const dirPath = dirname(filePath);
|
|
24
|
+
if (!existsSync(dirPath)) {
|
|
25
|
+
mkdirSync(dirPath, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
writeFileSync(filePath, generatedContent);
|
|
28
|
+
}
|
|
29
|
+
resolveOutputPath(outputDir, fileName = 'type.ts') {
|
|
30
|
+
return resolve(outputDir, fileName);
|
|
31
|
+
}
|
|
32
|
+
}
|