zod-codegen 1.0.3 → 1.1.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/CHANGELOG.md +13 -0
- package/CONTRIBUTING.md +1 -1
- package/EXAMPLES.md +730 -0
- package/PERFORMANCE.md +59 -0
- package/README.md +272 -58
- package/dist/src/services/code-generator.service.js +249 -45
- package/dist/src/types/openapi.js +1 -1
- package/dist/tests/unit/code-generator.test.js +290 -0
- package/dist/tests/unit/file-reader.test.js +110 -0
- package/dist/tests/unit/generator.test.js +77 -7
- package/eslint.config.mjs +1 -0
- package/examples/.gitkeep +3 -0
- package/examples/README.md +74 -0
- package/examples/petstore/README.md +121 -0
- package/examples/petstore/authenticated-usage.ts +60 -0
- package/examples/petstore/basic-usage.ts +51 -0
- package/examples/petstore/server-variables-usage.ts +63 -0
- package/examples/petstore/type.ts +217 -0
- package/examples/pokeapi/README.md +105 -0
- package/examples/pokeapi/basic-usage.ts +57 -0
- package/examples/pokeapi/custom-client.ts +56 -0
- package/examples/pokeapi/type.ts +109 -0
- package/package.json +4 -2
- package/samples/pokeapi-openapi.json +212 -0
- package/samples/server-variables-example.yaml +49 -0
- package/src/services/code-generator.service.ts +707 -88
- package/src/types/openapi.ts +1 -1
- package/tests/unit/code-generator.test.ts +321 -0
- package/tests/unit/file-reader.test.ts +134 -0
- package/tests/unit/generator.test.ts +99 -7
- package/tsconfig.examples.json +17 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { SyncFileReaderService, OpenApiFileParserService } from '../../src/services/file-reader.service.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
describe('SyncFileReaderService', () => {
|
|
8
|
+
let reader;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
reader = new SyncFileReaderService();
|
|
11
|
+
});
|
|
12
|
+
describe('readFile', () => {
|
|
13
|
+
it('should read local JSON files', async () => {
|
|
14
|
+
const content = await reader.readFile(join(__dirname, '../../samples/openapi.json'));
|
|
15
|
+
expect(content).toBeTruthy();
|
|
16
|
+
expect(typeof content).toBe('string');
|
|
17
|
+
});
|
|
18
|
+
it('should read local YAML files', async () => {
|
|
19
|
+
const content = await reader.readFile(join(__dirname, '../../samples/swagger-petstore.yaml'));
|
|
20
|
+
expect(content).toBeTruthy();
|
|
21
|
+
expect(typeof content).toBe('string');
|
|
22
|
+
expect(content).toContain('openapi:');
|
|
23
|
+
});
|
|
24
|
+
it('should handle non-existent files', async () => {
|
|
25
|
+
await expect(reader.readFile('./non-existent-file.json')).rejects.toThrow();
|
|
26
|
+
});
|
|
27
|
+
it('should handle URLs', async () => {
|
|
28
|
+
// Mock fetch for URL test
|
|
29
|
+
const originalFetch = global.fetch;
|
|
30
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
31
|
+
ok: true,
|
|
32
|
+
text: async () => '{"openapi": "3.0.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": {}}',
|
|
33
|
+
});
|
|
34
|
+
const content = await reader.readFile('https://example.com/openapi.json');
|
|
35
|
+
expect(content).toBeTruthy();
|
|
36
|
+
global.fetch = originalFetch;
|
|
37
|
+
});
|
|
38
|
+
it('should handle URL fetch errors with non-ok responses', async () => {
|
|
39
|
+
const originalFetch = global.fetch;
|
|
40
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
41
|
+
ok: false,
|
|
42
|
+
status: 404,
|
|
43
|
+
statusText: 'Not Found',
|
|
44
|
+
});
|
|
45
|
+
global.fetch = mockFetch;
|
|
46
|
+
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
|
|
47
|
+
// Verify fetch was called
|
|
48
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com/not-found.json');
|
|
49
|
+
global.fetch = originalFetch;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('OpenApiFileParserService', () => {
|
|
54
|
+
let parser;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
parser = new OpenApiFileParserService();
|
|
57
|
+
});
|
|
58
|
+
describe('parse', () => {
|
|
59
|
+
it('should parse valid JSON OpenAPI spec', () => {
|
|
60
|
+
const jsonSpec = JSON.stringify({
|
|
61
|
+
openapi: '3.0.0',
|
|
62
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
63
|
+
paths: {},
|
|
64
|
+
});
|
|
65
|
+
const result = parser.parse(jsonSpec);
|
|
66
|
+
expect(result).toBeDefined();
|
|
67
|
+
expect(result.openapi).toBe('3.0.0');
|
|
68
|
+
expect(result.info.title).toBe('Test API');
|
|
69
|
+
});
|
|
70
|
+
it('should parse valid YAML OpenAPI spec', () => {
|
|
71
|
+
const yamlSpec = `
|
|
72
|
+
openapi: 3.0.0
|
|
73
|
+
info:
|
|
74
|
+
title: Test API
|
|
75
|
+
version: 1.0.0
|
|
76
|
+
paths: {}
|
|
77
|
+
`;
|
|
78
|
+
const result = parser.parse(yamlSpec);
|
|
79
|
+
expect(result).toBeDefined();
|
|
80
|
+
expect(result.openapi).toBe('3.0.0');
|
|
81
|
+
expect(result.info.title).toBe('Test API');
|
|
82
|
+
});
|
|
83
|
+
it('should parse already parsed objects', () => {
|
|
84
|
+
const spec = {
|
|
85
|
+
openapi: '3.0.0',
|
|
86
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
87
|
+
paths: {},
|
|
88
|
+
};
|
|
89
|
+
const result = parser.parse(spec);
|
|
90
|
+
expect(result).toBeDefined();
|
|
91
|
+
expect(result.openapi).toBe('3.0.0');
|
|
92
|
+
});
|
|
93
|
+
it('should validate OpenAPI structure', () => {
|
|
94
|
+
const invalidSpec = {
|
|
95
|
+
openapi: '2.0.0', // Wrong version format
|
|
96
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
97
|
+
paths: {},
|
|
98
|
+
};
|
|
99
|
+
expect(() => parser.parse(invalidSpec)).toThrow();
|
|
100
|
+
});
|
|
101
|
+
it('should handle missing required fields', () => {
|
|
102
|
+
const invalidSpec = {
|
|
103
|
+
openapi: '3.0.0',
|
|
104
|
+
// Missing info
|
|
105
|
+
paths: {},
|
|
106
|
+
};
|
|
107
|
+
expect(() => parser.parse(invalidSpec)).toThrow();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -1,30 +1,100 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { Generator } from '../../src/generator.js';
|
|
3
3
|
import { Reporter } from '../../src/utils/reporter.js';
|
|
4
|
+
import { readFileSync, existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const testOutputDir = join(__dirname, '../../test-output');
|
|
4
10
|
describe('Generator', () => {
|
|
5
11
|
let generator;
|
|
6
12
|
let mockReporter;
|
|
7
13
|
beforeEach(() => {
|
|
14
|
+
// Clean up test output directory
|
|
15
|
+
if (existsSync(testOutputDir)) {
|
|
16
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
mkdirSync(testOutputDir, { recursive: true });
|
|
8
19
|
// Create a mock reporter
|
|
9
20
|
mockReporter = {
|
|
10
|
-
|
|
21
|
+
log: vi.fn(),
|
|
11
22
|
error: vi.fn(),
|
|
12
|
-
warn: vi.fn(),
|
|
13
|
-
success: vi.fn(),
|
|
14
23
|
};
|
|
15
|
-
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Clean up test output directory
|
|
27
|
+
if (existsSync(testOutputDir)) {
|
|
28
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
16
30
|
});
|
|
17
31
|
describe('constructor', () => {
|
|
18
32
|
it('should create a new Generator instance', () => {
|
|
33
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
19
34
|
expect(generator).toBeInstanceOf(Generator);
|
|
20
35
|
});
|
|
36
|
+
it('should initialize with correct parameters', () => {
|
|
37
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
38
|
+
expect(generator).toBeDefined();
|
|
39
|
+
});
|
|
21
40
|
});
|
|
22
41
|
describe('run', () => {
|
|
23
42
|
it('should be a function', () => {
|
|
43
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
24
44
|
expect(typeof generator.run).toBe('function');
|
|
25
45
|
});
|
|
26
|
-
it('should
|
|
27
|
-
|
|
46
|
+
it('should generate code from a valid OpenAPI file', async () => {
|
|
47
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
48
|
+
const exitCode = await generator.run();
|
|
49
|
+
expect(exitCode).toBe(0);
|
|
50
|
+
expect(mockReporter.log).toHaveBeenCalled();
|
|
51
|
+
expect(mockReporter.error).not.toHaveBeenCalled();
|
|
52
|
+
// Verify output file was created
|
|
53
|
+
const outputFile = join(testOutputDir, 'type.ts');
|
|
54
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
55
|
+
// Verify file contains expected content
|
|
56
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
57
|
+
expect(content).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
58
|
+
expect(content).toContain('export class');
|
|
59
|
+
expect(content).toContain('getBaseRequestOptions');
|
|
60
|
+
});
|
|
61
|
+
it('should handle invalid file paths gracefully', async () => {
|
|
62
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './non-existent-file.yaml', testOutputDir);
|
|
63
|
+
const exitCode = await generator.run();
|
|
64
|
+
expect(exitCode).toBe(1);
|
|
65
|
+
expect(mockReporter.error).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
it('should handle invalid OpenAPI specifications', async () => {
|
|
68
|
+
// Create a temporary invalid OpenAPI file
|
|
69
|
+
const invalidFile = join(testOutputDir, 'invalid.yaml');
|
|
70
|
+
mkdirSync(testOutputDir, { recursive: true });
|
|
71
|
+
readFileSync; // Ensure we can write
|
|
72
|
+
const { writeFileSync } = await import('node:fs');
|
|
73
|
+
writeFileSync(invalidFile, 'invalid: yaml: content: [unclosed');
|
|
74
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, invalidFile, testOutputDir);
|
|
75
|
+
const exitCode = await generator.run();
|
|
76
|
+
expect(exitCode).toBe(1);
|
|
77
|
+
expect(mockReporter.error).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
it('should generate code with correct structure', async () => {
|
|
80
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
81
|
+
await generator.run();
|
|
82
|
+
const outputFile = join(testOutputDir, 'type.ts');
|
|
83
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
84
|
+
// Check for key components
|
|
85
|
+
expect(content).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
86
|
+
expect(content).toContain('export const');
|
|
87
|
+
expect(content).toContain('protected getBaseRequestOptions');
|
|
88
|
+
expect(content).toContain('async #makeRequest');
|
|
89
|
+
});
|
|
90
|
+
it('should include header comments in generated file', async () => {
|
|
91
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
92
|
+
await generator.run();
|
|
93
|
+
const outputFile = join(testOutputDir, 'type.ts');
|
|
94
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
95
|
+
expect(content).toContain('AUTOGENERATED');
|
|
96
|
+
expect(content).toContain('test-app@1.0.0');
|
|
97
|
+
expect(content).toContain('eslint-disable');
|
|
28
98
|
});
|
|
29
99
|
});
|
|
30
100
|
});
|
package/eslint.config.mjs
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
This directory contains example implementations demonstrating how to use `zod-codegen` with real-world OpenAPI specifications.
|
|
4
|
+
|
|
5
|
+
## Available Examples
|
|
6
|
+
|
|
7
|
+
### 🐾 [Petstore API](./petstore/)
|
|
8
|
+
|
|
9
|
+
The Swagger Petstore API is a well-known example API that demonstrates various OpenAPI features. This example shows:
|
|
10
|
+
|
|
11
|
+
- Basic client usage
|
|
12
|
+
- Authentication with API keys
|
|
13
|
+
- Extending the client for custom headers
|
|
14
|
+
- Working with pets, orders, and users
|
|
15
|
+
|
|
16
|
+
**Generate the client:**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
zod-codegen --input ./samples/swagger-petstore.yaml --output ./examples/petstore
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Run examples:**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx ts-node examples/petstore/basic-usage.ts
|
|
26
|
+
npx ts-node examples/petstore/authenticated-usage.ts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### ⚡ [PokéAPI](./pokeapi/)
|
|
30
|
+
|
|
31
|
+
PokéAPI is a public RESTful API that provides data about Pokémon. This example demonstrates:
|
|
32
|
+
|
|
33
|
+
- Working with a real-world public API
|
|
34
|
+
- Fetching Pokémon data
|
|
35
|
+
- Custom client configuration
|
|
36
|
+
|
|
37
|
+
**Generate the client:**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
zod-codegen --input https://pokeapi.co/api/v2/openapi.json --output ./examples/pokeapi
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Structure
|
|
44
|
+
|
|
45
|
+
Each example directory contains:
|
|
46
|
+
|
|
47
|
+
- `type.ts` - Generated client and schemas (created by zod-codegen)
|
|
48
|
+
- `README.md` - Example-specific documentation
|
|
49
|
+
- `basic-usage.ts` - Basic usage examples
|
|
50
|
+
- `authenticated-usage.ts` - Authentication examples (if applicable)
|
|
51
|
+
|
|
52
|
+
## Getting Started
|
|
53
|
+
|
|
54
|
+
1. **Choose an example** that matches your use case
|
|
55
|
+
2. **Generate the client** using the command shown in the example's README
|
|
56
|
+
3. **Review the generated code** in `type.ts`
|
|
57
|
+
4. **Run the example scripts** to see it in action
|
|
58
|
+
5. **Extend the client** using patterns from [EXAMPLES.md](../EXAMPLES.md)
|
|
59
|
+
|
|
60
|
+
## Learning Path
|
|
61
|
+
|
|
62
|
+
1. Start with **Petstore** to understand basic concepts
|
|
63
|
+
2. Try **PokéAPI** to see a real-world public API
|
|
64
|
+
3. Read [EXAMPLES.md](../EXAMPLES.md) for advanced patterns
|
|
65
|
+
4. Create your own example with your API!
|
|
66
|
+
|
|
67
|
+
## Contributing Examples
|
|
68
|
+
|
|
69
|
+
If you'd like to add an example:
|
|
70
|
+
|
|
71
|
+
1. Create a new directory under `examples/`
|
|
72
|
+
2. Add a `README.md` explaining the example
|
|
73
|
+
3. Include example TypeScript files
|
|
74
|
+
4. Update this README to list your example
|