zod-codegen 1.7.0 → 1.7.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 +6 -0
- package/dist/src/services/file-reader.service.d.ts +2 -0
- package/dist/src/services/file-reader.service.d.ts.map +1 -1
- package/dist/src/services/file-reader.service.js +24 -10
- package/dist/tests/integration/cli-comprehensive.test.js +13 -0
- package/dist/tests/unit/file-reader.test.js +35 -7
- package/package.json +1 -1
- package/src/services/file-reader.service.ts +30 -11
- package/tests/integration/cli-comprehensive.test.ts +16 -0
- package/tests/unit/file-reader.test.ts +47 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [1.7.1](https://github.com/julienandreu/zod-codegen/compare/v1.7.0...v1.7.1) (2026-03-10)
|
|
2
|
+
|
|
3
|
+
### 🐛 Bug Fixes
|
|
4
|
+
|
|
5
|
+
- properly handle URL inputs with authentication support ([#87](https://github.com/julienandreu/zod-codegen/issues/87)) ([49ba2c4](https://github.com/julienandreu/zod-codegen/commit/49ba2c483458397d9f12e03b8f6c634981e4454a))
|
|
6
|
+
|
|
1
7
|
## [1.7.0](https://github.com/julienandreu/zod-codegen/compare/v1.6.3...v1.7.0) (2026-03-09)
|
|
2
8
|
|
|
3
9
|
### 🚀 Features
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { OpenApiFileParser, OpenApiFileReader } from '../interfaces/file-reader';
|
|
2
2
|
import type { OpenApiSpecType } from '../types/openapi';
|
|
3
3
|
export declare class SyncFileReaderService implements OpenApiFileReader {
|
|
4
|
+
private isUrl;
|
|
4
5
|
readFile(path: string): Promise<string>;
|
|
6
|
+
private fetchUrl;
|
|
5
7
|
}
|
|
6
8
|
export declare class OpenApiFileParserService implements OpenApiFileParser<OpenApiSpecType> {
|
|
7
9
|
parse(input: unknown): OpenApiSpecType;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-reader.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-reader.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,qBAAa,qBAAsB,YAAW,iBAAiB;
|
|
1
|
+
{"version":3,"file":"file-reader.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-reader.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,qBAAa,qBAAsB,YAAW,iBAAiB;IAC7D,OAAO,CAAC,KAAK;IASP,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAQ/B,QAAQ;CAkBvB;AAED,qBAAa,wBAAyB,YAAW,iBAAiB,CAAC,eAAe,CAAC;IACjF,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe;CAgBvC"}
|
|
@@ -2,21 +2,35 @@ import { load } from 'js-yaml';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { OpenApiSpec } from '../types/openapi.js';
|
|
4
4
|
export class SyncFileReaderService {
|
|
5
|
-
|
|
6
|
-
// Check if path is a URL
|
|
5
|
+
isUrl(path) {
|
|
7
6
|
try {
|
|
8
7
|
const url = new URL(path);
|
|
9
|
-
|
|
10
|
-
const response = await fetch(url.toString());
|
|
11
|
-
if (!response.ok) {
|
|
12
|
-
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
13
|
-
}
|
|
14
|
-
return await response.text();
|
|
8
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
15
9
|
}
|
|
16
10
|
catch {
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async readFile(path) {
|
|
15
|
+
if (this.isUrl(path)) {
|
|
16
|
+
return await this.fetchUrl(path);
|
|
17
|
+
}
|
|
18
|
+
return readFileSync(path, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
async fetchUrl(path) {
|
|
21
|
+
const url = new URL(path);
|
|
22
|
+
const headers = {};
|
|
23
|
+
if (url.username || url.password) {
|
|
24
|
+
const credentials = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
|
|
25
|
+
headers['Authorization'] = `Basic ${btoa(credentials)}`;
|
|
26
|
+
url.username = '';
|
|
27
|
+
url.password = '';
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(url.toString(), { headers });
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
19
32
|
}
|
|
33
|
+
return await response.text();
|
|
20
34
|
}
|
|
21
35
|
}
|
|
22
36
|
export class OpenApiFileParserService {
|
|
@@ -106,5 +106,18 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
106
106
|
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
107
107
|
expect(existsSync(outputFile)).toBe(true);
|
|
108
108
|
});
|
|
109
|
+
it('should generate code from a remote URL', () => {
|
|
110
|
+
execSync(`node ./dist/src/cli.js --input https://petstore3.swagger.io/api/v3/openapi.json --output ${testOutputDir}`, {
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
cwd,
|
|
113
|
+
timeout: 30000
|
|
114
|
+
});
|
|
115
|
+
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
116
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
117
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
118
|
+
expect(content).toContain('export default class');
|
|
119
|
+
expect(content).toContain('class ResponseValidationError<T> extends Error');
|
|
120
|
+
expect(content).toContain('import { z }');
|
|
121
|
+
});
|
|
109
122
|
});
|
|
110
123
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
4
|
import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname } from 'node:path';
|
|
6
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
6
|
describe('SyncFileReaderService', () => {
|
|
8
7
|
let reader;
|
|
@@ -25,7 +24,6 @@ describe('SyncFileReaderService', () => {
|
|
|
25
24
|
await expect(reader.readFile('./non-existent-file.json')).rejects.toThrow();
|
|
26
25
|
});
|
|
27
26
|
it('should handle URLs', async () => {
|
|
28
|
-
// Mock fetch for URL test
|
|
29
27
|
const originalFetch = global.fetch;
|
|
30
28
|
global.fetch = vi.fn().mockResolvedValue({
|
|
31
29
|
ok: true,
|
|
@@ -33,6 +31,7 @@ describe('SyncFileReaderService', () => {
|
|
|
33
31
|
});
|
|
34
32
|
const content = await reader.readFile('https://example.com/openapi.json');
|
|
35
33
|
expect(content).toBeTruthy();
|
|
34
|
+
expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
|
|
36
35
|
global.fetch = originalFetch;
|
|
37
36
|
});
|
|
38
37
|
it('should handle URL fetch errors with non-ok responses', async () => {
|
|
@@ -43,11 +42,40 @@ describe('SyncFileReaderService', () => {
|
|
|
43
42
|
statusText: 'Not Found'
|
|
44
43
|
});
|
|
45
44
|
global.fetch = mockFetch;
|
|
46
|
-
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow('Failed to fetch');
|
|
46
|
+
global.fetch = originalFetch;
|
|
47
|
+
});
|
|
48
|
+
it('should propagate network errors for URLs instead of falling back to readFileSync', async () => {
|
|
49
|
+
const originalFetch = global.fetch;
|
|
50
|
+
global.fetch = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
|
|
51
|
+
await expect(reader.readFile('https://example.com/openapi.json')).rejects.toThrow('fetch failed');
|
|
52
|
+
global.fetch = originalFetch;
|
|
53
|
+
});
|
|
54
|
+
it('should extract basic auth credentials from URL into Authorization header', async () => {
|
|
55
|
+
const originalFetch = global.fetch;
|
|
56
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
57
|
+
ok: true,
|
|
58
|
+
text: async () => '{}'
|
|
59
|
+
});
|
|
60
|
+
global.fetch = mockFetch;
|
|
61
|
+
await reader.readFile('https://user:pass@example.com/openapi.json');
|
|
62
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: { Authorization: `Basic ${btoa('user:pass')}` } });
|
|
63
|
+
global.fetch = originalFetch;
|
|
64
|
+
});
|
|
65
|
+
it('should decode percent-encoded credentials from URL', async () => {
|
|
66
|
+
const originalFetch = global.fetch;
|
|
67
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
text: async () => '{}'
|
|
70
|
+
});
|
|
71
|
+
global.fetch = mockFetch;
|
|
72
|
+
await reader.readFile('https://julien%2Bstaging%40saris.ai:qwerty123@api.staging.saris.ai/api/openapi.json');
|
|
73
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.staging.saris.ai/api/openapi.json', { headers: { Authorization: `Basic ${btoa('julien+staging@saris.ai:qwerty123')}` } });
|
|
49
74
|
global.fetch = originalFetch;
|
|
50
75
|
});
|
|
76
|
+
it('should not treat non-http schemes as URLs', async () => {
|
|
77
|
+
await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
|
|
78
|
+
});
|
|
51
79
|
});
|
|
52
80
|
});
|
|
53
81
|
describe('OpenApiFileParserService', () => {
|
package/package.json
CHANGED
|
@@ -5,21 +5,40 @@ import type { OpenApiSpecType } from '../types/openapi';
|
|
|
5
5
|
import { OpenApiSpec } from '../types/openapi';
|
|
6
6
|
|
|
7
7
|
export class SyncFileReaderService implements OpenApiFileReader {
|
|
8
|
-
|
|
9
|
-
// Check if path is a URL
|
|
8
|
+
private isUrl(path: string): boolean {
|
|
10
9
|
try {
|
|
11
10
|
const url = new URL(path);
|
|
12
|
-
|
|
13
|
-
const response = await fetch(url.toString());
|
|
14
|
-
if (!response.ok) {
|
|
15
|
-
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return await response.text();
|
|
11
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
19
12
|
} catch {
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async readFile(path: string): Promise<string> {
|
|
18
|
+
if (this.isUrl(path)) {
|
|
19
|
+
return await this.fetchUrl(path);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return readFileSync(path, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async fetchUrl(path: string): Promise<string> {
|
|
26
|
+
const url = new URL(path);
|
|
27
|
+
|
|
28
|
+
const headers: Record<string, string> = {};
|
|
29
|
+
if (url.username || url.password) {
|
|
30
|
+
const credentials = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
|
|
31
|
+
headers['Authorization'] = `Basic ${btoa(credentials)}`;
|
|
32
|
+
url.username = '';
|
|
33
|
+
url.password = '';
|
|
22
34
|
}
|
|
35
|
+
|
|
36
|
+
const response = await fetch(url.toString(), { headers });
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await response.text();
|
|
23
42
|
}
|
|
24
43
|
}
|
|
25
44
|
|
|
@@ -125,5 +125,21 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
125
125
|
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
126
126
|
expect(existsSync(outputFile)).toBe(true);
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
it('should generate code from a remote URL', () => {
|
|
130
|
+
execSync(`node ./dist/src/cli.js --input https://petstore3.swagger.io/api/v3/openapi.json --output ${testOutputDir}`, {
|
|
131
|
+
encoding: 'utf-8',
|
|
132
|
+
cwd,
|
|
133
|
+
timeout: 30000
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
137
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
138
|
+
|
|
139
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
140
|
+
expect(content).toContain('export default class');
|
|
141
|
+
expect(content).toContain('class ResponseValidationError<T> extends Error');
|
|
142
|
+
expect(content).toContain('import { z }');
|
|
143
|
+
});
|
|
128
144
|
});
|
|
129
145
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
4
|
import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname } from 'node:path';
|
|
6
5
|
|
|
7
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
7
|
|
|
@@ -32,7 +31,6 @@ describe('SyncFileReaderService', () => {
|
|
|
32
31
|
});
|
|
33
32
|
|
|
34
33
|
it('should handle URLs', async () => {
|
|
35
|
-
// Mock fetch for URL test
|
|
36
34
|
const originalFetch = global.fetch;
|
|
37
35
|
global.fetch = vi.fn().mockResolvedValue({
|
|
38
36
|
ok: true,
|
|
@@ -41,6 +39,7 @@ describe('SyncFileReaderService', () => {
|
|
|
41
39
|
|
|
42
40
|
const content = await reader.readFile('https://example.com/openapi.json');
|
|
43
41
|
expect(content).toBeTruthy();
|
|
42
|
+
expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
|
|
44
43
|
|
|
45
44
|
global.fetch = originalFetch;
|
|
46
45
|
});
|
|
@@ -54,12 +53,53 @@ describe('SyncFileReaderService', () => {
|
|
|
54
53
|
} as Response);
|
|
55
54
|
global.fetch = mockFetch;
|
|
56
55
|
|
|
57
|
-
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
|
|
56
|
+
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow('Failed to fetch');
|
|
57
|
+
|
|
58
|
+
global.fetch = originalFetch;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should propagate network errors for URLs instead of falling back to readFileSync', async () => {
|
|
62
|
+
const originalFetch = global.fetch;
|
|
63
|
+
global.fetch = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
|
|
64
|
+
|
|
65
|
+
await expect(reader.readFile('https://example.com/openapi.json')).rejects.toThrow('fetch failed');
|
|
66
|
+
|
|
67
|
+
global.fetch = originalFetch;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should extract basic auth credentials from URL into Authorization header', async () => {
|
|
71
|
+
const originalFetch = global.fetch;
|
|
72
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
73
|
+
ok: true,
|
|
74
|
+
text: async () => '{}'
|
|
75
|
+
} as Response);
|
|
76
|
+
global.fetch = mockFetch;
|
|
77
|
+
|
|
78
|
+
await reader.readFile('https://user:pass@example.com/openapi.json');
|
|
79
|
+
|
|
80
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: { Authorization: `Basic ${btoa('user:pass')}` } });
|
|
81
|
+
|
|
82
|
+
global.fetch = originalFetch;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should decode percent-encoded credentials from URL', async () => {
|
|
86
|
+
const originalFetch = global.fetch;
|
|
87
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
88
|
+
ok: true,
|
|
89
|
+
text: async () => '{}'
|
|
90
|
+
} as Response);
|
|
91
|
+
global.fetch = mockFetch;
|
|
92
|
+
|
|
93
|
+
await reader.readFile('https://julien%2Bstaging%40saris.ai:qwerty123@api.staging.saris.ai/api/openapi.json');
|
|
94
|
+
|
|
95
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.staging.saris.ai/api/openapi.json', { headers: { Authorization: `Basic ${btoa('julien+staging@saris.ai:qwerty123')}` } });
|
|
58
96
|
|
|
59
|
-
// Verify fetch was called
|
|
60
|
-
expect(mockFetch).toHaveBeenCalledWith('https://example.com/not-found.json');
|
|
61
97
|
global.fetch = originalFetch;
|
|
62
98
|
});
|
|
99
|
+
|
|
100
|
+
it('should not treat non-http schemes as URLs', async () => {
|
|
101
|
+
await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
|
|
102
|
+
});
|
|
63
103
|
});
|
|
64
104
|
});
|
|
65
105
|
|