apify-schema-tools 2.0.4 → 2.1.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/.cspell/custom-dictionary.txt +1 -0
- package/.husky/pre-commit +0 -8
- package/CHANGELOG.md +21 -0
- package/README.md +109 -45
- package/biome.json +121 -9
- package/dist/apify-schema-tools.d.ts +1 -0
- package/dist/apify-schema-tools.d.ts.map +1 -1
- package/dist/apify-schema-tools.js +133 -106
- package/dist/apify-schema-tools.js.map +1 -1
- package/dist/apify.d.ts +1 -1
- package/dist/apify.d.ts.map +1 -1
- package/dist/apify.js +4 -2
- package/dist/apify.js.map +1 -1
- package/dist/configuration.d.ts +17 -16
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js +8 -1
- package/dist/configuration.js.map +1 -1
- package/dist/json-schemas.d.ts.map +1 -1
- package/dist/json-schemas.js +31 -21
- package/dist/json-schemas.js.map +1 -1
- package/dist/typescript.d.ts +1 -0
- package/dist/typescript.d.ts.map +1 -1
- package/dist/typescript.js +294 -194
- package/dist/typescript.js.map +1 -1
- package/package.json +16 -5
- package/samples/all-defaults/src/generated/dataset.ts +1 -0
- package/samples/all-defaults/src/generated/input-utils.ts +5 -1
- package/samples/all-defaults/src/generated/input.ts +2 -0
- package/samples/all-defaults/src-schemas/input.json +6 -3
- package/samples/deep-merged-schemas/src/generated/dataset.ts +1 -0
- package/samples/deep-merged-schemas/src/generated/input-utils.ts +5 -1
- package/samples/deep-merged-schemas/src/generated/input.ts +2 -0
- package/samples/deep-merged-schemas/src-schemas/input.json +6 -3
- package/samples/merged-schemas/src/generated/dataset.ts +1 -0
- package/samples/merged-schemas/src/generated/input-utils.ts +5 -1
- package/samples/merged-schemas/src/generated/input.ts +3 -0
- package/samples/merged-schemas/src-schemas/input.json +6 -3
- package/samples/package-json-config/custom-src-schemas/input.json +6 -3
- package/samples/package-json-config/package.json +8 -2
- package/samples/package-json-config/src/custom-generated/dataset.ts +1 -0
- package/samples/package-json-config/src/custom-generated/input-utils.ts +5 -1
- package/samples/package-json-config/src/custom-generated/input.ts +2 -0
- package/src/apify-schema-tools.ts +179 -150
- package/src/apify.ts +6 -4
- package/src/configuration.ts +14 -5
- package/src/json-schemas.ts +44 -22
- package/src/typescript.ts +370 -207
- package/test/apify-schema-tools.test.ts +122 -124
- package/test/apify.test.ts +8 -6
- package/test/common.ts +2 -2
- package/test/configuration.test.ts +1 -1
- package/test/json-schemas.test.ts +56 -40
- package/test/typescript.test.ts +181 -31
- package/tsconfig.json +1 -1
package/src/typescript.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { JSONSchema4 } from "json-schema";
|
|
2
|
-
import
|
|
2
|
+
import { equals } from "ramda";
|
|
3
3
|
import { readFile, writeFile } from "./filesystem.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isArraySchema, isObjectSchema, type ObjectSchema } from "./json-schemas.js";
|
|
5
5
|
|
|
6
6
|
const TYPESCRIPT_FILE_HEADER = `\
|
|
7
7
|
/**
|
|
@@ -57,6 +57,7 @@ function isTypeScriptEnum(schema: TypeScriptSchema): schema is TypeScriptEnum {
|
|
|
57
57
|
|
|
58
58
|
export interface TypeScriptInterface extends TypeScriptBasicData {
|
|
59
59
|
properties: Record<string, TypeScriptSchema>;
|
|
60
|
+
additionalProperties?: TypeScriptSchema;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function isTypeScriptInterface(schema: TypeScriptSchema): schema is TypeScriptInterface {
|
|
@@ -65,34 +66,51 @@ function isTypeScriptInterface(schema: TypeScriptSchema): schema is TypeScriptIn
|
|
|
65
66
|
|
|
66
67
|
export type TypeScriptSchema = TypeScriptBasicType | TypeScriptEnum | TypeScriptInterface;
|
|
67
68
|
|
|
68
|
-
function
|
|
69
|
-
schema:
|
|
69
|
+
function createTypeScriptInterface(
|
|
70
|
+
schema: ObjectSchema,
|
|
71
|
+
doc: string | undefined,
|
|
70
72
|
isArray: boolean,
|
|
71
73
|
isRequired: boolean,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
): TypeScriptInterface {
|
|
75
|
+
const requiredProperties = schema.required ?? [];
|
|
76
|
+
|
|
77
|
+
const properties = Object.fromEntries(
|
|
78
|
+
Object.entries(schema.properties ?? {}).map(([key, value]) => {
|
|
79
|
+
return [key, jsonSchemaToTypeScriptSchema(value, false, requiredProperties.includes(key))];
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
let additionalProperties: TypeScriptSchema | undefined;
|
|
84
|
+
if (schema.additionalProperties === true || schema.additionalProperties === undefined) {
|
|
85
|
+
// If additionalProperties is true or undefined, we parse it as an "empty" schema
|
|
86
|
+
additionalProperties = { doc: undefined, isArray: false, isRequired: true, type: "unknown" };
|
|
87
|
+
} else if (schema.additionalProperties !== false) {
|
|
88
|
+
additionalProperties = jsonSchemaToTypeScriptSchema(schema.additionalProperties, false, true);
|
|
83
89
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
|
|
91
|
+
return { doc, isArray, isRequired, properties, additionalProperties };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createTypeScriptArraySchema(
|
|
95
|
+
schema: JSONSchema4 & { type: "array" },
|
|
96
|
+
doc: string | undefined,
|
|
97
|
+
isRequired: boolean,
|
|
98
|
+
): TypeScriptSchema {
|
|
99
|
+
if (Array.isArray(schema.items)) {
|
|
100
|
+
throw new Error("Array schema with multiple items is not supported");
|
|
92
101
|
}
|
|
93
|
-
if (schema.
|
|
94
|
-
return { doc, isArray, isRequired,
|
|
102
|
+
if (!schema.items) {
|
|
103
|
+
return { doc, isArray: true, isRequired, type: "unknown" };
|
|
95
104
|
}
|
|
105
|
+
return jsonSchemaToTypeScriptSchema(schema.items, true, isRequired, doc);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createTypeScriptBasicType(
|
|
109
|
+
schema: JSONSchema4,
|
|
110
|
+
doc: string | undefined,
|
|
111
|
+
isArray: boolean,
|
|
112
|
+
isRequired: boolean,
|
|
113
|
+
): TypeScriptBasicType {
|
|
96
114
|
const types: AllowedType[] = [];
|
|
97
115
|
for (const type of Array.isArray(schema.type) ? schema.type : [schema.type ?? "unknown"]) {
|
|
98
116
|
const tsType = JSON_SCHEMA_TYPE_TO_TS_TYPE[type] ?? type;
|
|
@@ -112,41 +130,76 @@ function jsonSchemaToTypeScriptSchema(
|
|
|
112
130
|
};
|
|
113
131
|
}
|
|
114
132
|
|
|
133
|
+
function jsonSchemaToTypeScriptSchema(
|
|
134
|
+
schema: JSONSchema4,
|
|
135
|
+
isArray: boolean,
|
|
136
|
+
isRequired: boolean,
|
|
137
|
+
inheritedDoc?: string,
|
|
138
|
+
): TypeScriptSchema {
|
|
139
|
+
const doc = inheritedDoc ?? schema.description;
|
|
140
|
+
|
|
141
|
+
if (isObjectSchema(schema)) {
|
|
142
|
+
return createTypeScriptInterface(schema, doc, isArray, isRequired);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isArraySchema(schema)) {
|
|
146
|
+
return createTypeScriptArraySchema(schema, doc, isRequired);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (schema.enum) {
|
|
150
|
+
return { doc, isArray, isRequired, enum: schema.enum };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return createTypeScriptBasicType(schema, doc, isArray, isRequired);
|
|
154
|
+
}
|
|
155
|
+
|
|
115
156
|
export function jsonSchemaToTypeScriptInterface(schema: ObjectSchema): TypeScriptInterface {
|
|
116
157
|
return jsonSchemaToTypeScriptSchema(schema, false, true) as TypeScriptInterface;
|
|
117
158
|
}
|
|
118
159
|
|
|
119
|
-
|
|
160
|
+
function serializeInterfaceSchema(schema: TypeScriptInterface, indentLevel: number): string {
|
|
120
161
|
const indent = "\t".repeat(indentLevel);
|
|
121
|
-
|
|
162
|
+
const properties = Object.entries(schema.properties);
|
|
163
|
+
if (properties.length === 0) {
|
|
164
|
+
return "{}";
|
|
165
|
+
}
|
|
166
|
+
const innerIndentLevel = indentLevel + 1;
|
|
167
|
+
const innerIndent = "\t".repeat(innerIndentLevel);
|
|
168
|
+
let result = "{";
|
|
169
|
+
for (const [key, value] of properties) {
|
|
170
|
+
if (value.doc) {
|
|
171
|
+
result += `\n${innerIndent}/**\n${innerIndent} * ${value.doc}\n${innerIndent} */`;
|
|
172
|
+
}
|
|
173
|
+
result += `\n${innerIndent}${key}${value.isRequired ? "" : "?"}: ${serializeTypeScriptSchema(value, innerIndentLevel)};`;
|
|
174
|
+
}
|
|
175
|
+
if (schema.additionalProperties) {
|
|
176
|
+
result += `\n${innerIndent}[key: string]: ${serializeTypeScriptSchema(schema.additionalProperties, innerIndentLevel)};`;
|
|
177
|
+
}
|
|
178
|
+
result += `\n${indent}}`;
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function serializeEnumSchema(schema: TypeScriptEnum): string {
|
|
183
|
+
const serializedEnum = JSON.stringify(schema.enum).replace(/\[|\]/g, "").replace(/,/g, " | ");
|
|
184
|
+
return schema.isArray && schema.enum.length > 1 ? `(${serializedEnum})` : serializedEnum;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function serializeBasicTypeSchema(schema: TypeScriptBasicType): string {
|
|
188
|
+
if (Array.isArray(schema.type)) {
|
|
189
|
+
return schema.isArray ? `(${schema.type.join(" | ")})` : schema.type.join(" | ");
|
|
190
|
+
}
|
|
191
|
+
return schema.type;
|
|
192
|
+
}
|
|
122
193
|
|
|
194
|
+
export function serializeTypeScriptSchema(schema: TypeScriptSchema, indentLevel = 0): string {
|
|
195
|
+
let result = "";
|
|
123
196
|
if (isTypeScriptInterface(schema)) {
|
|
124
|
-
|
|
125
|
-
if (properties.length === 0) {
|
|
126
|
-
return "{}";
|
|
127
|
-
}
|
|
128
|
-
const innerIndentLevel = indentLevel + 1;
|
|
129
|
-
const innerIndent = "\t".repeat(innerIndentLevel);
|
|
130
|
-
result += "{";
|
|
131
|
-
for (const [key, value] of properties) {
|
|
132
|
-
if (value.doc) {
|
|
133
|
-
result += `\n${innerIndent}/**\n${innerIndent} * ${value.doc}\n${innerIndent} */`;
|
|
134
|
-
}
|
|
135
|
-
result += `\n${innerIndent}${key}${
|
|
136
|
-
value.isRequired ? "" : "?"
|
|
137
|
-
}: ${serializeTypeScriptSchema(value, innerIndentLevel)};`;
|
|
138
|
-
}
|
|
139
|
-
result += `\n${indent}}`;
|
|
197
|
+
result = serializeInterfaceSchema(schema, indentLevel);
|
|
140
198
|
} else if (isTypeScriptEnum(schema)) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const serializedTypes = schema.type.join(" | ");
|
|
145
|
-
result += schema.isArray ? `(${serializedTypes})` : serializedTypes;
|
|
146
|
-
} else {
|
|
147
|
-
result += schema.type;
|
|
199
|
+
result = serializeEnumSchema(schema);
|
|
200
|
+
} else if (isTypeScriptBasicType(schema)) {
|
|
201
|
+
result = serializeBasicTypeSchema(schema);
|
|
148
202
|
}
|
|
149
|
-
|
|
150
203
|
if (schema.isArray) {
|
|
151
204
|
result += "[]";
|
|
152
205
|
}
|
|
@@ -159,9 +212,11 @@ export function serializeTypeScriptInterface(name: string, schema: TypeScriptInt
|
|
|
159
212
|
return `${docComment}export interface ${name} ${serializedSchema}${schema.isRequired ? "" : " | undefined"}`;
|
|
160
213
|
}
|
|
161
214
|
|
|
215
|
+
const TYPESCRIPT_HEADER_REGEX = /\/\*\*[\s\S]*?\*\//;
|
|
216
|
+
|
|
162
217
|
export function removeTypeScriptHeader(fileContent: string): string {
|
|
163
218
|
// Match the first jsdoc and remove it
|
|
164
|
-
const headerMatch = fileContent.match(
|
|
219
|
+
const headerMatch = fileContent.match(TYPESCRIPT_HEADER_REGEX);
|
|
165
220
|
if (!headerMatch || headerMatch.index === undefined) {
|
|
166
221
|
throw new Error("No header found in the TypeScript file");
|
|
167
222
|
}
|
|
@@ -169,149 +224,293 @@ export function removeTypeScriptHeader(fileContent: string): string {
|
|
|
169
224
|
return fileContent.slice(headerEndIndex).trim();
|
|
170
225
|
}
|
|
171
226
|
|
|
172
|
-
|
|
173
|
-
const properties: Record<string, TypeScriptSchema> = {};
|
|
174
|
-
let i = 0;
|
|
175
|
-
let currentDoc: string | undefined;
|
|
227
|
+
const WHITESPACE_REGEX = /\s/;
|
|
176
228
|
|
|
177
|
-
|
|
178
|
-
|
|
229
|
+
function skipWhitespace(body: string, i: number): number {
|
|
230
|
+
let cursor = i;
|
|
231
|
+
while (cursor < body.length && WHITESPACE_REGEX.test(body[cursor])) {
|
|
232
|
+
cursor++;
|
|
233
|
+
}
|
|
234
|
+
return cursor;
|
|
235
|
+
}
|
|
179
236
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
237
|
+
const TYPESCRIPT_DOC_DECORATIONS_REGEX = /^\s*\*\s?/;
|
|
238
|
+
const TYPESCRIPT_DOC_PREFIX = "/**";
|
|
239
|
+
|
|
240
|
+
function tryParseJsDoc(body: string, i: number): { doc: string | undefined; nextIndex: number } | null {
|
|
241
|
+
if (body.slice(i, i + TYPESCRIPT_DOC_PREFIX.length) === TYPESCRIPT_DOC_PREFIX) {
|
|
242
|
+
const docEnd = body.indexOf("*/", i);
|
|
243
|
+
if (docEnd !== -1) {
|
|
244
|
+
const docContent = body.slice(i + TYPESCRIPT_DOC_PREFIX.length, docEnd);
|
|
245
|
+
const docLines = docContent.split("\n");
|
|
246
|
+
const cleanDoc = docLines
|
|
247
|
+
.map((line) => line.replace(TYPESCRIPT_DOC_DECORATIONS_REGEX, "").trim())
|
|
248
|
+
.filter((line) => line.length > 0)
|
|
249
|
+
.join(" ");
|
|
250
|
+
return { doc: cleanDoc || undefined, nextIndex: docEnd + 2 };
|
|
184
251
|
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
185
255
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
continue;
|
|
256
|
+
function extractTypeString(body: string, i: number): { typeStr: string; isArray: boolean; typeEnd: number } {
|
|
257
|
+
let braceDepth = 0;
|
|
258
|
+
let typeEnd = i;
|
|
259
|
+
|
|
260
|
+
while (typeEnd < body.length) {
|
|
261
|
+
const char = body[typeEnd];
|
|
262
|
+
if (char === "{") {
|
|
263
|
+
braceDepth++;
|
|
264
|
+
} else if (char === "}") {
|
|
265
|
+
braceDepth--;
|
|
266
|
+
if (braceDepth < 0) {
|
|
267
|
+
break;
|
|
199
268
|
}
|
|
269
|
+
} else if (char === ";" && braceDepth === 0) {
|
|
270
|
+
break;
|
|
200
271
|
}
|
|
272
|
+
typeEnd++;
|
|
273
|
+
}
|
|
201
274
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Find the type
|
|
210
|
-
const typeStart = i;
|
|
211
|
-
let braceDepth = 0;
|
|
212
|
-
let typeEnd = i;
|
|
213
|
-
|
|
214
|
-
while (typeEnd < body.length) {
|
|
215
|
-
const char = body[typeEnd];
|
|
216
|
-
if (char === "{") {
|
|
217
|
-
braceDepth++;
|
|
218
|
-
} else if (char === "}") {
|
|
219
|
-
braceDepth--;
|
|
220
|
-
if (braceDepth < 0) {
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
} else if (char === ";" && braceDepth === 0) {
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
typeEnd++;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let typeStr = body.slice(typeStart, typeEnd).trim();
|
|
230
|
-
const isArray = typeStr.endsWith("[]");
|
|
231
|
-
if (isArray) {
|
|
232
|
-
typeStr = typeStr.slice(0, -2).trim(); // Remove the "[]" part
|
|
233
|
-
}
|
|
275
|
+
let typeStr = body.slice(i, typeEnd).trim();
|
|
276
|
+
const isArray = typeStr.endsWith("[]");
|
|
277
|
+
if (isArray) {
|
|
278
|
+
typeStr = typeStr.slice(0, -2).trim();
|
|
279
|
+
}
|
|
280
|
+
return { typeStr, isArray, typeEnd };
|
|
281
|
+
}
|
|
234
282
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
isRequired,
|
|
268
|
-
enum: enumValues,
|
|
269
|
-
};
|
|
270
|
-
} catch {
|
|
271
|
-
throw new Error(`Invalid enum format for property "${propName}": ${typeStr}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
} else {
|
|
275
|
-
// Primitive type
|
|
283
|
+
function extractSchemaFromText(
|
|
284
|
+
text: string,
|
|
285
|
+
currentDoc: string | undefined,
|
|
286
|
+
isRequired: boolean,
|
|
287
|
+
isArray: boolean,
|
|
288
|
+
): TypeScriptSchema {
|
|
289
|
+
let schema: TypeScriptSchema;
|
|
290
|
+
|
|
291
|
+
if (text.startsWith("{") && text.endsWith("}")) {
|
|
292
|
+
const objectBody = text.slice(1, -1);
|
|
293
|
+
const { properties: nestedProperties, additionalProperties: additionalNestedProperties } =
|
|
294
|
+
parseInterfaceProperties(objectBody);
|
|
295
|
+
schema = {
|
|
296
|
+
doc: currentDoc,
|
|
297
|
+
isArray,
|
|
298
|
+
isRequired,
|
|
299
|
+
properties: nestedProperties,
|
|
300
|
+
additionalProperties: additionalNestedProperties,
|
|
301
|
+
};
|
|
302
|
+
} else if (text.includes("|")) {
|
|
303
|
+
const unionBody = isArray ? text.slice(1, -1) : text;
|
|
304
|
+
const unionParts = unionBody.split("|").map((part) => part.trim());
|
|
305
|
+
if (unionParts.every((part) => isAllowedType(part))) {
|
|
306
|
+
schema = {
|
|
307
|
+
doc: currentDoc,
|
|
308
|
+
isArray,
|
|
309
|
+
isRequired,
|
|
310
|
+
type: unionParts,
|
|
311
|
+
};
|
|
312
|
+
} else {
|
|
313
|
+
try {
|
|
314
|
+
const enumValues = JSON.parse(`[${unionBody.replace(/\|/g, ",")}]`);
|
|
276
315
|
schema = {
|
|
277
316
|
doc: currentDoc,
|
|
278
317
|
isArray,
|
|
279
318
|
isRequired,
|
|
280
|
-
|
|
319
|
+
enum: enumValues,
|
|
281
320
|
};
|
|
321
|
+
} catch {
|
|
322
|
+
throw new Error(`Invalid enum format for TypeScript text: ${text}`);
|
|
282
323
|
}
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
schema = {
|
|
327
|
+
doc: currentDoc,
|
|
328
|
+
isArray,
|
|
329
|
+
isRequired,
|
|
330
|
+
type: text,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
283
333
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
334
|
+
return schema;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const TYPESCRIPT_ADDITIONAL_PROPERTIES_REGEX = /^\s*\[\s*key\s*:\s*string\s*\]\s*:\s*/;
|
|
338
|
+
const TYPESCRIPT_PROPERTY_REGEX = /^(\w+)(\?)?:\s*/;
|
|
339
|
+
|
|
340
|
+
function tryParseProperty(
|
|
341
|
+
body: string,
|
|
342
|
+
i: number,
|
|
343
|
+
currentDoc: string | undefined,
|
|
344
|
+
): { propName?: string; schema: TypeScriptSchema; nextIndex: number } | null {
|
|
345
|
+
let cursor = i;
|
|
346
|
+
|
|
347
|
+
let propName: string | undefined;
|
|
348
|
+
let isRequired: boolean;
|
|
349
|
+
|
|
350
|
+
const bodySlice = body.slice(cursor);
|
|
351
|
+
const additionalPropertiesMatch = bodySlice.match(TYPESCRIPT_ADDITIONAL_PROPERTIES_REGEX);
|
|
352
|
+
if (additionalPropertiesMatch) {
|
|
353
|
+
cursor += additionalPropertiesMatch[0].length;
|
|
354
|
+
isRequired = true;
|
|
355
|
+
} else {
|
|
356
|
+
const propMatch = bodySlice.match(TYPESCRIPT_PROPERTY_REGEX);
|
|
357
|
+
if (!propMatch) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
propName = propMatch[1];
|
|
362
|
+
isRequired = !propMatch[2];
|
|
363
|
+
cursor += propMatch[0].length;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const { typeStr, isArray, typeEnd } = extractTypeString(body, cursor);
|
|
367
|
+
const schema = extractSchemaFromText(typeStr, currentDoc, isRequired, isArray);
|
|
368
|
+
|
|
369
|
+
let nextIndex = typeEnd;
|
|
370
|
+
if (body[nextIndex] === ";") {
|
|
371
|
+
nextIndex++;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { propName, schema, nextIndex };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function parseInterfaceProperties(body: string): {
|
|
378
|
+
properties: Record<string, TypeScriptSchema>;
|
|
379
|
+
additionalProperties?: TypeScriptSchema;
|
|
380
|
+
} {
|
|
381
|
+
const properties: Record<string, TypeScriptSchema> = {};
|
|
382
|
+
let additionalProperties: TypeScriptSchema | undefined;
|
|
383
|
+
let i = 0;
|
|
384
|
+
let currentDoc: string | undefined;
|
|
385
|
+
|
|
386
|
+
while (i < body.length) {
|
|
387
|
+
i = skipWhitespace(body, i);
|
|
287
388
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
389
|
+
const docResult = tryParseJsDoc(body, i);
|
|
390
|
+
if (docResult) {
|
|
391
|
+
currentDoc = docResult.doc;
|
|
392
|
+
i = docResult.nextIndex;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const propResult = tryParseProperty(body, i, currentDoc);
|
|
397
|
+
if (propResult) {
|
|
398
|
+
if (propResult.propName) {
|
|
399
|
+
properties[propResult.propName] = propResult.schema;
|
|
400
|
+
} else {
|
|
401
|
+
additionalProperties = propResult.schema;
|
|
291
402
|
}
|
|
292
|
-
|
|
293
|
-
i
|
|
403
|
+
currentDoc = undefined;
|
|
404
|
+
i = propResult.nextIndex;
|
|
405
|
+
continue;
|
|
294
406
|
}
|
|
407
|
+
|
|
408
|
+
i++;
|
|
295
409
|
}
|
|
296
410
|
|
|
297
|
-
return properties;
|
|
411
|
+
return { properties, additionalProperties };
|
|
298
412
|
}
|
|
299
413
|
|
|
414
|
+
const TYPESCRIPT_JSDOC_REGEX = /\/\*\*\s*\n\s*\*\s*(.*?)\s*\n\s*\*\//;
|
|
415
|
+
const TYPESCRIPT_INTERFACE_REGEX = /export interface (\w+) \{([\s\S]*)\}$/;
|
|
416
|
+
|
|
300
417
|
export function parseTypeScriptInterface(text: string): TypeScriptInterface {
|
|
301
418
|
// Extract JSDoc comment
|
|
302
|
-
const docMatch = text.match(
|
|
419
|
+
const docMatch = text.match(TYPESCRIPT_JSDOC_REGEX);
|
|
303
420
|
const doc = docMatch ? docMatch[1] : undefined;
|
|
304
421
|
|
|
305
422
|
// Extract interface declaration
|
|
306
|
-
const interfaceMatch = text.match(
|
|
423
|
+
const interfaceMatch = text.match(TYPESCRIPT_INTERFACE_REGEX);
|
|
307
424
|
if (!interfaceMatch) {
|
|
308
425
|
throw new Error("Invalid TypeScript interface format");
|
|
309
426
|
}
|
|
310
427
|
|
|
311
428
|
const body = interfaceMatch[2];
|
|
312
|
-
const properties = parseInterfaceProperties(body);
|
|
429
|
+
const { properties, additionalProperties } = parseInterfaceProperties(body);
|
|
430
|
+
|
|
431
|
+
return { doc, isArray: false, isRequired: true, properties, additionalProperties };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function compareInterfaceProperty(
|
|
435
|
+
key: string,
|
|
436
|
+
propA: TypeScriptInterface,
|
|
437
|
+
propB: TypeScriptSchema,
|
|
438
|
+
ignoreDocs?: boolean,
|
|
439
|
+
): boolean {
|
|
440
|
+
if (!isTypeScriptInterface(propB)) {
|
|
441
|
+
console.error(`Property "${key}" is an interface in one schema but not in the other.`);
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (!compareTypescriptInterfaces(propA, propB, ignoreDocs)) {
|
|
445
|
+
console.error(`Property "${key}" interfaces do not match.`);
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function compareEnumProperty(key: string, propA: TypeScriptSchema, propB: TypeScriptSchema): boolean {
|
|
452
|
+
if (!isTypeScriptEnum(propB)) {
|
|
453
|
+
console.error(`Property "${key}" is an enum in one schema but not in the other.`);
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
if (JSON.stringify((propA as TypeScriptEnum).enum) !== JSON.stringify((propB as TypeScriptEnum).enum)) {
|
|
457
|
+
console.error(
|
|
458
|
+
`Property "${key}" enums do not match: ${JSON.stringify((propA as TypeScriptEnum).enum)} vs ${JSON.stringify((propB as TypeScriptEnum).enum)}`,
|
|
459
|
+
);
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
313
464
|
|
|
314
|
-
|
|
465
|
+
function compareBasicTypeProperty(key: string, propA: TypeScriptSchema, propB: TypeScriptSchema): boolean {
|
|
466
|
+
if (!isTypeScriptBasicType(propB)) {
|
|
467
|
+
console.error(`Property "${key}" is a basic type in one schema but not in the other.`);
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
if (!equals((propA as TypeScriptBasicType).type, (propB as TypeScriptBasicType).type)) {
|
|
471
|
+
console.error(
|
|
472
|
+
`Property "${key}" types do not match: ${JSON.stringify((propA as TypeScriptBasicType).type)} vs ${JSON.stringify((propB as TypeScriptBasicType).type)}`,
|
|
473
|
+
);
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function compareProperty(key: string, propA: TypeScriptSchema, propB: TypeScriptSchema, ignoreDocs?: boolean): boolean {
|
|
480
|
+
if (propA.isArray !== propB.isArray) {
|
|
481
|
+
console.error(`Property "${key}" has different array status: ${propA.isArray} vs ${propB.isArray}`);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (propA.isRequired !== propB.isRequired) {
|
|
486
|
+
console.error(`Property "${key}" has different required status: ${propA.isRequired} vs ${propB.isRequired}`);
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!ignoreDocs && propA.doc !== propB.doc) {
|
|
491
|
+
console.error(`Property "${key}" has different documentation: "${propA.doc}" vs "${propB.doc}"`);
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (isTypeScriptInterface(propA)) {
|
|
496
|
+
return compareInterfaceProperty(key, propA, propB, ignoreDocs);
|
|
497
|
+
}
|
|
498
|
+
if (isTypeScriptEnum(propA)) {
|
|
499
|
+
return compareEnumProperty(key, propA, propB);
|
|
500
|
+
}
|
|
501
|
+
return compareBasicTypeProperty(key, propA, propB);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function compareAdditionalProperties(a: TypeScriptInterface, b: TypeScriptInterface, ignoreDocs?: boolean): boolean {
|
|
505
|
+
if (!(a.additionalProperties || b.additionalProperties)) {
|
|
506
|
+
return true; // Both have no additional properties
|
|
507
|
+
}
|
|
508
|
+
if (!(a.additionalProperties && b.additionalProperties)) {
|
|
509
|
+
console.error(`One interface has additionalProperties defined, but the other does not.
|
|
510
|
+
If you generated an interface from a JSON schema, pay attention that "additionalProperties" is set to an empty schema by default.`);
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
return compareProperty("[key: string]", a.additionalProperties, b.additionalProperties, ignoreDocs);
|
|
315
514
|
}
|
|
316
515
|
|
|
317
516
|
export function compareTypescriptInterfaces(
|
|
@@ -325,57 +524,21 @@ export function compareTypescriptInterfaces(
|
|
|
325
524
|
}
|
|
326
525
|
|
|
327
526
|
for (const key in a.properties) {
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
const propA = a.properties[key];
|
|
332
|
-
const propB = b.properties[key];
|
|
333
|
-
|
|
334
|
-
if (propA.isArray !== propB.isArray) {
|
|
335
|
-
console.error(`Property "${key}" has different array status: ${propA.isArray} vs ${propB.isArray}`);
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (propA.isRequired !== propB.isRequired) {
|
|
340
|
-
console.error(`Property "${key}" has different required status: ${propA.isRequired} vs ${propB.isRequired}`);
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (!ignoreDocs && propA.doc !== propB.doc) {
|
|
345
|
-
console.error(`Property "${key}" has different documentation: "${propA.doc}" vs "${propB.doc}"`);
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (isTypeScriptInterface(propA)) {
|
|
350
|
-
if (!isTypeScriptInterface(propB)) {
|
|
351
|
-
console.error(`Property "${key}" is an interface in one schema but not in the other.`);
|
|
352
|
-
return false;
|
|
353
|
-
}
|
|
354
|
-
if (!compareTypescriptInterfaces(propA, propB, ignoreDocs)) {
|
|
355
|
-
console.error(`Property "${key}" interfaces do not match.`);
|
|
527
|
+
if (Object.hasOwn(a.properties, key)) {
|
|
528
|
+
if (!(key in b.properties)) {
|
|
356
529
|
return false;
|
|
357
530
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
531
|
+
const propA = a.properties[key];
|
|
532
|
+
const propB = b.properties[key];
|
|
533
|
+
if (!compareProperty(key, propA, propB, ignoreDocs)) {
|
|
361
534
|
return false;
|
|
362
535
|
}
|
|
363
|
-
if (JSON.stringify(propA.enum) !== JSON.stringify(propB.enum)) {
|
|
364
|
-
console.error(
|
|
365
|
-
`Property "${key}" enums do not match: ${JSON.stringify(propA.enum)} vs ${JSON.stringify(propB.enum)}`,
|
|
366
|
-
);
|
|
367
|
-
return false;
|
|
368
|
-
}
|
|
369
|
-
} else if (!isTypeScriptBasicType(propB)) {
|
|
370
|
-
console.error(`Property "${key}" is a basic type in one schema but not in the other.`);
|
|
371
|
-
return false;
|
|
372
|
-
} else if (!R.equals(propA.type, propB.type)) {
|
|
373
|
-
console.error(
|
|
374
|
-
`Property "${key}" types do not match: ${JSON.stringify(propA.type)} vs ${JSON.stringify(propB.type)}`,
|
|
375
|
-
);
|
|
376
|
-
return false;
|
|
377
536
|
}
|
|
378
537
|
}
|
|
379
538
|
|
|
539
|
+
if (!compareAdditionalProperties(a, b, ignoreDocs)) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
380
543
|
return true;
|
|
381
544
|
}
|