zod-codegen 1.6.2 → 1.7.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/workflows/ci.yml +50 -48
- package/.github/workflows/release.yml +13 -3
- package/.husky/commit-msg +1 -1
- package/.husky/pre-commit +1 -1
- package/.lintstagedrc.json +5 -1
- package/.nvmrc +1 -1
- package/.prettierrc.json +12 -5
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +12 -12
- package/EXAMPLES.md +135 -57
- package/PERFORMANCE.md +4 -4
- package/README.md +87 -64
- package/SECURITY.md +1 -1
- package/dist/src/cli.js +11 -18
- package/dist/src/generator.d.ts +2 -2
- package/dist/src/generator.d.ts.map +1 -1
- package/dist/src/generator.js +5 -3
- package/dist/src/interfaces/code-generator.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.d.ts +24 -1
- package/dist/src/services/code-generator.service.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.js +385 -216
- package/dist/src/services/file-reader.service.d.ts.map +1 -1
- package/dist/src/services/file-reader.service.js +1 -1
- package/dist/src/services/file-writer.service.d.ts.map +1 -1
- package/dist/src/services/file-writer.service.js +2 -2
- package/dist/src/services/import-builder.service.d.ts.map +1 -1
- package/dist/src/services/import-builder.service.js +3 -3
- package/dist/src/services/type-builder.service.d.ts.map +1 -1
- package/dist/src/types/generator-options.d.ts.map +1 -1
- package/dist/src/types/openapi.d.ts.map +1 -1
- package/dist/src/types/openapi.js +20 -20
- package/dist/src/utils/error-handler.d.ts.map +1 -1
- package/dist/src/utils/naming-convention.d.ts.map +1 -1
- package/dist/src/utils/naming-convention.js +6 -3
- package/dist/src/utils/signal-handler.d.ts.map +1 -1
- package/dist/tests/integration/cli-comprehensive.test.d.ts +2 -0
- package/dist/tests/integration/cli-comprehensive.test.d.ts.map +1 -0
- package/dist/tests/integration/cli-comprehensive.test.js +110 -0
- package/dist/tests/integration/cli.test.d.ts +2 -0
- package/dist/tests/integration/cli.test.d.ts.map +1 -0
- package/dist/tests/integration/cli.test.js +25 -0
- package/dist/tests/integration/error-scenarios.test.d.ts +2 -0
- package/dist/tests/integration/error-scenarios.test.d.ts.map +1 -0
- package/dist/tests/integration/error-scenarios.test.js +169 -0
- package/dist/tests/integration/snapshots.test.d.ts +2 -0
- package/dist/tests/integration/snapshots.test.d.ts.map +1 -0
- package/dist/tests/integration/snapshots.test.js +100 -0
- package/dist/tests/unit/code-generator-edge-cases.test.d.ts +2 -0
- package/dist/tests/unit/code-generator-edge-cases.test.d.ts.map +1 -0
- package/dist/tests/unit/code-generator-edge-cases.test.js +506 -0
- package/dist/tests/unit/code-generator.test.d.ts +2 -0
- package/dist/tests/unit/code-generator.test.d.ts.map +1 -0
- package/dist/tests/unit/code-generator.test.js +1364 -0
- package/dist/tests/unit/file-reader.test.d.ts +2 -0
- package/dist/tests/unit/file-reader.test.d.ts.map +1 -0
- package/dist/tests/unit/file-reader.test.js +125 -0
- package/dist/tests/unit/generator.test.d.ts +2 -0
- package/dist/tests/unit/generator.test.d.ts.map +1 -0
- package/dist/tests/unit/generator.test.js +119 -0
- package/dist/tests/unit/naming-convention.test.d.ts +2 -0
- package/dist/tests/unit/naming-convention.test.d.ts.map +1 -0
- package/dist/tests/unit/naming-convention.test.js +256 -0
- package/dist/tests/unit/reporter.test.d.ts +2 -0
- package/dist/tests/unit/reporter.test.d.ts.map +1 -0
- package/dist/tests/unit/reporter.test.js +44 -0
- package/dist/tests/unit/type-builder.test.d.ts +2 -0
- package/dist/tests/unit/type-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/type-builder.test.js +108 -0
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +10 -20
- package/eslint.config.mjs +38 -28
- package/examples/.gitkeep +1 -1
- package/examples/README.md +4 -2
- package/examples/petstore/README.md +18 -17
- package/examples/petstore/{type.ts → api.ts} +158 -74
- package/examples/petstore/authenticated-usage.ts +6 -4
- package/examples/petstore/basic-usage.ts +4 -3
- package/examples/petstore/error-handling-usage.ts +84 -0
- package/examples/petstore/retry-handler-usage.ts +11 -18
- package/examples/petstore/server-variables-usage.ts +10 -10
- package/examples/pokeapi/README.md +8 -8
- package/examples/pokeapi/api.ts +218 -0
- package/examples/pokeapi/basic-usage.ts +3 -2
- package/examples/pokeapi/custom-client.ts +5 -4
- package/package.json +17 -21
- package/src/cli.ts +20 -25
- package/src/generator.ts +13 -11
- package/src/interfaces/code-generator.ts +1 -1
- package/src/services/code-generator.service.ts +989 -1099
- package/src/services/file-reader.service.ts +6 -5
- package/src/services/file-writer.service.ts +7 -7
- package/src/services/import-builder.service.ts +9 -13
- package/src/services/type-builder.service.ts +8 -19
- package/src/types/generator-options.ts +1 -1
- package/src/types/openapi.ts +22 -22
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/naming-convention.ts +13 -10
- package/src/utils/reporter.ts +2 -2
- package/src/utils/signal-handler.ts +7 -8
- package/tests/integration/cli-comprehensive.test.ts +38 -32
- package/tests/integration/cli.test.ts +5 -5
- package/tests/integration/error-scenarios.test.ts +20 -26
- package/tests/integration/snapshots.test.ts +19 -23
- package/tests/unit/code-generator-edge-cases.test.ts +133 -133
- package/tests/unit/code-generator.test.ts +674 -268
- package/tests/unit/file-reader.test.ts +14 -14
- package/tests/unit/generator.test.ts +30 -18
- package/tests/unit/naming-convention.test.ts +27 -27
- package/tests/unit/type-builder.test.ts +2 -2
- package/tsconfig.json +5 -3
- package/vitest.config.ts +11 -21
- package/dist/scripts/update-manifest.d.ts +0 -14
- package/dist/scripts/update-manifest.d.ts.map +0 -1
- package/dist/scripts/update-manifest.js +0 -33
- package/dist/src/assets/manifest.json +0 -5
- package/examples/pokeapi/type.ts +0 -109
- package/generated/type.ts +0 -326
- package/scripts/update-manifest.ts +0 -49
- package/src/assets/manifest.json +0 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type {OpenApiFileParser, OpenApiFileReader} from '../interfaces/file-reader';
|
|
4
|
-
import type {OpenApiSpecType} from '../types/openapi';
|
|
5
|
-
import {OpenApiSpec} from '../types/openapi';
|
|
1
|
+
import { load } from 'js-yaml';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import type { OpenApiFileParser, OpenApiFileReader } from '../interfaces/file-reader';
|
|
4
|
+
import type { OpenApiSpecType } from '../types/openapi';
|
|
5
|
+
import { OpenApiSpec } from '../types/openapi';
|
|
6
6
|
|
|
7
7
|
export class SyncFileReaderService implements OpenApiFileReader {
|
|
8
8
|
async readFile(path: string): Promise<string> {
|
|
@@ -14,6 +14,7 @@ export class SyncFileReaderService implements OpenApiFileReader {
|
|
|
14
14
|
if (!response.ok) {
|
|
15
15
|
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
16
16
|
}
|
|
17
|
+
|
|
17
18
|
return await response.text();
|
|
18
19
|
} catch {
|
|
19
20
|
// If URL parsing fails, treat it as a local file path
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {existsSync, mkdirSync, writeFileSync} from 'node:fs';
|
|
2
|
-
import {dirname, resolve} from 'node:path';
|
|
3
|
-
import type {FileWriter} from '../interfaces/code-generator';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import type { FileWriter } from '../interfaces/code-generator';
|
|
4
4
|
|
|
5
5
|
export class SyncFileWriterService implements FileWriter {
|
|
6
6
|
constructor(
|
|
7
7
|
private readonly name: string,
|
|
8
8
|
private readonly version: string,
|
|
9
|
-
private readonly inputPath: string
|
|
9
|
+
private readonly inputPath: string
|
|
10
10
|
) {}
|
|
11
11
|
|
|
12
12
|
writeFile(filePath: string, content: string): void {
|
|
@@ -18,19 +18,19 @@ export class SyncFileWriterService implements FileWriter {
|
|
|
18
18
|
'/* eslint-disable */',
|
|
19
19
|
'// @ts-nocheck',
|
|
20
20
|
'',
|
|
21
|
-
content
|
|
21
|
+
content
|
|
22
22
|
].join('\n');
|
|
23
23
|
|
|
24
24
|
const dirPath = dirname(filePath);
|
|
25
25
|
|
|
26
26
|
if (!existsSync(dirPath)) {
|
|
27
|
-
mkdirSync(dirPath, {recursive: true});
|
|
27
|
+
mkdirSync(dirPath, { recursive: true });
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
writeFileSync(filePath, generatedContent);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
resolveOutputPath(outputDir: string, fileName = '
|
|
33
|
+
resolveOutputPath(outputDir: string, fileName = 'api.ts'): string {
|
|
34
34
|
return resolve(outputDir, fileName);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import {z} from 'zod';
|
|
3
|
-
import type {ImportBuilder} from '../interfaces/code-generator';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ImportBuilder } from '../interfaces/code-generator';
|
|
4
4
|
|
|
5
5
|
const IsTypeImport = z.boolean();
|
|
6
6
|
const ImportedElement = z.record(z.string(), IsTypeImport);
|
|
7
7
|
|
|
8
8
|
const ImportOptions = z.object({
|
|
9
9
|
defaultImport: ImportedElement.optional(),
|
|
10
|
-
namedImports: ImportedElement.optional()
|
|
10
|
+
namedImports: ImportedElement.optional()
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
type ImportOptionsType = z.infer<typeof ImportOptions>;
|
|
@@ -16,15 +16,15 @@ export class TypeScriptImportBuilderService implements ImportBuilder {
|
|
|
16
16
|
buildImports(): ts.ImportDeclaration[] {
|
|
17
17
|
return [
|
|
18
18
|
this.createImport('zod', {
|
|
19
|
-
namedImports: {z: false}
|
|
20
|
-
})
|
|
19
|
+
namedImports: { z: false }
|
|
20
|
+
})
|
|
21
21
|
];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
createImport(target: string, options: ImportOptionsType): ts.ImportDeclaration {
|
|
25
25
|
const safeOptions = ImportOptions.parse(options);
|
|
26
26
|
const [defaultImport] = Object.entries(safeOptions.defaultImport ?? {})[0] ?? [undefined, false];
|
|
27
|
-
const {success: hasDefaultImport} = z.string().safeParse(defaultImport);
|
|
27
|
+
const { success: hasDefaultImport } = z.string().safeParse(defaultImport);
|
|
28
28
|
|
|
29
29
|
const safeNameImports = ImportedElement.safeParse(safeOptions.namedImports);
|
|
30
30
|
const namedImportList = safeNameImports.success ? Object.entries(safeNameImports.data) : [];
|
|
@@ -35,7 +35,7 @@ export class TypeScriptImportBuilderService implements ImportBuilder {
|
|
|
35
35
|
? ts.factory.createNamedImports(
|
|
36
36
|
namedImportList.map(([name, isTypeImport]) => {
|
|
37
37
|
return ts.factory.createImportSpecifier(isTypeImport, undefined, ts.factory.createIdentifier(name));
|
|
38
|
-
})
|
|
38
|
+
})
|
|
39
39
|
)
|
|
40
40
|
: undefined;
|
|
41
41
|
|
|
@@ -48,14 +48,10 @@ export class TypeScriptImportBuilderService implements ImportBuilder {
|
|
|
48
48
|
undefined,
|
|
49
49
|
hasAnyImports
|
|
50
50
|
? // eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
51
|
-
ts.factory.createImportClause(
|
|
52
|
-
false,
|
|
53
|
-
hasDefaultImport && defaultImport ? ts.factory.createIdentifier(defaultImport) : undefined,
|
|
54
|
-
namedImports ?? undefined,
|
|
55
|
-
)
|
|
51
|
+
ts.factory.createImportClause(false, hasDefaultImport && defaultImport ? ts.factory.createIdentifier(defaultImport) : undefined, namedImports ?? undefined)
|
|
56
52
|
: undefined,
|
|
57
53
|
ts.factory.createStringLiteral(target, true),
|
|
58
|
-
undefined
|
|
54
|
+
undefined
|
|
59
55
|
);
|
|
60
56
|
}
|
|
61
57
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import type {TypeBuilder} from '../interfaces/code-generator';
|
|
2
|
+
import type { TypeBuilder } from '../interfaces/code-generator';
|
|
3
3
|
|
|
4
4
|
export class TypeScriptTypeBuilderService implements TypeBuilder {
|
|
5
5
|
buildType(type: string): ts.TypeNode {
|
|
@@ -18,16 +18,15 @@ export class TypeScriptTypeBuilderService implements TypeBuilder {
|
|
|
18
18
|
const itemType = type.slice(0, -2);
|
|
19
19
|
return ts.factory.createArrayTypeNode(this.buildType(itemType));
|
|
20
20
|
}
|
|
21
|
+
|
|
21
22
|
// Handle Record types
|
|
22
23
|
if (type.startsWith('Record<')) {
|
|
23
24
|
// For now, return unknown for complex Record types
|
|
24
25
|
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
25
26
|
}
|
|
27
|
+
|
|
26
28
|
// Custom type name - create a type reference
|
|
27
|
-
return ts.factory.createTypeReferenceNode(
|
|
28
|
-
ts.factory.createIdentifier(this.sanitizeIdentifier(type)),
|
|
29
|
-
undefined,
|
|
30
|
-
);
|
|
29
|
+
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(this.sanitizeIdentifier(type)), undefined);
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
|
|
@@ -39,33 +38,23 @@ export class TypeScriptTypeBuilderService implements TypeBuilder {
|
|
|
39
38
|
ts.factory[createIdentifier](name),
|
|
40
39
|
undefined,
|
|
41
40
|
this.buildType(type),
|
|
42
|
-
undefined
|
|
41
|
+
undefined
|
|
43
42
|
);
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
createParameter(
|
|
47
|
-
name: string,
|
|
48
|
-
type?: string | ts.TypeNode,
|
|
49
|
-
defaultValue?: ts.Expression,
|
|
50
|
-
isOptional = false,
|
|
51
|
-
): ts.ParameterDeclaration {
|
|
45
|
+
createParameter(name: string, type?: string | ts.TypeNode, defaultValue?: ts.Expression, isOptional = false): ts.ParameterDeclaration {
|
|
52
46
|
return ts.factory.createParameterDeclaration(
|
|
53
47
|
undefined,
|
|
54
48
|
undefined,
|
|
55
49
|
ts.factory.createIdentifier(this.sanitizeIdentifier(name)),
|
|
56
50
|
isOptional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
|
|
57
51
|
typeof type === 'string' ? this.buildType(type) : type,
|
|
58
|
-
defaultValue
|
|
52
|
+
defaultValue
|
|
59
53
|
);
|
|
60
54
|
}
|
|
61
55
|
|
|
62
56
|
createGenericType(name: string): ts.TypeParameterDeclaration {
|
|
63
|
-
return ts.factory.createTypeParameterDeclaration(
|
|
64
|
-
undefined,
|
|
65
|
-
ts.factory.createIdentifier(name),
|
|
66
|
-
undefined,
|
|
67
|
-
undefined,
|
|
68
|
-
);
|
|
57
|
+
return ts.factory.createTypeParameterDeclaration(undefined, ts.factory.createIdentifier(name), undefined, undefined);
|
|
69
58
|
}
|
|
70
59
|
|
|
71
60
|
sanitizeIdentifier(name: string): string {
|
package/src/types/openapi.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {z} from 'zod';
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
|
|
3
3
|
export const Reference = z.object({
|
|
4
|
-
$ref: z.string().optional()
|
|
4
|
+
$ref: z.string().optional()
|
|
5
5
|
});
|
|
6
6
|
|
|
7
7
|
const BaseSchemaProperties = z.object({
|
|
@@ -38,32 +38,32 @@ const BaseSchemaProperties = z.object({
|
|
|
38
38
|
xml: z
|
|
39
39
|
.object({
|
|
40
40
|
name: z.string().optional(),
|
|
41
|
-
wrapped: z.boolean().optional()
|
|
41
|
+
wrapped: z.boolean().optional()
|
|
42
42
|
})
|
|
43
43
|
.optional(),
|
|
44
44
|
externalDocs: Reference.optional(),
|
|
45
45
|
example: z.unknown().optional(),
|
|
46
|
-
deprecated: z.boolean().optional()
|
|
46
|
+
deprecated: z.boolean().optional()
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
50
|
export const SchemaProperties: z.ZodLazy<z.ZodObject<any>> = z.lazy(() =>
|
|
51
51
|
BaseSchemaProperties.extend({
|
|
52
52
|
properties: z.record(z.string(), SchemaProperties).optional(),
|
|
53
|
-
items: SchemaProperties.optional()
|
|
54
|
-
})
|
|
53
|
+
items: SchemaProperties.optional()
|
|
54
|
+
})
|
|
55
55
|
);
|
|
56
56
|
|
|
57
57
|
const ServerVariable = z.object({
|
|
58
58
|
default: z.string(),
|
|
59
59
|
description: z.string().optional(),
|
|
60
|
-
enum: z.array(z.string()).optional()
|
|
60
|
+
enum: z.array(z.string()).optional()
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
const Server = z.object({
|
|
64
64
|
url: z.string(), // Allow templated URLs with {variables}
|
|
65
65
|
description: z.string().optional(),
|
|
66
|
-
variables: z.record(z.string(), ServerVariable).optional()
|
|
66
|
+
variables: z.record(z.string(), ServerVariable).optional()
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
export const Parameter = z.object({
|
|
@@ -77,7 +77,7 @@ export const Parameter = z.object({
|
|
|
77
77
|
style: z.string().optional(),
|
|
78
78
|
explode: z.boolean().optional(),
|
|
79
79
|
allowReserved: z.boolean().optional(),
|
|
80
|
-
schema: SchemaProperties.optional()
|
|
80
|
+
schema: SchemaProperties.optional()
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
const ResponseHeader = z.object({
|
|
@@ -89,25 +89,25 @@ const ResponseHeader = z.object({
|
|
|
89
89
|
style: z.string().optional(),
|
|
90
90
|
explode: z.boolean().optional(),
|
|
91
91
|
allowReserved: z.boolean().optional(),
|
|
92
|
-
schema: Reference.optional()
|
|
92
|
+
schema: Reference.optional()
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
const MediaType = z.object({
|
|
96
|
-
schema: z.unknown().optional()
|
|
96
|
+
schema: z.unknown().optional()
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
export const Response = z.object({
|
|
100
100
|
$ref: z.string().optional(),
|
|
101
101
|
description: z.string(),
|
|
102
102
|
headers: z.record(z.string(), ResponseHeader).optional(),
|
|
103
|
-
content: z.record(z.string(), MediaType).optional()
|
|
103
|
+
content: z.record(z.string(), MediaType).optional()
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
export const RequestBody = z.object({
|
|
107
107
|
$ref: z.string().optional(),
|
|
108
108
|
description: z.string().optional(),
|
|
109
109
|
required: z.boolean().optional(),
|
|
110
|
-
content: z.record(z.string(), MediaType).optional()
|
|
110
|
+
content: z.record(z.string(), MediaType).optional()
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
export const MethodSchema = z.object({
|
|
@@ -118,7 +118,7 @@ export const MethodSchema = z.object({
|
|
|
118
118
|
requestBody: RequestBody.optional(),
|
|
119
119
|
responses: z.record(z.string(), Response).optional(),
|
|
120
120
|
tags: z.array(z.string()).optional(),
|
|
121
|
-
deprecated: z.boolean().optional()
|
|
121
|
+
deprecated: z.boolean().optional()
|
|
122
122
|
});
|
|
123
123
|
|
|
124
124
|
export const PathItem = z.object({
|
|
@@ -133,7 +133,7 @@ export const PathItem = z.object({
|
|
|
133
133
|
head: MethodSchema.optional(),
|
|
134
134
|
options: MethodSchema.optional(),
|
|
135
135
|
trace: MethodSchema.optional(),
|
|
136
|
-
parameters: z.array(Parameter).optional()
|
|
136
|
+
parameters: z.array(Parameter).optional()
|
|
137
137
|
});
|
|
138
138
|
|
|
139
139
|
const Info = z.object({
|
|
@@ -145,15 +145,15 @@ const Info = z.object({
|
|
|
145
145
|
.object({
|
|
146
146
|
name: z.string().optional(),
|
|
147
147
|
email: z.email().optional(),
|
|
148
|
-
url: z.url().optional()
|
|
148
|
+
url: z.url().optional()
|
|
149
149
|
})
|
|
150
150
|
.optional(),
|
|
151
151
|
license: z
|
|
152
152
|
.object({
|
|
153
153
|
name: z.string().min(1),
|
|
154
|
-
url: z.url().optional()
|
|
154
|
+
url: z.url().optional()
|
|
155
155
|
})
|
|
156
|
-
.optional()
|
|
156
|
+
.optional()
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
const SecurityRequirement = z.record(z.string(), z.array(z.string()));
|
|
@@ -161,12 +161,12 @@ const SecurityRequirement = z.record(z.string(), z.array(z.string()));
|
|
|
161
161
|
const Tag = z.object({
|
|
162
162
|
name: z.string().min(1),
|
|
163
163
|
description: z.string().optional(),
|
|
164
|
-
externalDocs: Reference.optional()
|
|
164
|
+
externalDocs: Reference.optional()
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
const ExternalDocumentation = z.object({
|
|
168
168
|
description: z.string().optional(),
|
|
169
|
-
url: z.url()
|
|
169
|
+
url: z.url()
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
const Components = z.object({
|
|
@@ -178,7 +178,7 @@ const Components = z.object({
|
|
|
178
178
|
headers: z.record(z.string(), ResponseHeader).optional(),
|
|
179
179
|
securitySchemes: z.record(z.string(), Reference).optional(),
|
|
180
180
|
links: z.record(z.string(), Reference).optional(),
|
|
181
|
-
callbacks: z.record(z.string(), Reference).optional()
|
|
181
|
+
callbacks: z.record(z.string(), Reference).optional()
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
export const OpenApiSpec = z.object({
|
|
@@ -189,7 +189,7 @@ export const OpenApiSpec = z.object({
|
|
|
189
189
|
components: Components.optional(),
|
|
190
190
|
security: z.array(SecurityRequirement).optional(),
|
|
191
191
|
tags: z.array(Tag).optional(),
|
|
192
|
-
externalDocs: ExternalDocumentation.optional()
|
|
192
|
+
externalDocs: ExternalDocumentation.optional()
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
export type OpenApiSpecType = z.infer<typeof OpenApiSpec>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {getExecutionTime} from './execution-time';
|
|
2
|
-
import type {Reporter} from './reporter';
|
|
1
|
+
import { getExecutionTime } from './execution-time';
|
|
2
|
+
import type { Reporter } from './reporter';
|
|
3
3
|
|
|
4
4
|
export const errorReceived = (process: NodeJS.Process, startTime: bigint, reporter: Reporter) => (): void => {
|
|
5
5
|
reporter.log(`Done after ${String(getExecutionTime(startTime))}s`);
|
|
@@ -8,13 +8,7 @@
|
|
|
8
8
|
* transformNamingConvention('getUserById', 'snake_case') // 'get_user_by_id'
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
11
|
-
export type NamingConvention =
|
|
12
|
-
| 'camelCase'
|
|
13
|
-
| 'PascalCase'
|
|
14
|
-
| 'snake_case'
|
|
15
|
-
| 'kebab-case'
|
|
16
|
-
| 'SCREAMING_SNAKE_CASE'
|
|
17
|
-
| 'SCREAMING-KEBAB-CASE';
|
|
11
|
+
export type NamingConvention = 'camelCase' | 'PascalCase' | 'snake_case' | 'kebab-case' | 'SCREAMING_SNAKE_CASE' | 'SCREAMING-KEBAB-CASE';
|
|
18
12
|
|
|
19
13
|
/**
|
|
20
14
|
* Operation details provided to custom transformers.
|
|
@@ -66,7 +60,10 @@ export type OperationNameTransformer = (details: OperationDetails) => string;
|
|
|
66
60
|
* Capitalizes the first letter of a word
|
|
67
61
|
*/
|
|
68
62
|
function capitalize(word: string): string {
|
|
69
|
-
if (word.length === 0)
|
|
63
|
+
if (word.length === 0) {
|
|
64
|
+
return word;
|
|
65
|
+
}
|
|
66
|
+
|
|
70
67
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
71
68
|
}
|
|
72
69
|
|
|
@@ -126,9 +123,15 @@ function normalizeToWords(input: string): string[] {
|
|
|
126
123
|
* Converts words array to camelCase
|
|
127
124
|
*/
|
|
128
125
|
function toCamelCase(words: string[]): string {
|
|
129
|
-
if (words.length === 0)
|
|
126
|
+
if (words.length === 0) {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
130
|
const [first, ...rest] = words;
|
|
131
|
-
if (!first)
|
|
131
|
+
if (!first) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
|
|
132
135
|
return first + rest.map((w) => capitalize(w)).join('');
|
|
133
136
|
}
|
|
134
137
|
|
package/src/utils/reporter.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {format} from 'node:util';
|
|
1
|
+
import { format } from 'node:util';
|
|
2
2
|
|
|
3
3
|
export class Reporter {
|
|
4
4
|
constructor(
|
|
5
5
|
private readonly stdout: NodeJS.WriteStream,
|
|
6
|
-
private readonly stderr: NodeJS.WriteStream = stdout
|
|
6
|
+
private readonly stderr: NodeJS.WriteStream = stdout
|
|
7
7
|
) {
|
|
8
8
|
this.log = this.log.bind(this);
|
|
9
9
|
this.error = this.error.bind(this);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {getExecutionTime} from './execution-time';
|
|
2
|
-
import type {Reporter} from './reporter';
|
|
1
|
+
import { getExecutionTime } from './execution-time';
|
|
2
|
+
import type { Reporter } from './reporter';
|
|
3
3
|
|
|
4
|
-
export const signalReceived =
|
|
5
|
-
(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
4
|
+
export const signalReceived = (process: NodeJS.Process, startTime: bigint, event: NodeJS.Signals, reporter: Reporter) => (): void => {
|
|
5
|
+
reporter.log(`Done after ${String(getExecutionTime(startTime))}s`);
|
|
6
|
+
process.kill(process.pid, event);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
};
|
|
10
9
|
|
|
11
10
|
export const handleSignals = (process: NodeJS.Process, startTime: bigint, reporter: Reporter): void => {
|
|
12
11
|
const catchSignals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {resolve} from 'node:path';
|
|
4
|
-
import {
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
5
|
|
|
6
6
|
describe('CLI Comprehensive Integration', () => {
|
|
7
7
|
const cwd = resolve(__dirname, '../..');
|
|
@@ -9,13 +9,13 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
9
9
|
|
|
10
10
|
beforeEach(() => {
|
|
11
11
|
if (existsSync(testOutputDir)) {
|
|
12
|
-
rmSync(testOutputDir, {recursive: true, force: true});
|
|
12
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
13
13
|
}
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
afterEach(() => {
|
|
17
17
|
if (existsSync(testOutputDir)) {
|
|
18
|
-
rmSync(testOutputDir, {recursive: true, force: true});
|
|
18
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -23,10 +23,10 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
23
23
|
it('should generate code with default output directory', () => {
|
|
24
24
|
execSync('node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output generated', {
|
|
25
25
|
encoding: 'utf-8',
|
|
26
|
-
cwd
|
|
26
|
+
cwd
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
const outputFile = resolve(cwd, 'generated/
|
|
29
|
+
const outputFile = resolve(cwd, 'generated/api.ts');
|
|
30
30
|
expect(existsSync(outputFile)).toBe(true);
|
|
31
31
|
|
|
32
32
|
const content = readFileSync(outputFile, 'utf-8');
|
|
@@ -36,23 +36,20 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
36
36
|
it('should generate code with custom output directory', () => {
|
|
37
37
|
execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir}`, {
|
|
38
38
|
encoding: 'utf-8',
|
|
39
|
-
cwd
|
|
39
|
+
cwd
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
const outputFile = resolve(testOutputDir, '
|
|
42
|
+
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
43
43
|
expect(existsSync(outputFile)).toBe(true);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it('should accept naming convention option', () => {
|
|
47
|
-
execSync(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
cwd,
|
|
52
|
-
},
|
|
53
|
-
);
|
|
47
|
+
execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir} --naming-convention camelCase`, {
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
cwd
|
|
50
|
+
});
|
|
54
51
|
|
|
55
|
-
const outputFile = resolve(testOutputDir, '
|
|
52
|
+
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
56
53
|
const content = readFileSync(outputFile, 'utf-8');
|
|
57
54
|
|
|
58
55
|
// Verify camelCase is applied (operation IDs should be camelCase)
|
|
@@ -61,16 +58,25 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
61
58
|
|
|
62
59
|
it('should reject invalid naming convention', () => {
|
|
63
60
|
expect(() => {
|
|
64
|
-
execSync(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
stdio: 'pipe',
|
|
70
|
-
},
|
|
71
|
-
);
|
|
61
|
+
execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir} --naming-convention invalid`, {
|
|
62
|
+
encoding: 'utf-8',
|
|
63
|
+
cwd,
|
|
64
|
+
stdio: 'pipe'
|
|
65
|
+
});
|
|
72
66
|
}).toThrow();
|
|
73
67
|
});
|
|
68
|
+
|
|
69
|
+
it('should write to custom file when output is a .ts path', () => {
|
|
70
|
+
const outputFile = resolve(testOutputDir, 'client.ts');
|
|
71
|
+
execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output "${outputFile}"`, {
|
|
72
|
+
encoding: 'utf-8',
|
|
73
|
+
cwd
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
77
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
78
|
+
expect(content).toContain('SwaggerPetstoreOpenAPI30');
|
|
79
|
+
});
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
describe('CLI Error Handling', () => {
|
|
@@ -79,7 +85,7 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
79
85
|
execSync('node ./dist/src/cli.js --input ./nonexistent.yaml --output generated', {
|
|
80
86
|
encoding: 'utf-8',
|
|
81
87
|
cwd,
|
|
82
|
-
stdio: 'pipe'
|
|
88
|
+
stdio: 'pipe'
|
|
83
89
|
});
|
|
84
90
|
expect.fail('Should have thrown an error');
|
|
85
91
|
} catch (error: unknown) {
|
|
@@ -93,7 +99,7 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
93
99
|
execSync('node ./dist/src/cli.js --output generated', {
|
|
94
100
|
encoding: 'utf-8',
|
|
95
101
|
cwd,
|
|
96
|
-
stdio: 'pipe'
|
|
102
|
+
stdio: 'pipe'
|
|
97
103
|
});
|
|
98
104
|
}).toThrow();
|
|
99
105
|
});
|
|
@@ -103,20 +109,20 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
103
109
|
it('should handle JSON format', () => {
|
|
104
110
|
execSync(`node ./dist/src/cli.js --input ./samples/pokeapi-openapi.json --output ${testOutputDir}`, {
|
|
105
111
|
encoding: 'utf-8',
|
|
106
|
-
cwd
|
|
112
|
+
cwd
|
|
107
113
|
});
|
|
108
114
|
|
|
109
|
-
const outputFile = resolve(testOutputDir, '
|
|
115
|
+
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
110
116
|
expect(existsSync(outputFile)).toBe(true);
|
|
111
117
|
});
|
|
112
118
|
|
|
113
119
|
it('should handle YAML format', () => {
|
|
114
120
|
execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir}`, {
|
|
115
121
|
encoding: 'utf-8',
|
|
116
|
-
cwd
|
|
122
|
+
cwd
|
|
117
123
|
});
|
|
118
124
|
|
|
119
|
-
const outputFile = resolve(testOutputDir, '
|
|
125
|
+
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
120
126
|
expect(existsSync(outputFile)).toBe(true);
|
|
121
127
|
});
|
|
122
128
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
5
|
describe('CLI Integration', () => {
|
|
6
6
|
describe('--help', () => {
|
|
7
7
|
it('should display help information', () => {
|
|
8
8
|
const result = execSync('node ./dist/src/cli.js --help', {
|
|
9
9
|
encoding: 'utf-8',
|
|
10
|
-
cwd: resolve(__dirname, '../..')
|
|
10
|
+
cwd: resolve(__dirname, '../..')
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
expect(result).toContain('Usage:');
|
|
@@ -20,7 +20,7 @@ describe('CLI Integration', () => {
|
|
|
20
20
|
it('should display version information', () => {
|
|
21
21
|
const result = execSync('node ./dist/src/cli.js --version', {
|
|
22
22
|
encoding: 'utf-8',
|
|
23
|
-
cwd: resolve(__dirname, '../..')
|
|
23
|
+
cwd: resolve(__dirname, '../..')
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
expect(result).toMatch(/\d+\.\d+\.\d+/);
|