zod-codegen 1.6.3 → 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/.github/workflows/ci.yml +50 -48
- package/.github/workflows/release.yml +13 -3
- package/.husky/commit-msg +1 -1
- package/.husky/pre-commit +1 -1
- package/.lintstagedrc.json +5 -1
- package/.nvmrc +1 -1
- package/.prettierrc.json +12 -5
- package/CHANGELOG.md +17 -0
- package/CONTRIBUTING.md +12 -12
- package/EXAMPLES.md +135 -57
- package/PERFORMANCE.md +4 -4
- package/README.md +87 -64
- package/SECURITY.md +1 -1
- package/dist/src/cli.js +11 -18
- package/dist/src/generator.d.ts +2 -2
- package/dist/src/generator.d.ts.map +1 -1
- package/dist/src/generator.js +5 -3
- package/dist/src/interfaces/code-generator.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.d.ts +3 -1
- package/dist/src/services/code-generator.service.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.js +236 -219
- 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 +25 -11
- package/dist/src/services/file-writer.service.d.ts.map +1 -1
- package/dist/src/services/file-writer.service.js +2 -2
- package/dist/src/services/import-builder.service.d.ts.map +1 -1
- package/dist/src/services/import-builder.service.js +3 -3
- package/dist/src/services/type-builder.service.d.ts.map +1 -1
- package/dist/src/types/generator-options.d.ts.map +1 -1
- package/dist/src/types/openapi.d.ts.map +1 -1
- package/dist/src/types/openapi.js +20 -20
- package/dist/src/utils/error-handler.d.ts.map +1 -1
- package/dist/src/utils/naming-convention.d.ts.map +1 -1
- package/dist/src/utils/naming-convention.js +6 -3
- package/dist/src/utils/signal-handler.d.ts.map +1 -1
- package/dist/tests/integration/cli-comprehensive.test.d.ts +2 -0
- package/dist/tests/integration/cli-comprehensive.test.d.ts.map +1 -0
- package/dist/tests/integration/cli-comprehensive.test.js +123 -0
- package/dist/tests/integration/cli.test.d.ts +2 -0
- package/dist/tests/integration/cli.test.d.ts.map +1 -0
- package/dist/tests/integration/cli.test.js +25 -0
- package/dist/tests/integration/error-scenarios.test.d.ts +2 -0
- package/dist/tests/integration/error-scenarios.test.d.ts.map +1 -0
- package/dist/tests/integration/error-scenarios.test.js +169 -0
- package/dist/tests/integration/snapshots.test.d.ts +2 -0
- package/dist/tests/integration/snapshots.test.d.ts.map +1 -0
- package/dist/tests/integration/snapshots.test.js +100 -0
- package/dist/tests/unit/code-generator-edge-cases.test.d.ts +2 -0
- package/dist/tests/unit/code-generator-edge-cases.test.d.ts.map +1 -0
- package/dist/tests/unit/code-generator-edge-cases.test.js +506 -0
- package/dist/tests/unit/code-generator.test.d.ts +2 -0
- package/dist/tests/unit/code-generator.test.d.ts.map +1 -0
- package/dist/tests/unit/code-generator.test.js +1364 -0
- package/dist/tests/unit/file-reader.test.d.ts +2 -0
- package/dist/tests/unit/file-reader.test.d.ts.map +1 -0
- package/dist/tests/unit/file-reader.test.js +153 -0
- package/dist/tests/unit/generator.test.d.ts +2 -0
- package/dist/tests/unit/generator.test.d.ts.map +1 -0
- package/dist/tests/unit/generator.test.js +119 -0
- package/dist/tests/unit/naming-convention.test.d.ts +2 -0
- package/dist/tests/unit/naming-convention.test.d.ts.map +1 -0
- package/dist/tests/unit/naming-convention.test.js +256 -0
- package/dist/tests/unit/reporter.test.d.ts +2 -0
- package/dist/tests/unit/reporter.test.d.ts.map +1 -0
- package/dist/tests/unit/reporter.test.js +44 -0
- package/dist/tests/unit/type-builder.test.d.ts +2 -0
- package/dist/tests/unit/type-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/type-builder.test.js +108 -0
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +10 -20
- package/eslint.config.mjs +38 -28
- package/examples/.gitkeep +1 -1
- package/examples/README.md +4 -2
- package/examples/petstore/README.md +18 -17
- package/examples/petstore/{type.ts → api.ts} +158 -74
- package/examples/petstore/authenticated-usage.ts +6 -4
- package/examples/petstore/basic-usage.ts +4 -3
- package/examples/petstore/error-handling-usage.ts +84 -0
- package/examples/petstore/retry-handler-usage.ts +11 -18
- package/examples/petstore/server-variables-usage.ts +10 -10
- package/examples/pokeapi/README.md +8 -8
- package/examples/pokeapi/api.ts +218 -0
- package/examples/pokeapi/basic-usage.ts +3 -2
- package/examples/pokeapi/custom-client.ts +5 -4
- package/package.json +17 -21
- package/src/cli.ts +20 -25
- package/src/generator.ts +13 -11
- package/src/interfaces/code-generator.ts +1 -1
- package/src/services/code-generator.service.ts +799 -1120
- package/src/services/file-reader.service.ts +35 -15
- package/src/services/file-writer.service.ts +7 -7
- package/src/services/import-builder.service.ts +9 -13
- package/src/services/type-builder.service.ts +8 -19
- package/src/types/generator-options.ts +1 -1
- package/src/types/openapi.ts +22 -22
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/naming-convention.ts +13 -10
- package/src/utils/reporter.ts +2 -2
- package/src/utils/signal-handler.ts +7 -8
- package/tests/integration/cli-comprehensive.test.ts +53 -31
- package/tests/integration/cli.test.ts +5 -5
- package/tests/integration/error-scenarios.test.ts +20 -26
- package/tests/integration/snapshots.test.ts +19 -23
- package/tests/unit/code-generator-edge-cases.test.ts +133 -133
- package/tests/unit/code-generator.test.ts +431 -330
- package/tests/unit/file-reader.test.ts +58 -18
- package/tests/unit/generator.test.ts +30 -18
- package/tests/unit/naming-convention.test.ts +27 -27
- package/tests/unit/type-builder.test.ts +2 -2
- package/tsconfig.json +5 -3
- package/vitest.config.ts +11 -21
- package/dist/scripts/update-manifest.d.ts +0 -14
- package/dist/scripts/update-manifest.d.ts.map +0 -1
- package/dist/scripts/update-manifest.js +0 -33
- package/dist/src/assets/manifest.json +0 -5
- package/examples/pokeapi/type.ts +0 -109
- package/generated/type.ts +0 -371
- package/scripts/update-manifest.ts +0 -49
- package/src/assets/manifest.json +0 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-reader.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/file-reader.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
describe('SyncFileReaderService', () => {
|
|
7
|
+
let reader;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
reader = new SyncFileReaderService();
|
|
10
|
+
});
|
|
11
|
+
describe('readFile', () => {
|
|
12
|
+
it('should read local JSON files', async () => {
|
|
13
|
+
const content = await reader.readFile(join(__dirname, '../../samples/openapi.json'));
|
|
14
|
+
expect(content).toBeTruthy();
|
|
15
|
+
expect(typeof content).toBe('string');
|
|
16
|
+
});
|
|
17
|
+
it('should read local YAML files', async () => {
|
|
18
|
+
const content = await reader.readFile(join(__dirname, '../../samples/swagger-petstore.yaml'));
|
|
19
|
+
expect(content).toBeTruthy();
|
|
20
|
+
expect(typeof content).toBe('string');
|
|
21
|
+
expect(content).toContain('openapi:');
|
|
22
|
+
});
|
|
23
|
+
it('should handle non-existent files', async () => {
|
|
24
|
+
await expect(reader.readFile('./non-existent-file.json')).rejects.toThrow();
|
|
25
|
+
});
|
|
26
|
+
it('should handle URLs', async () => {
|
|
27
|
+
const originalFetch = global.fetch;
|
|
28
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
29
|
+
ok: true,
|
|
30
|
+
text: async () => '{"openapi": "3.0.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": {}}'
|
|
31
|
+
});
|
|
32
|
+
const content = await reader.readFile('https://example.com/openapi.json');
|
|
33
|
+
expect(content).toBeTruthy();
|
|
34
|
+
expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
|
|
35
|
+
global.fetch = originalFetch;
|
|
36
|
+
});
|
|
37
|
+
it('should handle URL fetch errors with non-ok responses', async () => {
|
|
38
|
+
const originalFetch = global.fetch;
|
|
39
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
40
|
+
ok: false,
|
|
41
|
+
status: 404,
|
|
42
|
+
statusText: 'Not Found'
|
|
43
|
+
});
|
|
44
|
+
global.fetch = mockFetch;
|
|
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')}` } });
|
|
74
|
+
global.fetch = originalFetch;
|
|
75
|
+
});
|
|
76
|
+
it('should not treat non-http schemes as URLs', async () => {
|
|
77
|
+
await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('OpenApiFileParserService', () => {
|
|
82
|
+
let parser;
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
parser = new OpenApiFileParserService();
|
|
85
|
+
});
|
|
86
|
+
describe('parse', () => {
|
|
87
|
+
it('should parse valid JSON OpenAPI spec', () => {
|
|
88
|
+
const jsonSpec = JSON.stringify({
|
|
89
|
+
openapi: '3.0.0',
|
|
90
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
91
|
+
paths: {}
|
|
92
|
+
});
|
|
93
|
+
const result = parser.parse(jsonSpec);
|
|
94
|
+
expect(result).toBeDefined();
|
|
95
|
+
expect(result.openapi).toBe('3.0.0');
|
|
96
|
+
expect(result.info.title).toBe('Test API');
|
|
97
|
+
});
|
|
98
|
+
it('should parse valid YAML OpenAPI spec', () => {
|
|
99
|
+
const yamlSpec = `
|
|
100
|
+
openapi: 3.0.0
|
|
101
|
+
info:
|
|
102
|
+
title: Test API
|
|
103
|
+
version: 1.0.0
|
|
104
|
+
paths: {}
|
|
105
|
+
`;
|
|
106
|
+
const result = parser.parse(yamlSpec);
|
|
107
|
+
expect(result).toBeDefined();
|
|
108
|
+
expect(result.openapi).toBe('3.0.0');
|
|
109
|
+
expect(result.info.title).toBe('Test API');
|
|
110
|
+
});
|
|
111
|
+
it('should parse already parsed objects', () => {
|
|
112
|
+
const spec = {
|
|
113
|
+
openapi: '3.0.0',
|
|
114
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
115
|
+
paths: {}
|
|
116
|
+
};
|
|
117
|
+
const result = parser.parse(spec);
|
|
118
|
+
expect(result).toBeDefined();
|
|
119
|
+
expect(result.openapi).toBe('3.0.0');
|
|
120
|
+
});
|
|
121
|
+
it('should validate OpenAPI structure', () => {
|
|
122
|
+
const invalidSpec = {
|
|
123
|
+
openapi: '2.0.0', // Wrong version format
|
|
124
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
125
|
+
paths: {}
|
|
126
|
+
};
|
|
127
|
+
expect(() => parser.parse(invalidSpec)).toThrow();
|
|
128
|
+
});
|
|
129
|
+
it('should handle missing required fields', () => {
|
|
130
|
+
const invalidSpec = {
|
|
131
|
+
openapi: '3.0.0',
|
|
132
|
+
// Missing info
|
|
133
|
+
paths: {}
|
|
134
|
+
};
|
|
135
|
+
expect(() => parser.parse(invalidSpec)).toThrow();
|
|
136
|
+
});
|
|
137
|
+
it('should handle invalid JSON string that fails to parse', () => {
|
|
138
|
+
const invalidJson = '{invalid json}';
|
|
139
|
+
// When JSON.parse fails, it should fall back to YAML parsing
|
|
140
|
+
// This tests the catch block in the parse method (line 27)
|
|
141
|
+
// The YAML parser might succeed or fail, but the catch block should be executed
|
|
142
|
+
try {
|
|
143
|
+
const result = parser.parse(invalidJson);
|
|
144
|
+
// If YAML parsing succeeds, result should be defined
|
|
145
|
+
expect(result).toBeDefined();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// If both JSON and YAML parsing fail, that's also valid
|
|
149
|
+
// The important thing is that the catch block on line 27 was executed
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/generator.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { Generator } from '../../src/generator';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const testOutputDir = join(__dirname, '../../test-output');
|
|
8
|
+
describe('Generator', () => {
|
|
9
|
+
let generator;
|
|
10
|
+
let mockReporter;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Clean up test output directory
|
|
13
|
+
if (existsSync(testOutputDir)) {
|
|
14
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
mkdirSync(testOutputDir, { recursive: true });
|
|
17
|
+
// Create a mock reporter
|
|
18
|
+
mockReporter = {
|
|
19
|
+
log: vi.fn(),
|
|
20
|
+
error: vi.fn()
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Clean up test output directory
|
|
25
|
+
if (existsSync(testOutputDir)) {
|
|
26
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
describe('constructor', () => {
|
|
30
|
+
it('should create a new Generator instance', () => {
|
|
31
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
32
|
+
expect(generator).toBeInstanceOf(Generator);
|
|
33
|
+
});
|
|
34
|
+
it('should initialize with correct parameters', () => {
|
|
35
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
36
|
+
expect(generator).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('run', () => {
|
|
40
|
+
it('should be a function', () => {
|
|
41
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
42
|
+
expect(typeof generator.run).toBe('function');
|
|
43
|
+
});
|
|
44
|
+
it('should generate code from a valid OpenAPI file', async () => {
|
|
45
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
46
|
+
const exitCode = await generator.run();
|
|
47
|
+
expect(exitCode).toBe(0);
|
|
48
|
+
expect(mockReporter.log).toHaveBeenCalled();
|
|
49
|
+
expect(mockReporter.error).not.toHaveBeenCalled();
|
|
50
|
+
// Verify output file was created
|
|
51
|
+
const outputFile = join(testOutputDir, 'api.ts');
|
|
52
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
53
|
+
// Verify file contains expected content
|
|
54
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
55
|
+
expect(content).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
56
|
+
expect(content).toContain('export default class');
|
|
57
|
+
expect(content).toContain('getBaseRequestOptions');
|
|
58
|
+
});
|
|
59
|
+
it('should handle invalid file paths gracefully', async () => {
|
|
60
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './non-existent-file.yaml', testOutputDir);
|
|
61
|
+
const exitCode = await generator.run();
|
|
62
|
+
expect(exitCode).toBe(1);
|
|
63
|
+
expect(mockReporter.error).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
it('should handle invalid OpenAPI specifications', async () => {
|
|
66
|
+
// Create a temporary invalid OpenAPI file
|
|
67
|
+
const invalidFile = join(testOutputDir, 'invalid.yaml');
|
|
68
|
+
mkdirSync(testOutputDir, { recursive: true });
|
|
69
|
+
const { writeFileSync } = await import('node:fs');
|
|
70
|
+
writeFileSync(invalidFile, 'invalid: yaml: content: [unclosed');
|
|
71
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, invalidFile, testOutputDir);
|
|
72
|
+
const exitCode = await generator.run();
|
|
73
|
+
expect(exitCode).toBe(1);
|
|
74
|
+
expect(mockReporter.error).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it('should generate code with correct structure', async () => {
|
|
77
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
78
|
+
await generator.run();
|
|
79
|
+
const outputFile = join(testOutputDir, 'api.ts');
|
|
80
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
81
|
+
// Check for key components
|
|
82
|
+
expect(content).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
83
|
+
expect(content).toContain('export const');
|
|
84
|
+
expect(content).toContain('protected getBaseRequestOptions');
|
|
85
|
+
expect(content).toContain('protected async makeRequest');
|
|
86
|
+
});
|
|
87
|
+
it('should include header comments in generated file', async () => {
|
|
88
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
89
|
+
await generator.run();
|
|
90
|
+
const outputFile = join(testOutputDir, 'api.ts');
|
|
91
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
92
|
+
expect(content).toContain('AUTOGENERATED');
|
|
93
|
+
expect(content).toContain('test-app@1.0.0');
|
|
94
|
+
expect(content).toContain('eslint-disable');
|
|
95
|
+
});
|
|
96
|
+
it('should write to custom file path when output is a .ts path', async () => {
|
|
97
|
+
const customPath = join(testOutputDir, 'custom.ts');
|
|
98
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', customPath);
|
|
99
|
+
const exitCode = await generator.run();
|
|
100
|
+
expect(exitCode).toBe(0);
|
|
101
|
+
expect(existsSync(customPath)).toBe(true);
|
|
102
|
+
expect(existsSync(join(testOutputDir, 'api.ts'))).toBe(false);
|
|
103
|
+
const content = readFileSync(customPath, 'utf-8');
|
|
104
|
+
expect(content).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
105
|
+
});
|
|
106
|
+
it('should handle unknown error type (not Error instance)', async () => {
|
|
107
|
+
generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', testOutputDir);
|
|
108
|
+
// Mock the fileReader to throw a non-Error object
|
|
109
|
+
const { SyncFileReaderService } = await import('../../src/services/file-reader.service');
|
|
110
|
+
const originalReadFile = SyncFileReaderService.prototype.readFile;
|
|
111
|
+
SyncFileReaderService.prototype.readFile = vi.fn().mockRejectedValue('string error');
|
|
112
|
+
const exitCode = await generator.run();
|
|
113
|
+
expect(exitCode).toBe(1);
|
|
114
|
+
expect(mockReporter.error).toHaveBeenCalledWith('❌ An unknown error occurred');
|
|
115
|
+
// Restore original method
|
|
116
|
+
SyncFileReaderService.prototype.readFile = originalReadFile;
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"naming-convention.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/naming-convention.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { transformNamingConvention } from '../../src/utils/naming-convention';
|
|
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
|
+
describe('edge cases for internal functions', () => {
|
|
231
|
+
it('should handle empty string input (tests capitalize empty string)', () => {
|
|
232
|
+
expect(transformNamingConvention('', 'PascalCase')).toBe('');
|
|
233
|
+
expect(transformNamingConvention('', 'camelCase')).toBe('');
|
|
234
|
+
});
|
|
235
|
+
it('should handle single character input (tests capitalize single char)', () => {
|
|
236
|
+
expect(transformNamingConvention('a', 'PascalCase')).toBe('A');
|
|
237
|
+
expect(transformNamingConvention('A', 'camelCase')).toBe('a');
|
|
238
|
+
});
|
|
239
|
+
it('should handle input that results in empty words array (tests toCamelCase empty array)', () => {
|
|
240
|
+
// This tests the edge case where splitCamelCase might return empty array
|
|
241
|
+
// We need a case where the input cannot be split but results in empty words
|
|
242
|
+
// Actually, splitCamelCase always returns at least [input] if words.length === 0
|
|
243
|
+
// So we need to test the case where words array becomes empty after processing
|
|
244
|
+
// This is tricky - let's test with a string that when normalized becomes problematic
|
|
245
|
+
const result = transformNamingConvention('', 'camelCase');
|
|
246
|
+
expect(result).toBe('');
|
|
247
|
+
});
|
|
248
|
+
it('should handle splitCamelCase edge case where no words are created but input exists', () => {
|
|
249
|
+
// Test with a string that has no uppercase/digit boundaries
|
|
250
|
+
// This should still create at least one word
|
|
251
|
+
const result = transformNamingConvention('lowercase', 'camelCase');
|
|
252
|
+
expect(result).toBe('lowercase');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/reporter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Reporter } from '../../src/utils/reporter';
|
|
3
|
+
describe('Reporter', () => {
|
|
4
|
+
it('should write log messages to stdout', () => {
|
|
5
|
+
const stdout = { write: vi.fn() };
|
|
6
|
+
const reporter = new Reporter(stdout);
|
|
7
|
+
reporter.log('Hello', 'World');
|
|
8
|
+
expect(stdout.write).toHaveBeenCalledWith('Hello World\n');
|
|
9
|
+
});
|
|
10
|
+
it('should write error messages to stderr when provided', () => {
|
|
11
|
+
const stdout = { write: vi.fn() };
|
|
12
|
+
const stderr = { write: vi.fn() };
|
|
13
|
+
const reporter = new Reporter(stdout, stderr);
|
|
14
|
+
reporter.error('Error occurred');
|
|
15
|
+
expect(stderr.write).toHaveBeenCalledWith('Error occurred\n');
|
|
16
|
+
expect(stdout.write).not.toHaveBeenCalled();
|
|
17
|
+
});
|
|
18
|
+
it('should fall back to stdout for errors when stderr is not provided', () => {
|
|
19
|
+
const stdout = { write: vi.fn() };
|
|
20
|
+
const reporter = new Reporter(stdout);
|
|
21
|
+
reporter.error('Error occurred');
|
|
22
|
+
expect(stdout.write).toHaveBeenCalledWith('Error occurred\n');
|
|
23
|
+
});
|
|
24
|
+
it('should format multiple arguments', () => {
|
|
25
|
+
const stdout = { write: vi.fn() };
|
|
26
|
+
const reporter = new Reporter(stdout);
|
|
27
|
+
reporter.log('Count:', 42, 'items');
|
|
28
|
+
expect(stdout.write).toHaveBeenCalledWith('Count: 42 items\n');
|
|
29
|
+
});
|
|
30
|
+
it('should handle objects in log messages', () => {
|
|
31
|
+
const stdout = { write: vi.fn() };
|
|
32
|
+
const reporter = new Reporter(stdout);
|
|
33
|
+
reporter.log('Data:', { key: 'value' });
|
|
34
|
+
expect(stdout.write).toHaveBeenCalledWith("Data: { key: 'value' }\n");
|
|
35
|
+
});
|
|
36
|
+
it('should bind methods correctly for use as callbacks', () => {
|
|
37
|
+
const stdout = { write: vi.fn() };
|
|
38
|
+
const reporter = new Reporter(stdout);
|
|
39
|
+
const { log, error } = reporter;
|
|
40
|
+
log('Test log');
|
|
41
|
+
error('Test error');
|
|
42
|
+
expect(stdout.write).toHaveBeenCalledTimes(2);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-builder.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/type-builder.test.ts"],"names":[],"mappings":""}
|