ts-runtime-validation 1.6.16 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +430 -0
- package/README.md +505 -62
- package/dist/ICommandOptions.js +3 -0
- package/dist/ICommandOptions.js.map +1 -0
- package/dist/SchemaGenerator.deterministic-extended.test.js +420 -0
- package/dist/SchemaGenerator.deterministic-extended.test.js.map +1 -0
- package/dist/SchemaGenerator.deterministic.test.js +251 -0
- package/dist/SchemaGenerator.deterministic.test.js.map +1 -0
- package/dist/SchemaGenerator.integration.test.js +323 -0
- package/dist/SchemaGenerator.integration.test.js.map +1 -0
- package/dist/SchemaGenerator.js +120 -0
- package/dist/SchemaGenerator.js.map +1 -0
- package/dist/SchemaGenerator.test.js +226 -0
- package/dist/SchemaGenerator.test.js.map +1 -0
- package/dist/cli.test.js +155 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/errors/index.js +95 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/index.test.js +232 -0
- package/dist/errors/index.test.js.map +1 -0
- package/dist/getPosixPath.js +13 -0
- package/dist/getPosixPath.js.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.js.map +1 -0
- package/dist/services/CodeGenerator.js +321 -0
- package/dist/services/CodeGenerator.js.map +1 -0
- package/dist/services/FileDiscovery.js +123 -0
- package/dist/services/FileDiscovery.js.map +1 -0
- package/dist/services/FileDiscovery.test.js +184 -0
- package/dist/services/FileDiscovery.test.js.map +1 -0
- package/dist/services/SchemaProcessor.js +198 -0
- package/dist/services/SchemaProcessor.js.map +1 -0
- package/dist/services/SchemaProcessor.test.js +455 -0
- package/dist/services/SchemaProcessor.test.js.map +1 -0
- package/dist/services/SchemaWriter.js +76 -0
- package/dist/services/SchemaWriter.js.map +1 -0
- package/dist/services/SchemaWriter.test.js +255 -0
- package/dist/services/SchemaWriter.test.js.map +1 -0
- package/dist/test/basic-scenario/types.jsonschema.js +3 -0
- package/dist/test/basic-scenario/types.jsonschema.js.map +1 -0
- package/dist/test/duplicate-symbols-different-implementation/IBaseType.js +3 -0
- package/dist/test/duplicate-symbols-different-implementation/IBaseType.js.map +1 -0
- package/dist/test/duplicate-symbols-different-implementation/IBaseTypeDefinitionReplicated.js +3 -0
- package/dist/test/duplicate-symbols-different-implementation/IBaseTypeDefinitionReplicated.js.map +1 -0
- package/dist/test/duplicate-symbols-different-implementation/IBasicTypesA.jsonschema.js +3 -0
- package/dist/test/duplicate-symbols-different-implementation/IBasicTypesA.jsonschema.js.map +1 -0
- package/dist/test/duplicate-symbols-different-implementation/IBasicTypesB.jsonschema.js +3 -0
- package/dist/test/duplicate-symbols-different-implementation/IBasicTypesB.jsonschema.js.map +1 -0
- package/dist/test/duplicate-symbols-identitcal-implementation/IBaseType.js +3 -0
- package/dist/test/duplicate-symbols-identitcal-implementation/IBaseType.js.map +1 -0
- package/dist/test/duplicate-symbols-identitcal-implementation/IBaseTypeDefinitionReplicated.js +3 -0
- package/dist/test/duplicate-symbols-identitcal-implementation/IBaseTypeDefinitionReplicated.js.map +1 -0
- package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesA.jsonschema.js +3 -0
- package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesA.jsonschema.js.map +1 -0
- package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesB.jsonschema.js +3 -0
- package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesB.jsonschema.js.map +1 -0
- package/dist/utils/ProgressReporter.js +67 -0
- package/dist/utils/ProgressReporter.js.map +1 -0
- package/dist/utils/ProgressReporter.test.js +267 -0
- package/dist/utils/ProgressReporter.test.js.map +1 -0
- package/dist/writeLine.js +12 -0
- package/dist/writeLine.js.map +1 -0
- package/package.json +2 -2
- package/src/ICommandOptions.ts +7 -0
- package/src/SchemaGenerator.deterministic-extended.test.ts +429 -0
- package/src/SchemaGenerator.deterministic.test.ts +276 -0
- package/src/SchemaGenerator.integration.test.ts +411 -0
- package/src/SchemaGenerator.test.ts +118 -0
- package/src/SchemaGenerator.ts +112 -298
- package/src/cli.test.ts +130 -0
- package/src/errors/index.test.ts +319 -0
- package/src/errors/index.ts +92 -0
- package/src/index.ts +8 -1
- package/src/services/CodeGenerator.ts +370 -0
- package/src/services/FileDiscovery.test.ts +216 -0
- package/src/services/FileDiscovery.ts +140 -0
- package/src/services/SchemaProcessor.test.ts +536 -0
- package/src/services/SchemaProcessor.ts +194 -0
- package/src/services/SchemaWriter.test.ts +304 -0
- package/src/services/SchemaWriter.ts +75 -0
- package/src/utils/ProgressReporter.test.ts +357 -0
- package/src/utils/ProgressReporter.ts +76 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { fdir } from "fdir";
|
|
2
|
+
import picomatch from "picomatch";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import { FileDiscoveryError } from "../errors";
|
|
7
|
+
|
|
8
|
+
export interface FileDiscoveryOptions {
|
|
9
|
+
glob: string;
|
|
10
|
+
rootPath: string;
|
|
11
|
+
cacheEnabled?: boolean;
|
|
12
|
+
cachePath?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FileInfo {
|
|
16
|
+
path: string;
|
|
17
|
+
hash?: string;
|
|
18
|
+
lastModified?: Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class FileDiscovery {
|
|
22
|
+
private cacheFile: string;
|
|
23
|
+
private fileCache: Map<string, string> = new Map();
|
|
24
|
+
|
|
25
|
+
constructor(private options: FileDiscoveryOptions) {
|
|
26
|
+
this.cacheFile = path.join(
|
|
27
|
+
options.cachePath || ".ts-runtime-validation-cache",
|
|
28
|
+
"file-hashes.json"
|
|
29
|
+
);
|
|
30
|
+
if (options.cacheEnabled) {
|
|
31
|
+
this.loadCache();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public async discoverFiles(): Promise<FileInfo[]> {
|
|
36
|
+
const { glob, rootPath } = this.options;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const api = new fdir({
|
|
40
|
+
includeBasePath: true,
|
|
41
|
+
includeDirs: false,
|
|
42
|
+
filters: [
|
|
43
|
+
(filePath) => {
|
|
44
|
+
return picomatch.isMatch(filePath, glob, { contains: true });
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}).crawl(rootPath);
|
|
48
|
+
|
|
49
|
+
const files = await api.withPromise();
|
|
50
|
+
|
|
51
|
+
if (files.length === 0) {
|
|
52
|
+
throw new FileDiscoveryError(
|
|
53
|
+
`No files found matching pattern: ${glob} in ${rootPath}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sort files alphabetically to ensure consistent ordering
|
|
58
|
+
const sortedFiles = [...files].sort();
|
|
59
|
+
|
|
60
|
+
return this.options.cacheEnabled
|
|
61
|
+
? await this.enrichWithCacheInfo(sortedFiles)
|
|
62
|
+
: sortedFiles.map(path => ({ path }));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error instanceof FileDiscoveryError) {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
throw new FileDiscoveryError(
|
|
68
|
+
`Failed to discover files: ${error instanceof Error ? error.message : String(error)}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async enrichWithCacheInfo(files: string[]): Promise<FileInfo[]> {
|
|
74
|
+
const enrichedFiles = await Promise.all(
|
|
75
|
+
files.map(async (filePath) => {
|
|
76
|
+
const stats = await fs.promises.stat(filePath);
|
|
77
|
+
const hash = await this.getFileHash(filePath);
|
|
78
|
+
return {
|
|
79
|
+
path: filePath,
|
|
80
|
+
hash,
|
|
81
|
+
lastModified: stats.mtime
|
|
82
|
+
};
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await this.saveCache(enrichedFiles);
|
|
87
|
+
return enrichedFiles;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async getFileHash(filePath: string): Promise<string> {
|
|
91
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
92
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public hasFileChanged(filePath: string, currentHash: string): boolean {
|
|
96
|
+
const cachedHash = this.fileCache.get(filePath);
|
|
97
|
+
return cachedHash !== currentHash;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private loadCache(): void {
|
|
101
|
+
try {
|
|
102
|
+
if (fs.existsSync(this.cacheFile)) {
|
|
103
|
+
const cacheData = JSON.parse(
|
|
104
|
+
fs.readFileSync(this.cacheFile, 'utf-8')
|
|
105
|
+
);
|
|
106
|
+
this.fileCache = new Map(Object.entries(cacheData));
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.warn('Failed to load cache, starting fresh');
|
|
110
|
+
this.fileCache.clear();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async saveCache(files: FileInfo[]): Promise<void> {
|
|
115
|
+
const cacheDir = path.dirname(this.cacheFile);
|
|
116
|
+
if (!fs.existsSync(cacheDir)) {
|
|
117
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const cacheData: Record<string, string> = {};
|
|
121
|
+
files.forEach(file => {
|
|
122
|
+
if (file.hash) {
|
|
123
|
+
cacheData[file.path] = file.hash;
|
|
124
|
+
this.fileCache.set(file.path, file.hash);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await fs.promises.writeFile(
|
|
129
|
+
this.cacheFile,
|
|
130
|
+
JSON.stringify(cacheData, null, 2)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public clearCache(): void {
|
|
135
|
+
this.fileCache.clear();
|
|
136
|
+
if (fs.existsSync(this.cacheFile)) {
|
|
137
|
+
fs.unlinkSync(this.cacheFile);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { SchemaProcessor } from "./SchemaProcessor";
|
|
4
|
+
import { FileInfo } from "./FileDiscovery";
|
|
5
|
+
import { DuplicateSymbolError } from "../errors";
|
|
6
|
+
|
|
7
|
+
const testDir = path.resolve(__dirname, "../../.test-tmp/schema-processor");
|
|
8
|
+
|
|
9
|
+
const createTestFile = async (filePath: string, content: string) => {
|
|
10
|
+
const fullPath = path.resolve(testDir, filePath);
|
|
11
|
+
await fs.promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
12
|
+
await fs.promises.writeFile(fullPath, content);
|
|
13
|
+
return fullPath;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const cleanup = async () => {
|
|
17
|
+
if (fs.existsSync(testDir)) {
|
|
18
|
+
await fs.promises.rm(testDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeEach(cleanup);
|
|
23
|
+
afterAll(cleanup);
|
|
24
|
+
|
|
25
|
+
describe("SchemaProcessor", () => {
|
|
26
|
+
describe("processFiles", () => {
|
|
27
|
+
it("should process valid TypeScript files", async () => {
|
|
28
|
+
const filePath = await createTestFile("user.jsonschema.ts", `
|
|
29
|
+
export interface IUser {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
}
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
const processor = new SchemaProcessor({
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
parallel: false,
|
|
39
|
+
verbose: false
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const files: FileInfo[] = [{ path: filePath }];
|
|
43
|
+
const schemaMap = await processor.processFiles(files);
|
|
44
|
+
|
|
45
|
+
expect(schemaMap.size).toBe(1);
|
|
46
|
+
expect(schemaMap.has(filePath)).toBe(true);
|
|
47
|
+
|
|
48
|
+
const schema = schemaMap.get(filePath)!;
|
|
49
|
+
expect(schema.definitions).toBeDefined();
|
|
50
|
+
expect(schema.definitions!.IUser).toBeDefined();
|
|
51
|
+
expect(schema.definitions!.IUser).toMatchObject({
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
id: { type: "string" },
|
|
55
|
+
name: { type: "string" },
|
|
56
|
+
email: { type: "string" }
|
|
57
|
+
},
|
|
58
|
+
required: ["id", "name"],
|
|
59
|
+
additionalProperties: false
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should handle parallel processing", async () => {
|
|
64
|
+
const file1 = await createTestFile("user.jsonschema.ts", `
|
|
65
|
+
export interface IUser {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
}
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
const file2 = await createTestFile("product.jsonschema.ts", `
|
|
72
|
+
export interface IProduct {
|
|
73
|
+
id: string;
|
|
74
|
+
title: string;
|
|
75
|
+
price: number;
|
|
76
|
+
}
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
const processor = new SchemaProcessor({
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
parallel: true,
|
|
82
|
+
verbose: false
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const files: FileInfo[] = [
|
|
86
|
+
{ path: file1 },
|
|
87
|
+
{ path: file2 }
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const schemaMap = await processor.processFiles(files);
|
|
91
|
+
|
|
92
|
+
expect(schemaMap.size).toBe(2);
|
|
93
|
+
expect(schemaMap.has(file1)).toBe(true);
|
|
94
|
+
expect(schemaMap.has(file2)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle sequential processing", async () => {
|
|
98
|
+
const file1 = await createTestFile("user.jsonschema.ts", `
|
|
99
|
+
export interface IUser {
|
|
100
|
+
id: string;
|
|
101
|
+
name: string;
|
|
102
|
+
}
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
const processor = new SchemaProcessor({
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
parallel: false,
|
|
108
|
+
verbose: false
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const files: FileInfo[] = [{ path: file1 }];
|
|
112
|
+
const schemaMap = await processor.processFiles(files);
|
|
113
|
+
|
|
114
|
+
expect(schemaMap.size).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should handle files with syntax errors gracefully", async () => {
|
|
118
|
+
const validFile = await createTestFile("valid.jsonschema.ts", `
|
|
119
|
+
export interface IValid {
|
|
120
|
+
id: string;
|
|
121
|
+
}
|
|
122
|
+
`);
|
|
123
|
+
|
|
124
|
+
const invalidFile = await createTestFile("invalid.jsonschema.ts", `
|
|
125
|
+
export interface IInvalid {
|
|
126
|
+
id: string
|
|
127
|
+
// missing semicolon and other syntax errors
|
|
128
|
+
name string;
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
const processor = new SchemaProcessor({
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
parallel: false,
|
|
135
|
+
verbose: false
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const files: FileInfo[] = [
|
|
139
|
+
{ path: validFile },
|
|
140
|
+
{ path: invalidFile }
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Should not throw but should process valid files
|
|
144
|
+
const schemaMap = await processor.processFiles(files);
|
|
145
|
+
|
|
146
|
+
// At least valid file should be processed
|
|
147
|
+
expect(schemaMap.size).toBeGreaterThan(0);
|
|
148
|
+
expect(schemaMap.has(validFile)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should respect additionalProperties setting", async () => {
|
|
152
|
+
const filePath = await createTestFile("user.jsonschema.ts", `
|
|
153
|
+
export interface IUser {
|
|
154
|
+
id: string;
|
|
155
|
+
name: string;
|
|
156
|
+
}
|
|
157
|
+
`);
|
|
158
|
+
|
|
159
|
+
const processorStrict = new SchemaProcessor({
|
|
160
|
+
additionalProperties: false,
|
|
161
|
+
parallel: false
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const processorLoose = new SchemaProcessor({
|
|
165
|
+
additionalProperties: true,
|
|
166
|
+
parallel: false
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const files: FileInfo[] = [{ path: filePath }];
|
|
170
|
+
|
|
171
|
+
const strictSchema = await processorStrict.processFiles(files);
|
|
172
|
+
const looseSchema = await processorLoose.processFiles(files);
|
|
173
|
+
|
|
174
|
+
const strictUser = strictSchema.get(filePath)!.definitions!.IUser as any;
|
|
175
|
+
const looseUser = looseSchema.get(filePath)!.definitions!.IUser as any;
|
|
176
|
+
|
|
177
|
+
expect(strictUser.additionalProperties).toBe(false);
|
|
178
|
+
// Note: ts-json-schema-generator may not set additionalProperties: true explicitly
|
|
179
|
+
expect(looseUser.additionalProperties === true || looseUser.additionalProperties === undefined).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("validateSchemaCompatibility", () => {
|
|
184
|
+
it("should pass with identical schemas", async () => {
|
|
185
|
+
const file1 = await createTestFile("user1.jsonschema.ts", `
|
|
186
|
+
export interface IUser {
|
|
187
|
+
id: string;
|
|
188
|
+
name: string;
|
|
189
|
+
}
|
|
190
|
+
`);
|
|
191
|
+
|
|
192
|
+
const file2 = await createTestFile("user2.jsonschema.ts", `
|
|
193
|
+
export interface IUser {
|
|
194
|
+
id: string;
|
|
195
|
+
name: string;
|
|
196
|
+
}
|
|
197
|
+
`);
|
|
198
|
+
|
|
199
|
+
const processor = new SchemaProcessor({
|
|
200
|
+
additionalProperties: false,
|
|
201
|
+
parallel: false
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const files: FileInfo[] = [
|
|
205
|
+
{ path: file1 },
|
|
206
|
+
{ path: file2 }
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const schemaMap = await processor.processFiles(files);
|
|
210
|
+
|
|
211
|
+
expect(() => {
|
|
212
|
+
processor.validateSchemaCompatibility(schemaMap);
|
|
213
|
+
}).not.toThrow();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should throw DuplicateSymbolError for conflicting schemas", async () => {
|
|
217
|
+
const file1 = await createTestFile("user1.jsonschema.ts", `
|
|
218
|
+
export interface IUser {
|
|
219
|
+
id: string;
|
|
220
|
+
name: string;
|
|
221
|
+
}
|
|
222
|
+
`);
|
|
223
|
+
|
|
224
|
+
const file2 = await createTestFile("user2.jsonschema.ts", `
|
|
225
|
+
export interface IUser {
|
|
226
|
+
id: string;
|
|
227
|
+
email: string;
|
|
228
|
+
}
|
|
229
|
+
`);
|
|
230
|
+
|
|
231
|
+
const processor = new SchemaProcessor({
|
|
232
|
+
additionalProperties: false,
|
|
233
|
+
parallel: false
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const files: FileInfo[] = [
|
|
237
|
+
{ path: file1 },
|
|
238
|
+
{ path: file2 }
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const schemaMap = await processor.processFiles(files);
|
|
242
|
+
|
|
243
|
+
expect(() => {
|
|
244
|
+
processor.validateSchemaCompatibility(schemaMap);
|
|
245
|
+
}).toThrow(DuplicateSymbolError);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should allow different symbols with same name in different contexts", async () => {
|
|
249
|
+
const file1 = await createTestFile("api/user.jsonschema.ts", `
|
|
250
|
+
export interface IUser {
|
|
251
|
+
id: string;
|
|
252
|
+
name: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface IProduct {
|
|
256
|
+
id: string;
|
|
257
|
+
title: string;
|
|
258
|
+
}
|
|
259
|
+
`);
|
|
260
|
+
|
|
261
|
+
const processor = new SchemaProcessor({
|
|
262
|
+
additionalProperties: false,
|
|
263
|
+
parallel: false
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const files: FileInfo[] = [{ path: file1 }];
|
|
267
|
+
const schemaMap = await processor.processFiles(files);
|
|
268
|
+
|
|
269
|
+
expect(() => {
|
|
270
|
+
processor.validateSchemaCompatibility(schemaMap);
|
|
271
|
+
}).not.toThrow();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("mergeSchemas", () => {
|
|
276
|
+
it("should merge multiple schemas correctly", async () => {
|
|
277
|
+
const file1 = await createTestFile("user.jsonschema.ts", `
|
|
278
|
+
export interface IUser {
|
|
279
|
+
id: string;
|
|
280
|
+
name: string;
|
|
281
|
+
}
|
|
282
|
+
`);
|
|
283
|
+
|
|
284
|
+
const file2 = await createTestFile("product.jsonschema.ts", `
|
|
285
|
+
export interface IProduct {
|
|
286
|
+
id: string;
|
|
287
|
+
title: string;
|
|
288
|
+
price: number;
|
|
289
|
+
}
|
|
290
|
+
`);
|
|
291
|
+
|
|
292
|
+
const processor = new SchemaProcessor({
|
|
293
|
+
additionalProperties: false,
|
|
294
|
+
parallel: false
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const files: FileInfo[] = [
|
|
298
|
+
{ path: file1 },
|
|
299
|
+
{ path: file2 }
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const schemaMap = await processor.processFiles(files);
|
|
303
|
+
const mergedSchema = processor.mergeSchemas(schemaMap);
|
|
304
|
+
|
|
305
|
+
expect(mergedSchema.$schema).toBe("http://json-schema.org/draft-07/schema#");
|
|
306
|
+
expect(mergedSchema.definitions).toBeDefined();
|
|
307
|
+
expect(mergedSchema.definitions!.IUser).toBeDefined();
|
|
308
|
+
expect(mergedSchema.definitions!.IProduct).toBeDefined();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should preserve schema version from first file", async () => {
|
|
312
|
+
const file1 = await createTestFile("user.jsonschema.ts", `
|
|
313
|
+
export interface IUser {
|
|
314
|
+
id: string;
|
|
315
|
+
name: string;
|
|
316
|
+
}
|
|
317
|
+
`);
|
|
318
|
+
|
|
319
|
+
const processor = new SchemaProcessor({
|
|
320
|
+
additionalProperties: false,
|
|
321
|
+
parallel: false
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const files: FileInfo[] = [{ path: file1 }];
|
|
325
|
+
const schemaMap = await processor.processFiles(files);
|
|
326
|
+
const mergedSchema = processor.mergeSchemas(schemaMap);
|
|
327
|
+
|
|
328
|
+
expect(mergedSchema.$schema).toMatch(/json-schema\.org/);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("should handle empty schema map", () => {
|
|
332
|
+
const processor = new SchemaProcessor({
|
|
333
|
+
additionalProperties: false,
|
|
334
|
+
parallel: false
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const schemaMap = new Map();
|
|
338
|
+
const mergedSchema = processor.mergeSchemas(schemaMap);
|
|
339
|
+
|
|
340
|
+
expect(mergedSchema.$schema).toBe("http://json-schema.org/draft-07/schema#");
|
|
341
|
+
expect(mergedSchema.definitions).toEqual({});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should sort definitions alphabetically in merged schema", async () => {
|
|
345
|
+
const file1 = await createTestFile("types1.jsonschema.ts", `
|
|
346
|
+
export interface ZebraType {
|
|
347
|
+
id: string;
|
|
348
|
+
}
|
|
349
|
+
export interface AppleType {
|
|
350
|
+
name: string;
|
|
351
|
+
}
|
|
352
|
+
`);
|
|
353
|
+
|
|
354
|
+
const file2 = await createTestFile("types2.jsonschema.ts", `
|
|
355
|
+
export interface MiddleType {
|
|
356
|
+
value: number;
|
|
357
|
+
}
|
|
358
|
+
export interface BananaType {
|
|
359
|
+
flag: boolean;
|
|
360
|
+
}
|
|
361
|
+
`);
|
|
362
|
+
|
|
363
|
+
const processor = new SchemaProcessor({
|
|
364
|
+
additionalProperties: false,
|
|
365
|
+
parallel: false
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const files: FileInfo[] = [
|
|
369
|
+
{ path: file1 },
|
|
370
|
+
{ path: file2 }
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const schemaMap = await processor.processFiles(files);
|
|
374
|
+
const mergedSchema = processor.mergeSchemas(schemaMap);
|
|
375
|
+
|
|
376
|
+
const definitionKeys = Object.keys(mergedSchema.definitions || {});
|
|
377
|
+
const sortedKeys = [...definitionKeys].sort();
|
|
378
|
+
|
|
379
|
+
expect(definitionKeys).toEqual(sortedKeys);
|
|
380
|
+
expect(definitionKeys).toEqual(['AppleType', 'BananaType', 'MiddleType', 'ZebraType']);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should maintain alphabetical order for definitions with numbers and special characters", async () => {
|
|
384
|
+
const file1 = await createTestFile("special.jsonschema.ts", `
|
|
385
|
+
export interface Type1 {
|
|
386
|
+
id: string;
|
|
387
|
+
}
|
|
388
|
+
export interface TypeA {
|
|
389
|
+
name: string;
|
|
390
|
+
}
|
|
391
|
+
export interface Type10 {
|
|
392
|
+
value: number;
|
|
393
|
+
}
|
|
394
|
+
export interface Type2 {
|
|
395
|
+
flag: boolean;
|
|
396
|
+
}
|
|
397
|
+
`);
|
|
398
|
+
|
|
399
|
+
const processor = new SchemaProcessor({
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
parallel: false
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const files: FileInfo[] = [{ path: file1 }];
|
|
405
|
+
const schemaMap = await processor.processFiles(files);
|
|
406
|
+
const mergedSchema = processor.mergeSchemas(schemaMap);
|
|
407
|
+
|
|
408
|
+
const definitionKeys = Object.keys(mergedSchema.definitions || {});
|
|
409
|
+
const sortedKeys = [...definitionKeys].sort();
|
|
410
|
+
|
|
411
|
+
expect(definitionKeys).toEqual(sortedKeys);
|
|
412
|
+
// Natural alphabetical order: numbers before letters
|
|
413
|
+
expect(definitionKeys).toEqual(['Type1', 'Type10', 'Type2', 'TypeA']);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe("error handling", () => {
|
|
418
|
+
it("should handle missing files gracefully", async () => {
|
|
419
|
+
const processor = new SchemaProcessor({
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
parallel: false,
|
|
422
|
+
verbose: false
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const files: FileInfo[] = [
|
|
426
|
+
{ path: "/nonexistent/file.ts" }
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
// Should not throw but return empty map
|
|
430
|
+
const schemaMap = await processor.processFiles(files);
|
|
431
|
+
expect(schemaMap.size).toBe(0);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("should provide verbose error information", async () => {
|
|
435
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
436
|
+
|
|
437
|
+
const processor = new SchemaProcessor({
|
|
438
|
+
additionalProperties: false,
|
|
439
|
+
parallel: false,
|
|
440
|
+
verbose: true
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const files: FileInfo[] = [
|
|
444
|
+
{ path: "/nonexistent/file.ts" }
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
await processor.processFiles(files);
|
|
448
|
+
|
|
449
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
450
|
+
consoleWarnSpy.mockRestore();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe("TypeScript features", () => {
|
|
455
|
+
it("should handle union types", async () => {
|
|
456
|
+
const filePath = await createTestFile("types.jsonschema.ts", `
|
|
457
|
+
export type Status = "active" | "inactive" | "pending";
|
|
458
|
+
|
|
459
|
+
export interface IUser {
|
|
460
|
+
id: string;
|
|
461
|
+
status: Status;
|
|
462
|
+
}
|
|
463
|
+
`);
|
|
464
|
+
|
|
465
|
+
const processor = new SchemaProcessor({
|
|
466
|
+
additionalProperties: false,
|
|
467
|
+
parallel: false
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const files: FileInfo[] = [{ path: filePath }];
|
|
471
|
+
const schemaMap = await processor.processFiles(files);
|
|
472
|
+
|
|
473
|
+
expect(schemaMap.size).toBe(1);
|
|
474
|
+
const schema = schemaMap.get(filePath)!;
|
|
475
|
+
expect(schema.definitions!.Status).toBeDefined();
|
|
476
|
+
expect(schema.definitions!.IUser).toBeDefined();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("should handle optional properties", async () => {
|
|
480
|
+
const filePath = await createTestFile("user.jsonschema.ts", `
|
|
481
|
+
export interface IUser {
|
|
482
|
+
id: string;
|
|
483
|
+
name?: string;
|
|
484
|
+
email?: string;
|
|
485
|
+
age: number;
|
|
486
|
+
}
|
|
487
|
+
`);
|
|
488
|
+
|
|
489
|
+
const processor = new SchemaProcessor({
|
|
490
|
+
additionalProperties: false,
|
|
491
|
+
parallel: false
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const files: FileInfo[] = [{ path: filePath }];
|
|
495
|
+
const schemaMap = await processor.processFiles(files);
|
|
496
|
+
|
|
497
|
+
const schema = schemaMap.get(filePath)!;
|
|
498
|
+
const userSchema = schema.definitions!.IUser as any;
|
|
499
|
+
|
|
500
|
+
expect(userSchema.required).toEqual(expect.arrayContaining(["id", "age"]));
|
|
501
|
+
expect(userSchema.required).not.toContain("name");
|
|
502
|
+
expect(userSchema.required).not.toContain("email");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("should handle nested interfaces", async () => {
|
|
506
|
+
const filePath = await createTestFile("nested.jsonschema.ts", `
|
|
507
|
+
export interface IAddress {
|
|
508
|
+
street: string;
|
|
509
|
+
city: string;
|
|
510
|
+
country: string;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export interface IUser {
|
|
514
|
+
id: string;
|
|
515
|
+
name: string;
|
|
516
|
+
address: IAddress;
|
|
517
|
+
}
|
|
518
|
+
`);
|
|
519
|
+
|
|
520
|
+
const processor = new SchemaProcessor({
|
|
521
|
+
additionalProperties: false,
|
|
522
|
+
parallel: false
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const files: FileInfo[] = [{ path: filePath }];
|
|
526
|
+
const schemaMap = await processor.processFiles(files);
|
|
527
|
+
|
|
528
|
+
const schema = schemaMap.get(filePath)!;
|
|
529
|
+
expect(schema.definitions!.IAddress).toBeDefined();
|
|
530
|
+
expect(schema.definitions!.IUser).toBeDefined();
|
|
531
|
+
|
|
532
|
+
const userSchema = schema.definitions!.IUser as any;
|
|
533
|
+
expect(userSchema.properties.address.$ref).toBe("#/definitions/IAddress");
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
});
|