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,194 @@
|
|
|
1
|
+
import * as tsj from "ts-json-schema-generator";
|
|
2
|
+
import { Config, Schema } from "ts-json-schema-generator";
|
|
3
|
+
import assert from "assert";
|
|
4
|
+
import { SchemaGenerationError, DuplicateSymbolError } from "../errors";
|
|
5
|
+
import { FileInfo } from "./FileDiscovery";
|
|
6
|
+
|
|
7
|
+
export interface SchemaProcessorOptions {
|
|
8
|
+
additionalProperties: boolean;
|
|
9
|
+
tsconfigPath?: string;
|
|
10
|
+
parallel?: boolean;
|
|
11
|
+
verbose?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ProcessingResult {
|
|
15
|
+
file: string;
|
|
16
|
+
schema: Schema | null;
|
|
17
|
+
error?: Error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SchemaProcessor {
|
|
21
|
+
constructor(private options: SchemaProcessorOptions) {}
|
|
22
|
+
|
|
23
|
+
public async processFiles(files: FileInfo[]): Promise<Map<string, Schema>> {
|
|
24
|
+
const { parallel = true, verbose = false } = this.options;
|
|
25
|
+
|
|
26
|
+
if (verbose) {
|
|
27
|
+
console.log(`Processing ${files.length} files...`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Sort files by path to ensure consistent processing order
|
|
31
|
+
const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
32
|
+
|
|
33
|
+
const results = parallel
|
|
34
|
+
? await this.processInParallel(sortedFiles)
|
|
35
|
+
: await this.processSequentially(sortedFiles);
|
|
36
|
+
|
|
37
|
+
return this.consolidateSchemas(results);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async processInParallel(files: FileInfo[]): Promise<ProcessingResult[]> {
|
|
41
|
+
const promises = files.map(file => this.processFile(file));
|
|
42
|
+
const results = await Promise.allSettled(promises);
|
|
43
|
+
|
|
44
|
+
// Map results back to original file order to maintain consistency
|
|
45
|
+
return results.map((result, index) => {
|
|
46
|
+
if (result.status === 'fulfilled') {
|
|
47
|
+
return result.value;
|
|
48
|
+
} else {
|
|
49
|
+
return {
|
|
50
|
+
file: files[index].path,
|
|
51
|
+
schema: null,
|
|
52
|
+
error: result.reason
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async processSequentially(files: FileInfo[]): Promise<ProcessingResult[]> {
|
|
59
|
+
const results: ProcessingResult[] = [];
|
|
60
|
+
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
try {
|
|
63
|
+
const result = await this.processFile(file);
|
|
64
|
+
results.push(result);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
results.push({
|
|
67
|
+
file: file.path,
|
|
68
|
+
schema: null,
|
|
69
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async processFile(file: FileInfo): Promise<ProcessingResult> {
|
|
78
|
+
const { additionalProperties, tsconfigPath, verbose } = this.options;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (verbose) {
|
|
82
|
+
console.log(`Processing: ${file.path}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const config: Config = {
|
|
86
|
+
path: file.path,
|
|
87
|
+
type: "*",
|
|
88
|
+
additionalProperties,
|
|
89
|
+
encodeRefs: false,
|
|
90
|
+
sortProps: true,
|
|
91
|
+
...(tsconfigPath ? { tsconfig: tsconfigPath } : {}),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const schemaGenerator = tsj.createGenerator(config);
|
|
95
|
+
const schema = schemaGenerator.createSchema(config.type);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
file: file.path,
|
|
99
|
+
schema,
|
|
100
|
+
error: undefined
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new SchemaGenerationError(
|
|
104
|
+
`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private consolidateSchemas(results: ProcessingResult[]): Map<string, Schema> {
|
|
110
|
+
const schemaMap = new Map<string, Schema>();
|
|
111
|
+
const errors: Error[] = [];
|
|
112
|
+
|
|
113
|
+
// Sort results by file path to ensure consistent order
|
|
114
|
+
const sortedResults = [...results].sort((a, b) => a.file.localeCompare(b.file));
|
|
115
|
+
|
|
116
|
+
for (const result of sortedResults) {
|
|
117
|
+
if (result.error) {
|
|
118
|
+
errors.push(result.error);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (result.schema) {
|
|
123
|
+
schemaMap.set(result.file, result.schema);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (errors.length > 0 && this.options.verbose) {
|
|
128
|
+
console.warn(`Encountered ${errors.length} errors during processing:`);
|
|
129
|
+
errors.forEach(error => console.warn(` - ${error.message}`));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return schemaMap;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public validateSchemaCompatibility(schemaMap: Map<string, Schema>): void {
|
|
136
|
+
const definitions: { [id: string]: any } = {};
|
|
137
|
+
|
|
138
|
+
// Sort by file path for consistent processing order
|
|
139
|
+
const sortedEntries = [...schemaMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
140
|
+
|
|
141
|
+
for (const [filePath, fileSchema] of sortedEntries) {
|
|
142
|
+
const defs = fileSchema.definitions ?? {};
|
|
143
|
+
|
|
144
|
+
// Sort definition keys for consistent processing
|
|
145
|
+
Object.keys(defs).sort().forEach((key) => {
|
|
146
|
+
if (definitions[key] !== undefined) {
|
|
147
|
+
try {
|
|
148
|
+
assert.deepEqual(definitions[key], defs[key]);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
throw new DuplicateSymbolError(
|
|
151
|
+
`Duplicate symbol '${key}' found with different implementations`,
|
|
152
|
+
key,
|
|
153
|
+
filePath,
|
|
154
|
+
definitions[key],
|
|
155
|
+
defs[key]
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
definitions[key] = defs[key];
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public mergeSchemas(schemaMap: Map<string, Schema>): Schema {
|
|
165
|
+
const definitions: { [id: string]: Schema } = {};
|
|
166
|
+
let schemaVersion = "";
|
|
167
|
+
|
|
168
|
+
// Sort by file path for consistent processing order
|
|
169
|
+
const sortedEntries = [...schemaMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
170
|
+
|
|
171
|
+
for (const [, fileSchema] of sortedEntries) {
|
|
172
|
+
if (!schemaVersion && fileSchema["$schema"]) {
|
|
173
|
+
schemaVersion = fileSchema["$schema"];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const defs = fileSchema.definitions ?? {};
|
|
177
|
+
// Sort definition keys for consistent processing
|
|
178
|
+
Object.keys(defs).sort().forEach((key) => {
|
|
179
|
+
definitions[key] = defs[key] as Schema;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Sort definitions alphabetically
|
|
184
|
+
const sortedDefinitions: { [id: string]: Schema } = {};
|
|
185
|
+
Object.keys(definitions).sort().forEach(key => {
|
|
186
|
+
sortedDefinitions[key] = definitions[key];
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
$schema: schemaVersion || "http://json-schema.org/draft-07/schema#",
|
|
191
|
+
definitions: sortedDefinitions,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { SchemaWriter } from "./SchemaWriter";
|
|
4
|
+
import { CodeGenerationError } from "../errors";
|
|
5
|
+
|
|
6
|
+
const testDir = path.resolve(__dirname, "../../.test-tmp/schema-writer");
|
|
7
|
+
|
|
8
|
+
const cleanup = async () => {
|
|
9
|
+
if (fs.existsSync(testDir)) {
|
|
10
|
+
await fs.promises.rm(testDir, { recursive: true, force: true });
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
beforeEach(cleanup);
|
|
15
|
+
afterAll(cleanup);
|
|
16
|
+
|
|
17
|
+
describe("SchemaWriter", () => {
|
|
18
|
+
describe("writeJsonSchema", () => {
|
|
19
|
+
it("should write JSON schema with pretty formatting", async () => {
|
|
20
|
+
const writer = new SchemaWriter({
|
|
21
|
+
outputPath: testDir,
|
|
22
|
+
minify: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const schema: any = {
|
|
26
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
27
|
+
definitions: {
|
|
28
|
+
IUser: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
id: { type: "string" },
|
|
32
|
+
name: { type: "string" }
|
|
33
|
+
},
|
|
34
|
+
required: ["id", "name"],
|
|
35
|
+
additionalProperties: false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const outputFile = path.join(testDir, "schema.json");
|
|
41
|
+
await writer.writeJsonSchema(schema, outputFile);
|
|
42
|
+
|
|
43
|
+
expect(fs.existsSync(outputFile)).toBe(true);
|
|
44
|
+
|
|
45
|
+
const content = await fs.promises.readFile(outputFile, 'utf-8');
|
|
46
|
+
const parsed = JSON.parse(content);
|
|
47
|
+
|
|
48
|
+
expect(parsed).toEqual(schema);
|
|
49
|
+
// Pretty formatted should have indentation
|
|
50
|
+
expect(content).toMatch(/\n /);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should write minified JSON schema", async () => {
|
|
54
|
+
const writer = new SchemaWriter({
|
|
55
|
+
outputPath: testDir,
|
|
56
|
+
minify: true
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const schema: any = {
|
|
60
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
61
|
+
definitions: {
|
|
62
|
+
IUser: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
id: { type: "string" }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const outputFile = path.join(testDir, "schema.json");
|
|
72
|
+
await writer.writeJsonSchema(schema, outputFile);
|
|
73
|
+
|
|
74
|
+
expect(fs.existsSync(outputFile)).toBe(true);
|
|
75
|
+
|
|
76
|
+
const content = await fs.promises.readFile(outputFile, 'utf-8');
|
|
77
|
+
const parsed = JSON.parse(content);
|
|
78
|
+
|
|
79
|
+
expect(parsed).toEqual(schema);
|
|
80
|
+
// Minified should not have extra whitespace
|
|
81
|
+
expect(content).not.toMatch(/\n /);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should create output directory if it doesn't exist", async () => {
|
|
85
|
+
const nestedPath = path.join(testDir, "nested", "deep", "path");
|
|
86
|
+
const writer = new SchemaWriter({
|
|
87
|
+
outputPath: nestedPath,
|
|
88
|
+
minify: false
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const schema = { $schema: "test" };
|
|
92
|
+
const outputFile = path.join(nestedPath, "schema.json");
|
|
93
|
+
|
|
94
|
+
await writer.writeJsonSchema(schema, outputFile);
|
|
95
|
+
|
|
96
|
+
expect(fs.existsSync(outputFile)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should throw CodeGenerationError on write failure", async () => {
|
|
100
|
+
const writer = new SchemaWriter({
|
|
101
|
+
outputPath: testDir,
|
|
102
|
+
minify: false
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const schema = { $schema: "test" };
|
|
106
|
+
// Try to write to an invalid path (file as directory)
|
|
107
|
+
const invalidPath = path.join(testDir, "file.json", "invalid");
|
|
108
|
+
|
|
109
|
+
// Create file first to make path invalid
|
|
110
|
+
await fs.promises.mkdir(testDir, { recursive: true });
|
|
111
|
+
await fs.promises.writeFile(path.join(testDir, "file.json"), "test");
|
|
112
|
+
|
|
113
|
+
await expect(writer.writeJsonSchema(schema, invalidPath))
|
|
114
|
+
.rejects.toThrow(CodeGenerationError);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("writeTypeScriptFile", () => {
|
|
119
|
+
it("should write TypeScript file correctly", async () => {
|
|
120
|
+
const writer = new SchemaWriter({
|
|
121
|
+
outputPath: testDir,
|
|
122
|
+
minify: false
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const content = `export interface IUser {
|
|
126
|
+
id: string;
|
|
127
|
+
name: string;
|
|
128
|
+
}`;
|
|
129
|
+
|
|
130
|
+
const outputFile = path.join(testDir, "types.ts");
|
|
131
|
+
await writer.writeTypeScriptFile(content, outputFile);
|
|
132
|
+
|
|
133
|
+
expect(fs.existsSync(outputFile)).toBe(true);
|
|
134
|
+
|
|
135
|
+
const fileContent = await fs.promises.readFile(outputFile, 'utf-8');
|
|
136
|
+
expect(fileContent).toBe(content);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should create directories for TypeScript files", async () => {
|
|
140
|
+
const nestedPath = path.join(testDir, "src", "types");
|
|
141
|
+
const writer = new SchemaWriter({
|
|
142
|
+
outputPath: nestedPath,
|
|
143
|
+
minify: false
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const content = "export const test = 'value';";
|
|
147
|
+
const outputFile = path.join(nestedPath, "test.ts");
|
|
148
|
+
|
|
149
|
+
await writer.writeTypeScriptFile(content, outputFile);
|
|
150
|
+
|
|
151
|
+
expect(fs.existsSync(outputFile)).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should handle empty content", async () => {
|
|
155
|
+
const writer = new SchemaWriter({
|
|
156
|
+
outputPath: testDir,
|
|
157
|
+
minify: false
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const outputFile = path.join(testDir, "empty.ts");
|
|
161
|
+
await writer.writeTypeScriptFile("", outputFile);
|
|
162
|
+
|
|
163
|
+
expect(fs.existsSync(outputFile)).toBe(true);
|
|
164
|
+
|
|
165
|
+
const content = await fs.promises.readFile(outputFile, 'utf-8');
|
|
166
|
+
expect(content).toBe("");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("cleanOutputDirectory", () => {
|
|
171
|
+
it("should remove TypeScript and JSON files", async () => {
|
|
172
|
+
await fs.promises.mkdir(testDir, { recursive: true });
|
|
173
|
+
|
|
174
|
+
// Create various files
|
|
175
|
+
await fs.promises.writeFile(path.join(testDir, "schema.json"), "{}");
|
|
176
|
+
await fs.promises.writeFile(path.join(testDir, "types.ts"), "export interface Test {}");
|
|
177
|
+
await fs.promises.writeFile(path.join(testDir, "readme.md"), "# Test");
|
|
178
|
+
await fs.promises.writeFile(path.join(testDir, "config.yaml"), "test: value");
|
|
179
|
+
|
|
180
|
+
const writer = new SchemaWriter({
|
|
181
|
+
outputPath: testDir,
|
|
182
|
+
minify: false
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await writer.cleanOutputDirectory();
|
|
186
|
+
|
|
187
|
+
// Should remove .ts and .json files
|
|
188
|
+
expect(fs.existsSync(path.join(testDir, "schema.json"))).toBe(false);
|
|
189
|
+
expect(fs.existsSync(path.join(testDir, "types.ts"))).toBe(false);
|
|
190
|
+
|
|
191
|
+
// Should keep other files
|
|
192
|
+
expect(fs.existsSync(path.join(testDir, "readme.md"))).toBe(true);
|
|
193
|
+
expect(fs.existsSync(path.join(testDir, "config.yaml"))).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should handle non-existent directory", async () => {
|
|
197
|
+
const writer = new SchemaWriter({
|
|
198
|
+
outputPath: path.join(testDir, "nonexistent"),
|
|
199
|
+
minify: false
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Should not throw
|
|
203
|
+
await expect(writer.cleanOutputDirectory()).resolves.not.toThrow();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should not remove subdirectories", async () => {
|
|
207
|
+
await fs.promises.mkdir(path.join(testDir, "subdir"), { recursive: true });
|
|
208
|
+
await fs.promises.writeFile(path.join(testDir, "subdir", "file.ts"), "test");
|
|
209
|
+
await fs.promises.writeFile(path.join(testDir, "main.ts"), "test");
|
|
210
|
+
|
|
211
|
+
const writer = new SchemaWriter({
|
|
212
|
+
outputPath: testDir,
|
|
213
|
+
minify: false
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await writer.cleanOutputDirectory();
|
|
217
|
+
|
|
218
|
+
// Should remove file in root
|
|
219
|
+
expect(fs.existsSync(path.join(testDir, "main.ts"))).toBe(false);
|
|
220
|
+
|
|
221
|
+
// Should keep subdirectory and its contents
|
|
222
|
+
expect(fs.existsSync(path.join(testDir, "subdir"))).toBe(true);
|
|
223
|
+
expect(fs.existsSync(path.join(testDir, "subdir", "file.ts"))).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should handle permission errors gracefully", async () => {
|
|
227
|
+
// Skip this test as it's environment dependent
|
|
228
|
+
// Real permission errors would be caught by the try-catch in SchemaWriter
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("edge cases", () => {
|
|
233
|
+
it("should handle large files", async () => {
|
|
234
|
+
const writer = new SchemaWriter({
|
|
235
|
+
outputPath: testDir,
|
|
236
|
+
minify: false
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Create a large schema
|
|
240
|
+
const largeSchema: any = {
|
|
241
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
242
|
+
definitions: {}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Add many definitions
|
|
246
|
+
for (let i = 0; i < 1000; i++) {
|
|
247
|
+
largeSchema.definitions[`Interface${i}`] = {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
id: { type: "string" },
|
|
251
|
+
name: { type: "string" },
|
|
252
|
+
value: { type: "number" }
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const outputFile = path.join(testDir, "large-schema.json");
|
|
258
|
+
await writer.writeJsonSchema(largeSchema, outputFile);
|
|
259
|
+
|
|
260
|
+
expect(fs.existsSync(outputFile)).toBe(true);
|
|
261
|
+
|
|
262
|
+
const stats = await fs.promises.stat(outputFile);
|
|
263
|
+
expect(stats.size).toBeGreaterThan(1000); // Should be reasonably large
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should handle unicode content", async () => {
|
|
267
|
+
const writer = new SchemaWriter({
|
|
268
|
+
outputPath: testDir,
|
|
269
|
+
minify: false
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const unicodeContent = `export interface IUser {
|
|
273
|
+
// Unicode: café, naïve, résumé
|
|
274
|
+
name: string; // 中文注释
|
|
275
|
+
emoji: "👨💻" | "🚀" | "💡";
|
|
276
|
+
}`;
|
|
277
|
+
|
|
278
|
+
const outputFile = path.join(testDir, "unicode.ts");
|
|
279
|
+
await writer.writeTypeScriptFile(unicodeContent, outputFile);
|
|
280
|
+
|
|
281
|
+
const content = await fs.promises.readFile(outputFile, 'utf-8');
|
|
282
|
+
expect(content).toBe(unicodeContent);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should overwrite existing files", async () => {
|
|
286
|
+
const writer = new SchemaWriter({
|
|
287
|
+
outputPath: testDir,
|
|
288
|
+
minify: false
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const outputFile = path.join(testDir, "overwrite.ts");
|
|
292
|
+
|
|
293
|
+
// Write first content
|
|
294
|
+
await writer.writeTypeScriptFile("const first = 1;", outputFile);
|
|
295
|
+
const firstContent = await fs.promises.readFile(outputFile, 'utf-8');
|
|
296
|
+
expect(firstContent).toBe("const first = 1;");
|
|
297
|
+
|
|
298
|
+
// Overwrite with second content
|
|
299
|
+
await writer.writeTypeScriptFile("const second = 2;", outputFile);
|
|
300
|
+
const secondContent = await fs.promises.readFile(outputFile, 'utf-8');
|
|
301
|
+
expect(secondContent).toBe("const second = 2;");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Schema } from "ts-json-schema-generator";
|
|
4
|
+
import { CodeGenerationError } from "../errors";
|
|
5
|
+
|
|
6
|
+
export interface SchemaWriterOptions {
|
|
7
|
+
outputPath: string;
|
|
8
|
+
minify?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SchemaWriter {
|
|
12
|
+
constructor(private options: SchemaWriterOptions) {}
|
|
13
|
+
|
|
14
|
+
public async writeJsonSchema(
|
|
15
|
+
schema: Schema,
|
|
16
|
+
outputFile: string
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
await this.ensureOutputPath();
|
|
20
|
+
|
|
21
|
+
const content = this.options.minify
|
|
22
|
+
? JSON.stringify(schema)
|
|
23
|
+
: JSON.stringify(schema, null, 4);
|
|
24
|
+
|
|
25
|
+
await fs.promises.writeFile(outputFile, content, 'utf-8');
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new CodeGenerationError(
|
|
28
|
+
`Failed to write JSON schema: ${error instanceof Error ? error.message : String(error)}`,
|
|
29
|
+
outputFile
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async writeTypeScriptFile(
|
|
35
|
+
content: string,
|
|
36
|
+
outputFile: string
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
try {
|
|
39
|
+
await this.ensureOutputPath();
|
|
40
|
+
await fs.promises.writeFile(outputFile, content, 'utf-8');
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new CodeGenerationError(
|
|
43
|
+
`Failed to write TypeScript file: ${error instanceof Error ? error.message : String(error)}`,
|
|
44
|
+
outputFile
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async ensureOutputPath(): Promise<void> {
|
|
50
|
+
if (!fs.existsSync(this.options.outputPath)) {
|
|
51
|
+
await fs.promises.mkdir(this.options.outputPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async cleanOutputDirectory(): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
if (fs.existsSync(this.options.outputPath)) {
|
|
58
|
+
const files = await fs.promises.readdir(this.options.outputPath);
|
|
59
|
+
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const filePath = path.join(this.options.outputPath, file);
|
|
62
|
+
const stat = await fs.promises.stat(filePath);
|
|
63
|
+
|
|
64
|
+
if (stat.isFile() && (file.endsWith('.ts') || file.endsWith('.json'))) {
|
|
65
|
+
await fs.promises.unlink(filePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new CodeGenerationError(
|
|
71
|
+
`Failed to clean output directory: ${error instanceof Error ? error.message : String(error)}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|