zod-codegen 1.3.0 → 1.4.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/CHANGELOG.md +6 -0
- package/README.md +61 -9
- package/dist/scripts/update-manifest.d.ts +14 -0
- package/dist/scripts/update-manifest.d.ts.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +31 -2
- package/dist/src/generator.d.ts +23 -0
- package/dist/src/generator.d.ts.map +1 -0
- package/dist/src/generator.js +3 -2
- package/dist/src/http/fetch-client.d.ts +15 -0
- package/dist/src/http/fetch-client.d.ts.map +1 -0
- package/dist/src/interfaces/code-generator.d.ts +20 -0
- package/dist/src/interfaces/code-generator.d.ts.map +1 -0
- package/dist/src/interfaces/file-reader.d.ts +13 -0
- package/dist/src/interfaces/file-reader.d.ts.map +1 -0
- package/dist/src/polyfills/fetch.d.ts +5 -0
- package/dist/src/polyfills/fetch.d.ts.map +1 -0
- package/dist/src/services/code-generator.service.d.ts +57 -0
- package/dist/src/services/code-generator.service.d.ts.map +1 -0
- package/dist/src/services/code-generator.service.js +38 -1
- package/dist/src/services/file-reader.service.d.ts +9 -0
- package/dist/src/services/file-reader.service.d.ts.map +1 -0
- package/dist/src/services/file-writer.service.d.ts +10 -0
- package/dist/src/services/file-writer.service.d.ts.map +1 -0
- package/dist/src/services/import-builder.service.d.ts +14 -0
- package/dist/src/services/import-builder.service.d.ts.map +1 -0
- package/dist/src/services/type-builder.service.d.ts +12 -0
- package/dist/src/services/type-builder.service.d.ts.map +1 -0
- package/dist/src/types/generator-options.d.ts +59 -0
- package/dist/src/types/generator-options.d.ts.map +1 -0
- package/dist/src/types/generator-options.js +1 -0
- package/dist/src/types/http.d.ts +25 -0
- package/dist/src/types/http.d.ts.map +1 -0
- package/dist/src/types/openapi.d.ts +1120 -0
- package/dist/src/types/openapi.d.ts.map +1 -0
- package/dist/src/utils/error-handler.d.ts +3 -0
- package/dist/src/utils/error-handler.d.ts.map +1 -0
- package/dist/src/utils/error-handler.js +2 -2
- package/dist/src/utils/execution-time.d.ts +2 -0
- package/dist/src/utils/execution-time.d.ts.map +1 -0
- package/dist/src/utils/manifest.d.ts +8 -0
- package/dist/src/utils/manifest.d.ts.map +1 -0
- package/dist/src/utils/naming-convention.d.ts +80 -0
- package/dist/src/utils/naming-convention.d.ts.map +1 -0
- package/dist/src/utils/naming-convention.js +135 -0
- package/dist/src/utils/reporter.d.ts +7 -0
- package/dist/src/utils/reporter.d.ts.map +1 -0
- package/dist/src/utils/signal-handler.d.ts +3 -0
- package/dist/src/utils/signal-handler.d.ts.map +1 -0
- package/dist/src/utils/signal-handler.js +2 -2
- package/dist/src/utils/tty.d.ts +2 -0
- package/dist/src/utils/tty.d.ts.map +1 -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 +2 -2
- 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 +170 -1
- 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/generator.test.d.ts +2 -0
- package/dist/tests/unit/generator.test.d.ts.map +1 -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 +231 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/package.json +10 -5
- package/src/cli.ts +34 -3
- package/src/generator.ts +8 -1
- package/src/services/code-generator.service.ts +47 -1
- package/src/types/generator-options.ts +60 -0
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/naming-convention.ts +214 -0
- package/src/utils/signal-handler.ts +2 -2
- package/tests/integration/cli.test.ts +2 -2
- package/tests/unit/code-generator.test.ts +189 -1
- package/tests/unit/naming-convention.test.ts +263 -0
- package/tsconfig.json +2 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { transformNamingConvention } from '../../src/utils/naming-convention.js';
|
|
3
|
+
describe('transformNamingConvention', () => {
|
|
4
|
+
describe('transform', () => {
|
|
5
|
+
const testCases = [
|
|
6
|
+
// camelCase
|
|
7
|
+
{
|
|
8
|
+
input: 'get_user_by_id',
|
|
9
|
+
convention: 'camelCase',
|
|
10
|
+
expected: 'getUserById',
|
|
11
|
+
description: 'should convert snake_case to camelCase',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
input: 'GetUserById',
|
|
15
|
+
convention: 'camelCase',
|
|
16
|
+
expected: 'getUserById',
|
|
17
|
+
description: 'should convert PascalCase to camelCase',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
input: 'get-user-by-id',
|
|
21
|
+
convention: 'camelCase',
|
|
22
|
+
expected: 'getUserById',
|
|
23
|
+
description: 'should convert kebab-case to camelCase',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
input: 'getUserById',
|
|
27
|
+
convention: 'camelCase',
|
|
28
|
+
expected: 'getUserById',
|
|
29
|
+
description: 'should keep camelCase as camelCase',
|
|
30
|
+
},
|
|
31
|
+
// PascalCase
|
|
32
|
+
{
|
|
33
|
+
input: 'get_user_by_id',
|
|
34
|
+
convention: 'PascalCase',
|
|
35
|
+
expected: 'GetUserById',
|
|
36
|
+
description: 'should convert snake_case to PascalCase',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
input: 'get-user-by-id',
|
|
40
|
+
convention: 'PascalCase',
|
|
41
|
+
expected: 'GetUserById',
|
|
42
|
+
description: 'should convert kebab-case to PascalCase',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
input: 'getUserById',
|
|
46
|
+
convention: 'PascalCase',
|
|
47
|
+
expected: 'GetUserById',
|
|
48
|
+
description: 'should convert camelCase to PascalCase',
|
|
49
|
+
},
|
|
50
|
+
// snake_case
|
|
51
|
+
{
|
|
52
|
+
input: 'getUserById',
|
|
53
|
+
convention: 'snake_case',
|
|
54
|
+
expected: 'get_user_by_id',
|
|
55
|
+
description: 'should convert camelCase to snake_case',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
input: 'GetUserById',
|
|
59
|
+
convention: 'snake_case',
|
|
60
|
+
expected: 'get_user_by_id',
|
|
61
|
+
description: 'should convert PascalCase to snake_case',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
input: 'get-user-by-id',
|
|
65
|
+
convention: 'snake_case',
|
|
66
|
+
expected: 'get_user_by_id',
|
|
67
|
+
description: 'should convert kebab-case to snake_case',
|
|
68
|
+
},
|
|
69
|
+
// kebab-case
|
|
70
|
+
{
|
|
71
|
+
input: 'getUserById',
|
|
72
|
+
convention: 'kebab-case',
|
|
73
|
+
expected: 'get-user-by-id',
|
|
74
|
+
description: 'should convert camelCase to kebab-case',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
input: 'GetUserById',
|
|
78
|
+
convention: 'kebab-case',
|
|
79
|
+
expected: 'get-user-by-id',
|
|
80
|
+
description: 'should convert PascalCase to kebab-case',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
input: 'get_user_by_id',
|
|
84
|
+
convention: 'kebab-case',
|
|
85
|
+
expected: 'get-user-by-id',
|
|
86
|
+
description: 'should convert snake_case to kebab-case',
|
|
87
|
+
},
|
|
88
|
+
// SCREAMING_SNAKE_CASE
|
|
89
|
+
{
|
|
90
|
+
input: 'getUserById',
|
|
91
|
+
convention: 'SCREAMING_SNAKE_CASE',
|
|
92
|
+
expected: 'GET_USER_BY_ID',
|
|
93
|
+
description: 'should convert camelCase to SCREAMING_SNAKE_CASE',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
input: 'get-user-by-id',
|
|
97
|
+
convention: 'SCREAMING_SNAKE_CASE',
|
|
98
|
+
expected: 'GET_USER_BY_ID',
|
|
99
|
+
description: 'should convert kebab-case to SCREAMING_SNAKE_CASE',
|
|
100
|
+
},
|
|
101
|
+
// SCREAMING-KEBAB-CASE
|
|
102
|
+
{
|
|
103
|
+
input: 'getUserById',
|
|
104
|
+
convention: 'SCREAMING-KEBAB-CASE',
|
|
105
|
+
expected: 'GET-USER-BY-ID',
|
|
106
|
+
description: 'should convert camelCase to SCREAMING-KEBAB-CASE',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
input: 'get_user_by_id',
|
|
110
|
+
convention: 'SCREAMING-KEBAB-CASE',
|
|
111
|
+
expected: 'GET-USER-BY-ID',
|
|
112
|
+
description: 'should convert snake_case to SCREAMING-KEBAB-CASE',
|
|
113
|
+
},
|
|
114
|
+
// Edge cases
|
|
115
|
+
{
|
|
116
|
+
input: '',
|
|
117
|
+
convention: 'camelCase',
|
|
118
|
+
expected: '',
|
|
119
|
+
description: 'should handle empty string',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
input: 'a',
|
|
123
|
+
convention: 'camelCase',
|
|
124
|
+
expected: 'a',
|
|
125
|
+
description: 'should handle single character',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
input: 'API',
|
|
129
|
+
convention: 'camelCase',
|
|
130
|
+
expected: 'aPI',
|
|
131
|
+
description: 'should handle all uppercase (splits at uppercase boundaries)',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
input: 'getUser123ById',
|
|
135
|
+
convention: 'snake_case',
|
|
136
|
+
expected: 'get_user_123_by_id',
|
|
137
|
+
description: 'should handle numbers in identifiers (splits at digit boundaries)',
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
testCases.forEach(({ input, convention, expected, description }) => {
|
|
141
|
+
it(description, () => {
|
|
142
|
+
const result = transformNamingConvention(input, convention);
|
|
143
|
+
expect(result).toBe(expected);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('edge cases', () => {
|
|
148
|
+
describe('consecutive delimiters', () => {
|
|
149
|
+
it('should handle consecutive underscores', () => {
|
|
150
|
+
expect(transformNamingConvention('get__user__by__id', 'camelCase')).toBe('getUserById');
|
|
151
|
+
});
|
|
152
|
+
it('should handle consecutive hyphens', () => {
|
|
153
|
+
expect(transformNamingConvention('get--user--by--id', 'snake_case')).toBe('get_user_by_id');
|
|
154
|
+
});
|
|
155
|
+
it('should handle mixed consecutive delimiters', () => {
|
|
156
|
+
expect(transformNamingConvention('get__user--by_id', 'kebab-case')).toBe('get-user-by-id');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('mixed delimiters', () => {
|
|
160
|
+
it('should handle snake_case and kebab-case mixed', () => {
|
|
161
|
+
// Note: Delimiters split the string, so 'byId' becomes 'by' and 'id' (normalized to lowercase)
|
|
162
|
+
expect(transformNamingConvention('get_user-byId', 'camelCase')).toBe('getUserByid');
|
|
163
|
+
});
|
|
164
|
+
it('should handle camelCase with underscores', () => {
|
|
165
|
+
// Note: Underscore delimiter splits, so 'byId' becomes 'by' and 'id'
|
|
166
|
+
expect(transformNamingConvention('getUser_byId', 'PascalCase')).toBe('GetuserByid');
|
|
167
|
+
});
|
|
168
|
+
it('should handle dots as delimiters', () => {
|
|
169
|
+
expect(transformNamingConvention('get.user.by.id', 'snake_case')).toBe('get_user_by_id');
|
|
170
|
+
});
|
|
171
|
+
it('should handle spaces as delimiters', () => {
|
|
172
|
+
expect(transformNamingConvention('get user by id', 'kebab-case')).toBe('get-user-by-id');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('unicode and special characters', () => {
|
|
176
|
+
it('should handle accented characters', () => {
|
|
177
|
+
expect(transformNamingConvention('getRésumé', 'snake_case')).toBe('get_résumé');
|
|
178
|
+
});
|
|
179
|
+
it('should handle numbers at start', () => {
|
|
180
|
+
expect(transformNamingConvention('123getUser', 'camelCase')).toBe('123getUser');
|
|
181
|
+
});
|
|
182
|
+
it('should handle single uppercase letter', () => {
|
|
183
|
+
expect(transformNamingConvention('getX', 'snake_case')).toBe('get_x');
|
|
184
|
+
});
|
|
185
|
+
it('should handle all numbers', () => {
|
|
186
|
+
expect(transformNamingConvention('123456', 'camelCase')).toBe('123456');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('acronyms and abbreviations', () => {
|
|
190
|
+
it('should handle acronyms in camelCase (splits on uppercase boundaries)', () => {
|
|
191
|
+
// Note: Algorithm splits on uppercase boundaries, so 'XML' becomes 'X', 'M', 'L'
|
|
192
|
+
// This is correct behavior - detecting acronyms would require a dictionary
|
|
193
|
+
expect(transformNamingConvention('getXMLData', 'snake_case')).toBe('get_x_m_l_data');
|
|
194
|
+
});
|
|
195
|
+
it('should handle multiple acronyms (splits on uppercase boundaries)', () => {
|
|
196
|
+
// Note: 'JSON' and 'XML' are split into individual letters
|
|
197
|
+
// 'parseJSONToXML' → ['parse', 'j', 's', 'o', 'n', 'to', 'x', 'm', 'l']
|
|
198
|
+
expect(transformNamingConvention('parseJSONToXML', 'kebab-case')).toBe('parse-j-s-o-n-to-x-m-l');
|
|
199
|
+
});
|
|
200
|
+
it('should handle ID abbreviation (splits on uppercase boundaries)', () => {
|
|
201
|
+
// Note: 'ID' is split into 'I' and 'D' because the algorithm splits on uppercase boundaries
|
|
202
|
+
// This is correct behavior - detecting acronyms would require a dictionary
|
|
203
|
+
expect(transformNamingConvention('getUserID', 'snake_case')).toBe('get_user_i_d');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('already transformed names', () => {
|
|
207
|
+
it('should be idempotent for camelCase', () => {
|
|
208
|
+
const input = 'getUserById';
|
|
209
|
+
const result = transformNamingConvention(input, 'camelCase');
|
|
210
|
+
expect(transformNamingConvention(result, 'camelCase')).toBe(result);
|
|
211
|
+
});
|
|
212
|
+
it('should be idempotent for snake_case', () => {
|
|
213
|
+
const input = 'get_user_by_id';
|
|
214
|
+
const result = transformNamingConvention(input, 'snake_case');
|
|
215
|
+
expect(transformNamingConvention(result, 'snake_case')).toBe(result);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('special patterns', () => {
|
|
219
|
+
it('should handle single word', () => {
|
|
220
|
+
expect(transformNamingConvention('user', 'PascalCase')).toBe('User');
|
|
221
|
+
});
|
|
222
|
+
it('should handle two words', () => {
|
|
223
|
+
expect(transformNamingConvention('getUser', 'snake_case')).toBe('get_user');
|
|
224
|
+
});
|
|
225
|
+
it('should handle very long names', () => {
|
|
226
|
+
const longName = 'getVeryLongOperationNameWithManyWords';
|
|
227
|
+
expect(transformNamingConvention(longName, 'kebab-case')).toBe('get-very-long-operation-name-with-many-words');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":";AAGA,wBAmCG"}
|
package/package.json
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@types/jsonpath": "^0.2.4",
|
|
46
46
|
"@types/node": "^24.10.1",
|
|
47
47
|
"@types/yargs": "^17.0.35",
|
|
48
|
-
"@vitest/coverage-v8": "^4.0.
|
|
48
|
+
"@vitest/coverage-v8": "^4.0.14",
|
|
49
49
|
"eslint": "^9.39.1",
|
|
50
50
|
"eslint-config-prettier": "^10.1.8",
|
|
51
51
|
"husky": "^9.1.7",
|
|
@@ -54,16 +54,17 @@
|
|
|
54
54
|
"ts-node": "^10.9.2",
|
|
55
55
|
"typescript-eslint": "^8.46.4",
|
|
56
56
|
"undici": "^7.16.0",
|
|
57
|
-
"vitest": "^4.0.
|
|
57
|
+
"vitest": "^4.0.14",
|
|
58
58
|
"yarn-audit-fix": "^10.1.1"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
61
|
"undici": "^7.16.0"
|
|
62
62
|
},
|
|
63
63
|
"homepage": "https://github.com/julienandreu/zod-codegen",
|
|
64
|
-
"license": "
|
|
64
|
+
"license": "Apache-2.0",
|
|
65
65
|
"name": "zod-codegen",
|
|
66
66
|
"type": "module",
|
|
67
|
+
"types": "./dist/src/generator.d.ts",
|
|
67
68
|
"exports": {
|
|
68
69
|
".": {
|
|
69
70
|
"types": "./dist/src/generator.d.ts",
|
|
@@ -84,7 +85,11 @@
|
|
|
84
85
|
"tar": "^7.5.2"
|
|
85
86
|
},
|
|
86
87
|
"glob": "^11.1.0",
|
|
87
|
-
"tar": "^7.5.2"
|
|
88
|
+
"tar": "^7.5.2",
|
|
89
|
+
"yargs": "^18.0.0"
|
|
90
|
+
},
|
|
91
|
+
"resolutions": {
|
|
92
|
+
"yargs": "^18.0.0"
|
|
88
93
|
},
|
|
89
94
|
"scripts": {
|
|
90
95
|
"audit:fix": "yarn-audit-fix",
|
|
@@ -110,5 +115,5 @@
|
|
|
110
115
|
"release": "semantic-release",
|
|
111
116
|
"release:dry": "semantic-release --dry-run"
|
|
112
117
|
},
|
|
113
|
-
"version": "1.
|
|
118
|
+
"version": "1.4.0"
|
|
114
119
|
}
|
package/src/cli.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import yargs from 'yargs';
|
|
4
4
|
import {hideBin} from 'yargs/helpers';
|
|
5
|
-
import {Generator} from './generator.js';
|
|
5
|
+
import {Generator, type GeneratorOptions, type NamingConvention} from './generator.js';
|
|
6
6
|
import {readFileSync} from 'node:fs';
|
|
7
7
|
import {fileURLToPath} from 'node:url';
|
|
8
8
|
import {dirname, join} from 'node:path';
|
|
@@ -72,15 +72,46 @@ const argv = yargs(hideBin(process.argv))
|
|
|
72
72
|
description: 'Directory to output the generated files',
|
|
73
73
|
default: 'generated',
|
|
74
74
|
})
|
|
75
|
+
.option('naming-convention', {
|
|
76
|
+
alias: 'n',
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Naming convention to apply to operation IDs',
|
|
79
|
+
choices: ['camelCase', 'PascalCase', 'snake_case', 'kebab-case', 'SCREAMING_SNAKE_CASE', 'SCREAMING-KEBAB-CASE'],
|
|
80
|
+
default: undefined,
|
|
81
|
+
})
|
|
75
82
|
.strict()
|
|
76
83
|
.help()
|
|
77
84
|
.parseSync();
|
|
78
85
|
|
|
79
|
-
const {input, output} = argv;
|
|
86
|
+
const {input, output, namingConvention} = argv;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Type guard to validate that a string is a valid naming convention.
|
|
90
|
+
* This ensures type safety when parsing CLI arguments.
|
|
91
|
+
*
|
|
92
|
+
* @param value - The value to check
|
|
93
|
+
* @returns True if the value is a valid NamingConvention
|
|
94
|
+
*/
|
|
95
|
+
function isValidNamingConvention(value: string | undefined): value is NamingConvention {
|
|
96
|
+
if (value === undefined) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const validConventions: readonly NamingConvention[] = [
|
|
100
|
+
'camelCase',
|
|
101
|
+
'PascalCase',
|
|
102
|
+
'snake_case',
|
|
103
|
+
'kebab-case',
|
|
104
|
+
'SCREAMING_SNAKE_CASE',
|
|
105
|
+
'SCREAMING-KEBAB-CASE',
|
|
106
|
+
] as const;
|
|
107
|
+
return validConventions.includes(value as NamingConvention);
|
|
108
|
+
}
|
|
80
109
|
|
|
81
110
|
void (async () => {
|
|
82
111
|
try {
|
|
83
|
-
const
|
|
112
|
+
const options: GeneratorOptions = isValidNamingConvention(namingConvention) ? {namingConvention} : {};
|
|
113
|
+
|
|
114
|
+
const generator = new Generator(name, version, reporter, input, output, options);
|
|
84
115
|
const exitCode = await generator.run();
|
|
85
116
|
process.exit(exitCode);
|
|
86
117
|
} catch (error) {
|
package/src/generator.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type {Reporter} from './utils/reporter.js';
|
|
2
2
|
import type {OpenApiSpecType} from './types/openapi.js';
|
|
3
|
+
import type {GeneratorOptions} from './types/generator-options.js';
|
|
3
4
|
import {OpenApiFileParserService, SyncFileReaderService} from './services/file-reader.service.js';
|
|
4
5
|
import {TypeScriptCodeGeneratorService} from './services/code-generator.service.js';
|
|
5
6
|
import {SyncFileWriterService} from './services/file-writer.service.js';
|
|
6
7
|
|
|
8
|
+
// Re-export types for library users
|
|
9
|
+
export type {GeneratorOptions} from './types/generator-options.js';
|
|
10
|
+
export type {NamingConvention, OperationDetails, OperationNameTransformer} from './utils/naming-convention.js';
|
|
11
|
+
|
|
7
12
|
export class Generator {
|
|
8
13
|
private readonly fileReader = new SyncFileReaderService();
|
|
9
14
|
private readonly fileParser = new OpenApiFileParserService();
|
|
10
|
-
private readonly codeGenerator
|
|
15
|
+
private readonly codeGenerator: TypeScriptCodeGeneratorService;
|
|
11
16
|
private readonly fileWriter: SyncFileWriterService;
|
|
12
17
|
private readonly outputPath: string;
|
|
13
18
|
|
|
@@ -17,9 +22,11 @@ export class Generator {
|
|
|
17
22
|
private readonly reporter: Reporter,
|
|
18
23
|
private readonly inputPath: string,
|
|
19
24
|
private readonly _outputDir: string,
|
|
25
|
+
options: GeneratorOptions = {},
|
|
20
26
|
) {
|
|
21
27
|
this.fileWriter = new SyncFileWriterService(this._name, this._version, inputPath);
|
|
22
28
|
this.outputPath = this.fileWriter.resolveOutputPath(this._outputDir);
|
|
29
|
+
this.codeGenerator = new TypeScriptCodeGeneratorService(options);
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
async run(): Promise<number> {
|
|
@@ -3,14 +3,28 @@ import * as ts from 'typescript';
|
|
|
3
3
|
import {z} from 'zod';
|
|
4
4
|
import type {CodeGenerator, SchemaBuilder} from '../interfaces/code-generator.js';
|
|
5
5
|
import type {MethodSchemaType, OpenApiSpecType, ReferenceType} from '../types/openapi.js';
|
|
6
|
+
import type {GeneratorOptions} from '../types/generator-options.js';
|
|
6
7
|
import {MethodSchema, Reference, SchemaProperties} from '../types/openapi.js';
|
|
7
8
|
import {TypeScriptImportBuilderService} from './import-builder.service.js';
|
|
8
9
|
import {TypeScriptTypeBuilderService} from './type-builder.service.js';
|
|
10
|
+
import {
|
|
11
|
+
type NamingConvention,
|
|
12
|
+
type OperationDetails,
|
|
13
|
+
type OperationNameTransformer,
|
|
14
|
+
transformNamingConvention,
|
|
15
|
+
} from '../utils/naming-convention.js';
|
|
9
16
|
|
|
10
17
|
export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuilder {
|
|
11
18
|
private readonly typeBuilder = new TypeScriptTypeBuilderService();
|
|
12
19
|
private readonly importBuilder = new TypeScriptImportBuilderService();
|
|
13
20
|
private readonly printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
|
|
21
|
+
private readonly namingConvention: NamingConvention | undefined;
|
|
22
|
+
private readonly operationNameTransformer: OperationNameTransformer | undefined;
|
|
23
|
+
|
|
24
|
+
constructor(options: GeneratorOptions = {}) {
|
|
25
|
+
this.namingConvention = options.namingConvention;
|
|
26
|
+
this.operationNameTransformer = options.operationNameTransformer;
|
|
27
|
+
}
|
|
14
28
|
|
|
15
29
|
private readonly ZodAST = z.object({
|
|
16
30
|
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'unknown', 'record']),
|
|
@@ -894,6 +908,36 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
894
908
|
}, []);
|
|
895
909
|
}
|
|
896
910
|
|
|
911
|
+
/**
|
|
912
|
+
* Transforms operation ID according to the configured naming convention or transformer
|
|
913
|
+
* Ensures the result is a valid TypeScript identifier
|
|
914
|
+
*/
|
|
915
|
+
private transformOperationName(operationId: string, method: string, path: string, schema: MethodSchemaType): string {
|
|
916
|
+
let transformed: string;
|
|
917
|
+
|
|
918
|
+
// Custom transformer takes precedence
|
|
919
|
+
if (this.operationNameTransformer) {
|
|
920
|
+
const details: OperationDetails = {
|
|
921
|
+
operationId,
|
|
922
|
+
method,
|
|
923
|
+
path,
|
|
924
|
+
...(schema.tags !== undefined && {tags: schema.tags}),
|
|
925
|
+
...(schema.summary !== undefined && {summary: schema.summary}),
|
|
926
|
+
...(schema.description !== undefined && {description: schema.description}),
|
|
927
|
+
};
|
|
928
|
+
transformed = this.operationNameTransformer(details);
|
|
929
|
+
} else if (this.namingConvention) {
|
|
930
|
+
// Apply naming convention if specified
|
|
931
|
+
transformed = transformNamingConvention(operationId, this.namingConvention);
|
|
932
|
+
} else {
|
|
933
|
+
// Return original operationId if no transformation is configured
|
|
934
|
+
transformed = operationId;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Sanitize to ensure valid TypeScript identifier (handles edge cases from custom transformers)
|
|
938
|
+
return this.typeBuilder.sanitizeIdentifier(transformed);
|
|
939
|
+
}
|
|
940
|
+
|
|
897
941
|
private buildEndpointMethod(
|
|
898
942
|
method: string,
|
|
899
943
|
path: string,
|
|
@@ -979,10 +1023,12 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
979
1023
|
statements.push(ts.factory.createReturnStatement(ts.factory.createAwaitExpression(makeRequestCall)));
|
|
980
1024
|
}
|
|
981
1025
|
|
|
1026
|
+
const transformedOperationId = this.transformOperationName(String(schema.operationId), method, path, schema);
|
|
1027
|
+
|
|
982
1028
|
const methodDeclaration = ts.factory.createMethodDeclaration(
|
|
983
1029
|
[ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
|
|
984
1030
|
undefined,
|
|
985
|
-
ts.factory.createIdentifier(
|
|
1031
|
+
ts.factory.createIdentifier(transformedOperationId),
|
|
986
1032
|
undefined,
|
|
987
1033
|
undefined,
|
|
988
1034
|
parameters,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {NamingConvention, OperationNameTransformer} from '../utils/naming-convention.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the Generator class.
|
|
5
|
+
*
|
|
6
|
+
* These options control how operation IDs are transformed during code generation.
|
|
7
|
+
* You can either use a predefined naming convention or provide a custom transformer function.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // Using a naming convention
|
|
12
|
+
* const generator = new Generator(..., {
|
|
13
|
+
* namingConvention: 'camelCase'
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Using a custom transformer
|
|
17
|
+
* const generator = new Generator(..., {
|
|
18
|
+
* operationNameTransformer: (details) => {
|
|
19
|
+
* return `${details.method}_${details.operationId}`;
|
|
20
|
+
* }
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export interface GeneratorOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Naming convention to apply to operation IDs.
|
|
27
|
+
*
|
|
28
|
+
* If provided, all operation IDs will be transformed according to the specified convention.
|
|
29
|
+
* This is useful when OpenAPI specs have inconsistent or poorly named operation IDs.
|
|
30
|
+
*
|
|
31
|
+
* **Note:** If `operationNameTransformer` is also provided, it takes precedence.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* { namingConvention: 'camelCase' } // Transforms 'get_user_by_id' → 'getUserById'
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
namingConvention?: NamingConvention;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Custom transformer function for operation names.
|
|
42
|
+
*
|
|
43
|
+
* If provided, this function will be called for each operation with full operation details.
|
|
44
|
+
* This allows for advanced customization based on HTTP method, path, tags, etc.
|
|
45
|
+
*
|
|
46
|
+
* **Note:** This takes precedence over `namingConvention` if both are provided.
|
|
47
|
+
* The returned name will be sanitized to ensure it's a valid TypeScript identifier.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* {
|
|
52
|
+
* operationNameTransformer: (details) => {
|
|
53
|
+
* const tag = details.tags?.[0] || 'default';
|
|
54
|
+
* return `${details.method.toUpperCase()}_${tag}_${details.operationId}`;
|
|
55
|
+
* }
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
operationNameTransformer?: OperationNameTransformer;
|
|
60
|
+
}
|
|
@@ -7,7 +7,7 @@ export const errorReceived = (process: NodeJS.Process, startTime: bigint) => ():
|
|
|
7
7
|
|
|
8
8
|
export const handleErrors = (process: NodeJS.Process, startTime: bigint): void => {
|
|
9
9
|
const catchErrors: string[] = ['unhandledRejection', 'uncaughtException'];
|
|
10
|
-
catchErrors.
|
|
11
|
-
|
|
10
|
+
catchErrors.forEach((event) => {
|
|
11
|
+
process.on(event, errorReceived(process, startTime));
|
|
12
12
|
});
|
|
13
13
|
};
|