zod-codegen 1.3.0 → 1.4.1
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 +17 -17
- package/.github/workflows/release.yml +8 -8
- package/CHANGELOG.md +17 -0
- package/README.md +61 -9
- 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 +41 -4
- 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/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/package.json +15 -15
- package/src/cli.ts +34 -3
- package/src/generator.ts +8 -1
- package/src/services/code-generator.service.ts +51 -8
- 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 +192 -4
- package/tests/unit/generator.test.ts +2 -2
- package/tests/unit/naming-convention.test.ts +263 -0
- package/tsconfig.json +3 -1
- package/.claude/settings.local.json +0 -43
- package/dist/scripts/update-manifest.js +0 -31
- package/dist/tests/integration/cli.test.js +0 -25
- package/dist/tests/unit/code-generator.test.js +0 -290
- package/dist/tests/unit/file-reader.test.js +0 -110
- package/dist/tests/unit/generator.test.js +0 -100
- package/scripts/republish-versions.sh +0 -94
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest';
|
|
2
|
+
import {transformNamingConvention, type NamingConvention} from '../../src/utils/naming-convention.js';
|
|
3
|
+
|
|
4
|
+
describe('transformNamingConvention', () => {
|
|
5
|
+
describe('transform', () => {
|
|
6
|
+
const testCases: Array<{
|
|
7
|
+
input: string;
|
|
8
|
+
convention: NamingConvention;
|
|
9
|
+
expected: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}> = [
|
|
12
|
+
// camelCase
|
|
13
|
+
{
|
|
14
|
+
input: 'get_user_by_id',
|
|
15
|
+
convention: 'camelCase',
|
|
16
|
+
expected: 'getUserById',
|
|
17
|
+
description: 'should convert snake_case to camelCase',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
input: 'GetUserById',
|
|
21
|
+
convention: 'camelCase',
|
|
22
|
+
expected: 'getUserById',
|
|
23
|
+
description: 'should convert PascalCase to camelCase',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
input: 'get-user-by-id',
|
|
27
|
+
convention: 'camelCase',
|
|
28
|
+
expected: 'getUserById',
|
|
29
|
+
description: 'should convert kebab-case to camelCase',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
input: 'getUserById',
|
|
33
|
+
convention: 'camelCase',
|
|
34
|
+
expected: 'getUserById',
|
|
35
|
+
description: 'should keep camelCase as camelCase',
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// PascalCase
|
|
39
|
+
{
|
|
40
|
+
input: 'get_user_by_id',
|
|
41
|
+
convention: 'PascalCase',
|
|
42
|
+
expected: 'GetUserById',
|
|
43
|
+
description: 'should convert snake_case to PascalCase',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
input: 'get-user-by-id',
|
|
47
|
+
convention: 'PascalCase',
|
|
48
|
+
expected: 'GetUserById',
|
|
49
|
+
description: 'should convert kebab-case to PascalCase',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
input: 'getUserById',
|
|
53
|
+
convention: 'PascalCase',
|
|
54
|
+
expected: 'GetUserById',
|
|
55
|
+
description: 'should convert camelCase to PascalCase',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// snake_case
|
|
59
|
+
{
|
|
60
|
+
input: 'getUserById',
|
|
61
|
+
convention: 'snake_case',
|
|
62
|
+
expected: 'get_user_by_id',
|
|
63
|
+
description: 'should convert camelCase to snake_case',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
input: 'GetUserById',
|
|
67
|
+
convention: 'snake_case',
|
|
68
|
+
expected: 'get_user_by_id',
|
|
69
|
+
description: 'should convert PascalCase to snake_case',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
input: 'get-user-by-id',
|
|
73
|
+
convention: 'snake_case',
|
|
74
|
+
expected: 'get_user_by_id',
|
|
75
|
+
description: 'should convert kebab-case to snake_case',
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// kebab-case
|
|
79
|
+
{
|
|
80
|
+
input: 'getUserById',
|
|
81
|
+
convention: 'kebab-case',
|
|
82
|
+
expected: 'get-user-by-id',
|
|
83
|
+
description: 'should convert camelCase to kebab-case',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
input: 'GetUserById',
|
|
87
|
+
convention: 'kebab-case',
|
|
88
|
+
expected: 'get-user-by-id',
|
|
89
|
+
description: 'should convert PascalCase to kebab-case',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
input: 'get_user_by_id',
|
|
93
|
+
convention: 'kebab-case',
|
|
94
|
+
expected: 'get-user-by-id',
|
|
95
|
+
description: 'should convert snake_case to kebab-case',
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// SCREAMING_SNAKE_CASE
|
|
99
|
+
{
|
|
100
|
+
input: 'getUserById',
|
|
101
|
+
convention: 'SCREAMING_SNAKE_CASE',
|
|
102
|
+
expected: 'GET_USER_BY_ID',
|
|
103
|
+
description: 'should convert camelCase to SCREAMING_SNAKE_CASE',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
input: 'get-user-by-id',
|
|
107
|
+
convention: 'SCREAMING_SNAKE_CASE',
|
|
108
|
+
expected: 'GET_USER_BY_ID',
|
|
109
|
+
description: 'should convert kebab-case to SCREAMING_SNAKE_CASE',
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// SCREAMING-KEBAB-CASE
|
|
113
|
+
{
|
|
114
|
+
input: 'getUserById',
|
|
115
|
+
convention: 'SCREAMING-KEBAB-CASE',
|
|
116
|
+
expected: 'GET-USER-BY-ID',
|
|
117
|
+
description: 'should convert camelCase to SCREAMING-KEBAB-CASE',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
input: 'get_user_by_id',
|
|
121
|
+
convention: 'SCREAMING-KEBAB-CASE',
|
|
122
|
+
expected: 'GET-USER-BY-ID',
|
|
123
|
+
description: 'should convert snake_case to SCREAMING-KEBAB-CASE',
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Edge cases
|
|
127
|
+
{
|
|
128
|
+
input: '',
|
|
129
|
+
convention: 'camelCase',
|
|
130
|
+
expected: '',
|
|
131
|
+
description: 'should handle empty string',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
input: 'a',
|
|
135
|
+
convention: 'camelCase',
|
|
136
|
+
expected: 'a',
|
|
137
|
+
description: 'should handle single character',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
input: 'API',
|
|
141
|
+
convention: 'camelCase',
|
|
142
|
+
expected: 'aPI',
|
|
143
|
+
description: 'should handle all uppercase (splits at uppercase boundaries)',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
input: 'getUser123ById',
|
|
147
|
+
convention: 'snake_case',
|
|
148
|
+
expected: 'get_user_123_by_id',
|
|
149
|
+
description: 'should handle numbers in identifiers (splits at digit boundaries)',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
testCases.forEach(({input, convention, expected, description}) => {
|
|
154
|
+
it(description, () => {
|
|
155
|
+
const result = transformNamingConvention(input, convention);
|
|
156
|
+
expect(result).toBe(expected);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('edge cases', () => {
|
|
162
|
+
describe('consecutive delimiters', () => {
|
|
163
|
+
it('should handle consecutive underscores', () => {
|
|
164
|
+
expect(transformNamingConvention('get__user__by__id', 'camelCase')).toBe('getUserById');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle consecutive hyphens', () => {
|
|
168
|
+
expect(transformNamingConvention('get--user--by--id', 'snake_case')).toBe('get_user_by_id');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle mixed consecutive delimiters', () => {
|
|
172
|
+
expect(transformNamingConvention('get__user--by_id', 'kebab-case')).toBe('get-user-by-id');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('mixed delimiters', () => {
|
|
177
|
+
it('should handle snake_case and kebab-case mixed', () => {
|
|
178
|
+
// Note: Delimiters split the string, so 'byId' becomes 'by' and 'id' (normalized to lowercase)
|
|
179
|
+
expect(transformNamingConvention('get_user-byId', 'camelCase')).toBe('getUserByid');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should handle camelCase with underscores', () => {
|
|
183
|
+
// Note: Underscore delimiter splits, so 'byId' becomes 'by' and 'id'
|
|
184
|
+
expect(transformNamingConvention('getUser_byId', 'PascalCase')).toBe('GetuserByid');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle dots as delimiters', () => {
|
|
188
|
+
expect(transformNamingConvention('get.user.by.id', 'snake_case')).toBe('get_user_by_id');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle spaces as delimiters', () => {
|
|
192
|
+
expect(transformNamingConvention('get user by id', 'kebab-case')).toBe('get-user-by-id');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('unicode and special characters', () => {
|
|
197
|
+
it('should handle accented characters', () => {
|
|
198
|
+
expect(transformNamingConvention('getRésumé', 'snake_case')).toBe('get_résumé');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should handle numbers at start', () => {
|
|
202
|
+
expect(transformNamingConvention('123getUser', 'camelCase')).toBe('123getUser');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle single uppercase letter', () => {
|
|
206
|
+
expect(transformNamingConvention('getX', 'snake_case')).toBe('get_x');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should handle all numbers', () => {
|
|
210
|
+
expect(transformNamingConvention('123456', 'camelCase')).toBe('123456');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('acronyms and abbreviations', () => {
|
|
215
|
+
it('should handle acronyms in camelCase (splits on uppercase boundaries)', () => {
|
|
216
|
+
// Note: Algorithm splits on uppercase boundaries, so 'XML' becomes 'X', 'M', 'L'
|
|
217
|
+
// This is correct behavior - detecting acronyms would require a dictionary
|
|
218
|
+
expect(transformNamingConvention('getXMLData', 'snake_case')).toBe('get_x_m_l_data');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle multiple acronyms (splits on uppercase boundaries)', () => {
|
|
222
|
+
// Note: 'JSON' and 'XML' are split into individual letters
|
|
223
|
+
// 'parseJSONToXML' → ['parse', 'j', 's', 'o', 'n', 'to', 'x', 'm', 'l']
|
|
224
|
+
expect(transformNamingConvention('parseJSONToXML', 'kebab-case')).toBe('parse-j-s-o-n-to-x-m-l');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle ID abbreviation (splits on uppercase boundaries)', () => {
|
|
228
|
+
// Note: 'ID' is split into 'I' and 'D' because the algorithm splits on uppercase boundaries
|
|
229
|
+
// This is correct behavior - detecting acronyms would require a dictionary
|
|
230
|
+
expect(transformNamingConvention('getUserID', 'snake_case')).toBe('get_user_i_d');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('already transformed names', () => {
|
|
235
|
+
it('should be idempotent for camelCase', () => {
|
|
236
|
+
const input = 'getUserById';
|
|
237
|
+
const result = transformNamingConvention(input, 'camelCase');
|
|
238
|
+
expect(transformNamingConvention(result, 'camelCase')).toBe(result);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should be idempotent for snake_case', () => {
|
|
242
|
+
const input = 'get_user_by_id';
|
|
243
|
+
const result = transformNamingConvention(input, 'snake_case');
|
|
244
|
+
expect(transformNamingConvention(result, 'snake_case')).toBe(result);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('special patterns', () => {
|
|
249
|
+
it('should handle single word', () => {
|
|
250
|
+
expect(transformNamingConvention('user', 'PascalCase')).toBe('User');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle two words', () => {
|
|
254
|
+
expect(transformNamingConvention('getUser', 'snake_case')).toBe('get_user');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle very long names', () => {
|
|
258
|
+
const longName = 'getVeryLongOperationNameWithManyWords';
|
|
259
|
+
expect(transformNamingConvention(longName, 'kebab-case')).toBe('get-very-long-operation-name-with-many-words');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"lib": ["ES2022", "DOM"],
|
|
14
14
|
"module": "ESNext",
|
|
15
15
|
"moduleResolution": "Bundler",
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"declarationMap": true,
|
|
16
18
|
"noEmit": false,
|
|
17
19
|
"noFallthroughCasesInSwitch": true,
|
|
18
20
|
"noImplicitAny": true,
|
|
@@ -39,6 +41,6 @@
|
|
|
39
41
|
"useUnknownInCatchVariables": true,
|
|
40
42
|
"verbatimModuleSyntax": true
|
|
41
43
|
},
|
|
42
|
-
"exclude": ["node_modules", "dist", "build", "examples/**/*.ts"],
|
|
44
|
+
"exclude": ["node_modules", "dist", "build", "examples/**/*.ts", "tests/**/*.ts", "scripts/**/*.ts"],
|
|
43
45
|
"include": ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "vitest.config.ts"]
|
|
44
46
|
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm install:*)",
|
|
5
|
-
"Bash(npm uninstall:*)",
|
|
6
|
-
"Bash(npm run type-check:*)",
|
|
7
|
-
"Bash(npm run build:*)",
|
|
8
|
-
"Bash(./dist/src/cli.js --help)",
|
|
9
|
-
"Bash(./dist/src/cli.js --version)",
|
|
10
|
-
"Bash(npm run dev:*)",
|
|
11
|
-
"Bash(node:*)",
|
|
12
|
-
"Bash(npm run lint)",
|
|
13
|
-
"Bash(npx husky init:*)",
|
|
14
|
-
"Bash(chmod:*)",
|
|
15
|
-
"Bash(npm run validate:*)",
|
|
16
|
-
"Bash(npm run format:*)",
|
|
17
|
-
"Bash(npm run test:*)",
|
|
18
|
-
"Bash(npm test)",
|
|
19
|
-
"Bash(npm audit:*)",
|
|
20
|
-
"Bash(npx depcheck:*)",
|
|
21
|
-
"Bash(npm outdated)",
|
|
22
|
-
"Bash(npm update:*)",
|
|
23
|
-
"WebSearch",
|
|
24
|
-
"Bash(find:*)",
|
|
25
|
-
"Bash(npx zod-codegen:*)",
|
|
26
|
-
"Bash(DEBUG=1 node ./dist/src/cli.js --input ./samples/openapi.json --output ./test-generated)",
|
|
27
|
-
"Bash(rm:*)",
|
|
28
|
-
"Bash(npm outdated:*)",
|
|
29
|
-
"Bash(npm view:*)",
|
|
30
|
-
"Bash(git tag:*)",
|
|
31
|
-
"Bash(npm run release:*)",
|
|
32
|
-
"Bash(git log:*)",
|
|
33
|
-
"Bash(npx commitlint:*)",
|
|
34
|
-
"Bash(git rev-list:*)",
|
|
35
|
-
"Bash(./fix-commit-messages.sh:*)",
|
|
36
|
-
"Bash(git add:*)",
|
|
37
|
-
"Bash(git commit:*)",
|
|
38
|
-
"Bash(FILTER_BRANCH_SQUELCH_WARNING=1 ./fix-commit-messages.sh)"
|
|
39
|
-
],
|
|
40
|
-
"deny": [],
|
|
41
|
-
"ask": []
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
/**
|
|
5
|
-
* Type guard for the package.json object
|
|
6
|
-
*
|
|
7
|
-
* @param input Unknown input
|
|
8
|
-
* @returns true if the input is an event object
|
|
9
|
-
*/
|
|
10
|
-
export function isPackageJson(input) {
|
|
11
|
-
const event = z
|
|
12
|
-
.object({
|
|
13
|
-
name: z.string(),
|
|
14
|
-
version: z.string(),
|
|
15
|
-
description: z.string(),
|
|
16
|
-
})
|
|
17
|
-
.strict()
|
|
18
|
-
.catchall(z.any())
|
|
19
|
-
.required();
|
|
20
|
-
const { success } = event.safeParse(input);
|
|
21
|
-
return success;
|
|
22
|
-
}
|
|
23
|
-
const sourcePath = resolve(__dirname, '..', 'package.json');
|
|
24
|
-
const data = JSON.parse(readFileSync(sourcePath, 'utf8'));
|
|
25
|
-
if (!isPackageJson(data)) {
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
const { name, version, description } = data;
|
|
29
|
-
const targetPath = resolve(__dirname, '..', 'src', 'assets', 'manifest.json');
|
|
30
|
-
writeFileSync(targetPath, JSON.stringify({ name, version, description }, null, 2));
|
|
31
|
-
process.exit(0);
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
describe('CLI Integration', () => {
|
|
5
|
-
describe('--help', () => {
|
|
6
|
-
it('should display help information', () => {
|
|
7
|
-
const result = execSync('yarn build && node ./dist/src/cli.js --help', {
|
|
8
|
-
encoding: 'utf-8',
|
|
9
|
-
cwd: resolve(__dirname, '../..'),
|
|
10
|
-
});
|
|
11
|
-
expect(result).toContain('Usage:');
|
|
12
|
-
expect(result).toContain('--input');
|
|
13
|
-
expect(result).toContain('--output');
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
describe('--version', () => {
|
|
17
|
-
it('should display version information', () => {
|
|
18
|
-
const result = execSync('yarn build && node ./dist/src/cli.js --version', {
|
|
19
|
-
encoding: 'utf-8',
|
|
20
|
-
cwd: resolve(__dirname, '../..'),
|
|
21
|
-
});
|
|
22
|
-
expect(result).toMatch(/\d+\.\d+\.\d+/);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
});
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { TypeScriptCodeGeneratorService } from '../../src/services/code-generator.service.js';
|
|
3
|
-
describe('TypeScriptCodeGeneratorService', () => {
|
|
4
|
-
let generator;
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
generator = new TypeScriptCodeGeneratorService();
|
|
7
|
-
});
|
|
8
|
-
describe('generate', () => {
|
|
9
|
-
it('should generate code for a minimal OpenAPI spec', () => {
|
|
10
|
-
const spec = {
|
|
11
|
-
openapi: '3.0.0',
|
|
12
|
-
info: {
|
|
13
|
-
title: 'Test API',
|
|
14
|
-
version: '1.0.0',
|
|
15
|
-
},
|
|
16
|
-
paths: {},
|
|
17
|
-
};
|
|
18
|
-
const code = generator.generate(spec);
|
|
19
|
-
expect(code).toBeTruthy();
|
|
20
|
-
expect(typeof code).toBe('string');
|
|
21
|
-
expect(code).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
22
|
-
expect(code).toContain('export class');
|
|
23
|
-
});
|
|
24
|
-
it('should generate schemas for component schemas', () => {
|
|
25
|
-
const spec = {
|
|
26
|
-
openapi: '3.0.0',
|
|
27
|
-
info: {
|
|
28
|
-
title: 'Test API',
|
|
29
|
-
version: '1.0.0',
|
|
30
|
-
},
|
|
31
|
-
paths: {},
|
|
32
|
-
components: {
|
|
33
|
-
schemas: {
|
|
34
|
-
User: {
|
|
35
|
-
type: 'object',
|
|
36
|
-
properties: {
|
|
37
|
-
id: { type: 'integer' },
|
|
38
|
-
name: { type: 'string' },
|
|
39
|
-
},
|
|
40
|
-
required: ['id', 'name'],
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
const code = generator.generate(spec);
|
|
46
|
-
expect(code).toContain('export const User');
|
|
47
|
-
expect(code).toContain('z.object');
|
|
48
|
-
expect(code).toContain('z.number().int()');
|
|
49
|
-
expect(code).toContain('z.string()');
|
|
50
|
-
});
|
|
51
|
-
it('should generate client methods for paths', () => {
|
|
52
|
-
const spec = {
|
|
53
|
-
openapi: '3.0.0',
|
|
54
|
-
info: {
|
|
55
|
-
title: 'Test API',
|
|
56
|
-
version: '1.0.0',
|
|
57
|
-
},
|
|
58
|
-
paths: {
|
|
59
|
-
'/users': {
|
|
60
|
-
get: {
|
|
61
|
-
operationId: 'getUsers',
|
|
62
|
-
responses: {
|
|
63
|
-
'200': {
|
|
64
|
-
description: 'Success',
|
|
65
|
-
content: {
|
|
66
|
-
'application/json': {
|
|
67
|
-
schema: {
|
|
68
|
-
type: 'array',
|
|
69
|
-
items: { type: 'string' },
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
const code = generator.generate(spec);
|
|
80
|
-
expect(code).toContain('async getUsers');
|
|
81
|
-
expect(code).toContain('#makeRequest');
|
|
82
|
-
});
|
|
83
|
-
it('should generate getBaseRequestOptions method', () => {
|
|
84
|
-
const spec = {
|
|
85
|
-
openapi: '3.0.0',
|
|
86
|
-
info: {
|
|
87
|
-
title: 'Test API',
|
|
88
|
-
version: '1.0.0',
|
|
89
|
-
},
|
|
90
|
-
paths: {},
|
|
91
|
-
};
|
|
92
|
-
const code = generator.generate(spec);
|
|
93
|
-
expect(code).toContain('protected getBaseRequestOptions');
|
|
94
|
-
expect(code).toContain('Partial<Omit<RequestInit');
|
|
95
|
-
});
|
|
96
|
-
it('should handle servers configuration', () => {
|
|
97
|
-
const spec = {
|
|
98
|
-
openapi: '3.0.0',
|
|
99
|
-
info: {
|
|
100
|
-
title: 'Test API',
|
|
101
|
-
version: '1.0.0',
|
|
102
|
-
},
|
|
103
|
-
servers: [{ url: 'https://api.example.com' }],
|
|
104
|
-
paths: {},
|
|
105
|
-
};
|
|
106
|
-
const code = generator.generate(spec);
|
|
107
|
-
expect(code).toContain('defaultBaseUrl');
|
|
108
|
-
expect(code).toContain('https://api.example.com');
|
|
109
|
-
});
|
|
110
|
-
it('should handle complex schemas with references', () => {
|
|
111
|
-
const spec = {
|
|
112
|
-
openapi: '3.0.0',
|
|
113
|
-
info: {
|
|
114
|
-
title: 'Test API',
|
|
115
|
-
version: '1.0.0',
|
|
116
|
-
},
|
|
117
|
-
paths: {},
|
|
118
|
-
components: {
|
|
119
|
-
schemas: {
|
|
120
|
-
User: {
|
|
121
|
-
type: 'object',
|
|
122
|
-
properties: {
|
|
123
|
-
id: { type: 'integer' },
|
|
124
|
-
profile: { $ref: '#/components/schemas/Profile' },
|
|
125
|
-
},
|
|
126
|
-
required: ['id'],
|
|
127
|
-
},
|
|
128
|
-
Profile: {
|
|
129
|
-
type: 'object',
|
|
130
|
-
properties: {
|
|
131
|
-
name: { type: 'string' },
|
|
132
|
-
email: { type: 'string', format: 'email' },
|
|
133
|
-
},
|
|
134
|
-
required: ['name', 'email'],
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
const code = generator.generate(spec);
|
|
140
|
-
expect(code).toContain('export const User');
|
|
141
|
-
expect(code).toContain('export const Profile');
|
|
142
|
-
// Profile should be defined before User (topological sort)
|
|
143
|
-
const profileIndex = code.indexOf('Profile');
|
|
144
|
-
const userIndex = code.indexOf('User');
|
|
145
|
-
expect(profileIndex).toBeLessThan(userIndex);
|
|
146
|
-
});
|
|
147
|
-
it('should handle enum types', () => {
|
|
148
|
-
const spec = {
|
|
149
|
-
openapi: '3.0.0',
|
|
150
|
-
info: {
|
|
151
|
-
title: 'Test API',
|
|
152
|
-
version: '1.0.0',
|
|
153
|
-
},
|
|
154
|
-
paths: {},
|
|
155
|
-
components: {
|
|
156
|
-
schemas: {
|
|
157
|
-
Status: {
|
|
158
|
-
type: 'string',
|
|
159
|
-
enum: ['active', 'inactive', 'pending'],
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
const code = generator.generate(spec);
|
|
165
|
-
expect(code).toContain('z.enum');
|
|
166
|
-
expect(code).toContain('active');
|
|
167
|
-
expect(code).toContain('inactive');
|
|
168
|
-
expect(code).toContain('pending');
|
|
169
|
-
});
|
|
170
|
-
it('should handle numeric enum types with z.union and z.literal', () => {
|
|
171
|
-
const spec = {
|
|
172
|
-
openapi: '3.0.0',
|
|
173
|
-
info: {
|
|
174
|
-
title: 'Test API',
|
|
175
|
-
version: '1.0.0',
|
|
176
|
-
},
|
|
177
|
-
paths: {},
|
|
178
|
-
components: {
|
|
179
|
-
schemas: {
|
|
180
|
-
Status: {
|
|
181
|
-
type: 'integer',
|
|
182
|
-
enum: [-99, 0, 1, 2],
|
|
183
|
-
},
|
|
184
|
-
ExecutionMode: {
|
|
185
|
-
type: 'integer',
|
|
186
|
-
enum: [1, 2],
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
const code = generator.generate(spec);
|
|
192
|
-
// Numeric enums should use z.union([z.literal(...), ...])
|
|
193
|
-
expect(code).toContain('z.union');
|
|
194
|
-
expect(code).toContain('z.literal');
|
|
195
|
-
expect(code).toContain('-99');
|
|
196
|
-
expect(code).toContain('0');
|
|
197
|
-
expect(code).toContain('1');
|
|
198
|
-
expect(code).toContain('2');
|
|
199
|
-
// Should not use z.enum for numeric enums
|
|
200
|
-
expect(code).not.toContain('Status: z.enum');
|
|
201
|
-
expect(code).not.toContain('ExecutionMode: z.enum');
|
|
202
|
-
});
|
|
203
|
-
it('should merge baseOptions with request-specific options in #makeRequest', () => {
|
|
204
|
-
const spec = {
|
|
205
|
-
openapi: '3.0.0',
|
|
206
|
-
info: {
|
|
207
|
-
title: 'Test API',
|
|
208
|
-
version: '1.0.0',
|
|
209
|
-
},
|
|
210
|
-
paths: {
|
|
211
|
-
'/test': {
|
|
212
|
-
get: {
|
|
213
|
-
operationId: 'testEndpoint',
|
|
214
|
-
responses: {
|
|
215
|
-
'200': {
|
|
216
|
-
description: 'Success',
|
|
217
|
-
content: {
|
|
218
|
-
'application/json': {
|
|
219
|
-
schema: { type: 'string' },
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
const code = generator.generate(spec);
|
|
229
|
-
// Should call getBaseRequestOptions()
|
|
230
|
-
expect(code).toContain('getBaseRequestOptions()');
|
|
231
|
-
// Should merge headers: baseHeaders + Content-Type + request headers
|
|
232
|
-
expect(code).toContain('Object.assign');
|
|
233
|
-
expect(code).toContain('baseHeaders');
|
|
234
|
-
expect(code).toContain('Content-Type');
|
|
235
|
-
// Should merge all options: baseOptions + {method, headers, body}
|
|
236
|
-
expect(code).toMatch(/Object\.assign\s*\(\s*\{\s*\}\s*,\s*baseOptions/);
|
|
237
|
-
expect(code).toContain('method');
|
|
238
|
-
expect(code).toContain('headers');
|
|
239
|
-
expect(code).toContain('body');
|
|
240
|
-
});
|
|
241
|
-
it('should handle array types', () => {
|
|
242
|
-
const spec = {
|
|
243
|
-
openapi: '3.0.0',
|
|
244
|
-
info: {
|
|
245
|
-
title: 'Test API',
|
|
246
|
-
version: '1.0.0',
|
|
247
|
-
},
|
|
248
|
-
paths: {},
|
|
249
|
-
components: {
|
|
250
|
-
schemas: {
|
|
251
|
-
Tags: {
|
|
252
|
-
type: 'array',
|
|
253
|
-
items: { type: 'string' },
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
const code = generator.generate(spec);
|
|
259
|
-
expect(code).toContain('z.array');
|
|
260
|
-
expect(code).toContain('z.string()');
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
describe('buildSchema', () => {
|
|
264
|
-
it('should build schema for string type', () => {
|
|
265
|
-
const schema = { type: 'string' };
|
|
266
|
-
const result = generator.buildSchema(schema, true);
|
|
267
|
-
expect(result).toBeDefined();
|
|
268
|
-
});
|
|
269
|
-
it('should build schema for number type', () => {
|
|
270
|
-
const schema = { type: 'number' };
|
|
271
|
-
const result = generator.buildSchema(schema, true);
|
|
272
|
-
expect(result).toBeDefined();
|
|
273
|
-
});
|
|
274
|
-
it('should build schema for boolean type', () => {
|
|
275
|
-
const schema = { type: 'boolean' };
|
|
276
|
-
const result = generator.buildSchema(schema, true);
|
|
277
|
-
expect(result).toBeDefined();
|
|
278
|
-
});
|
|
279
|
-
it('should handle optional fields', () => {
|
|
280
|
-
const schema = { type: 'string' };
|
|
281
|
-
const result = generator.buildSchema(schema, false);
|
|
282
|
-
expect(result).toBeDefined();
|
|
283
|
-
});
|
|
284
|
-
it('should handle string formats', () => {
|
|
285
|
-
const schema = { type: 'string', format: 'email' };
|
|
286
|
-
const result = generator.buildSchema(schema, true);
|
|
287
|
-
expect(result).toBeDefined();
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
});
|