apify-schema-tools 3.1.0 → 3.2.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/.node-version +1 -1
- package/CHANGELOG.md +7 -1
- package/biome.json +8 -2
- package/dist/apify-schema-tools.js +12 -9
- 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 +19 -5
- package/dist/apify.js.map +1 -1
- package/dist/cli/check.d.ts +5 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +86 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +92 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/sync.d.ts +5 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +112 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/configuration.d.ts +14 -5
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js +9 -5
- package/dist/configuration.js.map +1 -1
- package/dist/main.d.ts +4 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +19 -0
- package/dist/main.js.map +1 -0
- package/dist/middle-schema/compare-schemas.d.ts +3 -0
- package/dist/middle-schema/compare-schemas.d.ts.map +1 -0
- package/dist/middle-schema/compare-schemas.js +90 -0
- package/dist/middle-schema/compare-schemas.js.map +1 -0
- package/dist/middle-schema/generate-typescript.d.ts +7 -0
- package/dist/middle-schema/generate-typescript.d.ts.map +1 -0
- package/dist/middle-schema/generate-typescript.js +70 -0
- package/dist/middle-schema/generate-typescript.js.map +1 -0
- package/dist/middle-schema/parse-json-schema.d.ts +4 -0
- package/dist/middle-schema/parse-json-schema.d.ts.map +1 -0
- package/dist/middle-schema/parse-json-schema.js +65 -0
- package/dist/middle-schema/parse-json-schema.js.map +1 -0
- package/dist/middle-schema/parse-typescript.d.ts +4 -0
- package/dist/middle-schema/parse-typescript.d.ts.map +1 -0
- package/dist/middle-schema/parse-typescript.js +199 -0
- package/dist/middle-schema/parse-typescript.js.map +1 -0
- package/dist/middle-schema/schema-types.d.ts +24 -0
- package/dist/middle-schema/schema-types.d.ts.map +1 -0
- package/dist/middle-schema/schema-types.js +14 -0
- package/dist/middle-schema/schema-types.js.map +1 -0
- package/dist/middle-schema/schema.d.ts +24 -0
- package/dist/middle-schema/schema.d.ts.map +1 -0
- package/dist/middle-schema/schema.js +14 -0
- package/dist/middle-schema/schema.js.map +1 -0
- package/dist/schema/entities/abstract-entity.d.ts +5 -0
- package/dist/schema/entities/abstract-entity.d.ts.map +1 -0
- package/dist/schema/entities/abstract-entity.js +3 -0
- package/dist/schema/entities/abstract-entity.js.map +1 -0
- package/dist/schema/entities/primitive-union.d.ts +12 -0
- package/dist/schema/entities/primitive-union.d.ts.map +1 -0
- package/dist/schema/entities/primitive-union.js +74 -0
- package/dist/schema/entities/primitive-union.js.map +1 -0
- package/dist/schema/entities/primitive.d.ts +15 -0
- package/dist/schema/entities/primitive.d.ts.map +1 -0
- package/dist/schema/entities/primitive.js +54 -0
- package/dist/schema/entities/primitive.js.map +1 -0
- package/dist/schema/parsers/json-schema.d.ts +4 -0
- package/dist/schema/parsers/json-schema.d.ts.map +1 -0
- package/dist/schema/parsers/json-schema.js +12 -0
- package/dist/schema/parsers/json-schema.js.map +1 -0
- package/dist/schema/parsers/typescript.d.ts +3 -0
- package/dist/schema/parsers/typescript.d.ts.map +1 -0
- package/dist/schema/parsers/typescript.js +24 -0
- package/dist/schema/parsers/typescript.js.map +1 -0
- package/dist/schemas/input.d.ts +840 -0
- package/dist/schemas/input.d.ts.map +1 -0
- package/dist/schemas/input.js +349 -0
- package/dist/schemas/input.js.map +1 -0
- package/dist/utils/filesystem.d.ts +8 -0
- package/dist/utils/filesystem.d.ts.map +1 -0
- package/dist/utils/filesystem.js +16 -0
- package/dist/utils/filesystem.js.map +1 -0
- package/dist/utils/json-schemas-interactive-conflict.d.ts +16 -0
- package/dist/utils/json-schemas-interactive-conflict.d.ts.map +1 -0
- package/dist/utils/json-schemas-interactive-conflict.js +165 -0
- package/dist/utils/json-schemas-interactive-conflict.js.map +1 -0
- package/dist/utils/json-schemas.d.ts +42 -0
- package/dist/utils/json-schemas.d.ts.map +1 -0
- package/dist/utils/json-schemas.js +162 -0
- package/dist/utils/json-schemas.js.map +1 -0
- package/dist/zod/schemas/input.d.ts +840 -0
- package/dist/zod/schemas/input.d.ts.map +1 -0
- package/dist/zod/schemas/input.js +393 -0
- package/dist/zod/schemas/input.js.map +1 -0
- package/package.json +12 -12
- package/samples/all-defaults/.actor/input_schema.json +32 -3
- package/samples/all-defaults/src-schemas/input.json +2 -1
- package/samples/deep-merged-schemas/.actor/input_schema.json +36 -3
- package/samples/merged-schemas/.actor/input_schema.json +27 -3
- package/samples/package-json-config/.actor/input_schema.json +32 -3
- package/samples/package-json-config-merged/.actor/input_schema.json +36 -3
- package/src/apify.ts +21 -6
- package/src/cli/check.ts +114 -0
- package/src/cli/init.ts +125 -0
- package/src/cli/sync.ts +164 -0
- package/src/configuration.ts +17 -7
- package/src/main.ts +25 -0
- package/src/middle-schema/compare-schemas.ts +113 -0
- package/src/middle-schema/generate-typescript.ts +88 -0
- package/src/middle-schema/parse-json-schema.ts +104 -0
- package/src/middle-schema/parse-typescript.ts +239 -0
- package/src/middle-schema/schema-types.ts +40 -0
- package/test/apify.test.ts +410 -2
- package/test/cli/check.test.ts +1571 -0
- package/test/cli/init.test.ts +459 -0
- package/test/cli/sync.test.ts +341 -0
- package/test/common.ts +68 -0
- package/test/configuration.test.ts +8 -8
- package/test/middle-schema/compare-schemas.test.ts +585 -0
- package/test/middle-schema/generate-typescript.test.ts +191 -0
- package/test/middle-schema/parse-json-schema.test.ts +178 -0
- package/test/middle-schema/parse-typescript.test.ts +143 -0
- package/test/{json-schema-conflicts.test.ts → utils/json-schemas-interactive-conflict.test.ts} +2 -2
- package/test/{json-schemas.test.ts → utils/json-schemas.test.ts} +3 -3
- package/src/apify-schema-tools.ts +0 -420
- package/src/typescript.ts +0 -563
- package/test/apify-schema-tools.test.ts +0 -2216
- package/test/typescript.test.ts +0 -1079
- /package/src/{filesystem.ts → utils/filesystem.ts} +0 -0
- /package/src/{json-schema-conflicts.ts → utils/json-schemas-interactive-conflict.ts} +0 -0
- /package/src/{json-schemas.ts → utils/json-schemas.ts} +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { equals } from "ramda";
|
|
2
|
+
import {
|
|
3
|
+
isMiddleBasicVar,
|
|
4
|
+
isMiddleEnum,
|
|
5
|
+
isMiddleObject,
|
|
6
|
+
type MiddleBasicVar,
|
|
7
|
+
type MiddleEnum,
|
|
8
|
+
type MiddleObject,
|
|
9
|
+
type MiddleSchema,
|
|
10
|
+
} from "./schema-types.js";
|
|
11
|
+
|
|
12
|
+
function compareObjectProperties(key: string, propA: MiddleObject, propB: MiddleSchema, ignoreDocs?: boolean): boolean {
|
|
13
|
+
if (!isMiddleObject(propB)) {
|
|
14
|
+
console.error(`Property "${key}" is an interface in one schema but not in the other.`);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (!compareMiddleObjects(propA, propB, ignoreDocs)) {
|
|
18
|
+
console.error(`Property "${key}" interfaces do not match.`);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function compareEnumProperties(key: string, propA: MiddleSchema, propB: MiddleSchema): boolean {
|
|
25
|
+
if (!isMiddleEnum(propB)) {
|
|
26
|
+
console.error(`Property "${key}" is an enum in one schema but not in the other.`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (JSON.stringify((propA as MiddleEnum).enum) !== JSON.stringify((propB as MiddleEnum).enum)) {
|
|
30
|
+
console.error(
|
|
31
|
+
`Property "${key}" enums do not match: ${JSON.stringify((propA as MiddleEnum).enum)} vs ${JSON.stringify((propB as MiddleEnum).enum)}`,
|
|
32
|
+
);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compareBasicVarProperties(key: string, propA: MiddleSchema, propB: MiddleSchema): boolean {
|
|
39
|
+
if (!isMiddleBasicVar(propB)) {
|
|
40
|
+
console.error(`Property "${key}" is a basic var in one schema but not in the other.`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (!equals((propA as MiddleBasicVar).type, (propB as MiddleBasicVar).type)) {
|
|
44
|
+
console.error(
|
|
45
|
+
`Property "${key}" types do not match: ${JSON.stringify((propA as MiddleBasicVar).type)} vs ${JSON.stringify((propB as MiddleBasicVar).type)}`,
|
|
46
|
+
);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function compareProperties(key: string, propA: MiddleSchema, propB: MiddleSchema, ignoreDocs?: boolean): boolean {
|
|
53
|
+
if (propA.isArray !== propB.isArray) {
|
|
54
|
+
console.error(`Property "${key}" has different array status: ${propA.isArray} vs ${propB.isArray}`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (propA.isRequired !== propB.isRequired) {
|
|
59
|
+
console.error(`Property "${key}" has different required status: ${propA.isRequired} vs ${propB.isRequired}`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!ignoreDocs && propA.doc !== propB.doc) {
|
|
64
|
+
console.error(`Property "${key}" has different documentation: "${propA.doc}" vs "${propB.doc}"`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isMiddleObject(propA)) {
|
|
69
|
+
return compareObjectProperties(key, propA, propB, ignoreDocs);
|
|
70
|
+
}
|
|
71
|
+
if (isMiddleEnum(propA)) {
|
|
72
|
+
return compareEnumProperties(key, propA, propB);
|
|
73
|
+
}
|
|
74
|
+
return compareBasicVarProperties(key, propA, propB);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compareAdditionalProperties(a: MiddleObject, b: MiddleObject, ignoreDocs?: boolean): boolean {
|
|
78
|
+
if (!(a.additionalProperties || b.additionalProperties)) {
|
|
79
|
+
return true; // Both have no additional properties
|
|
80
|
+
}
|
|
81
|
+
if (!(a.additionalProperties && b.additionalProperties)) {
|
|
82
|
+
console.error(`One interface has additionalProperties defined, but the other does not.
|
|
83
|
+
If you generated an interface from a JSON schema, pay attention that "additionalProperties" is set to an empty schema by default.`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return compareProperties("[key: string]", a.additionalProperties, b.additionalProperties, ignoreDocs);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function compareMiddleObjects(a: MiddleObject, b: MiddleObject, ignoreDocs?: boolean): boolean {
|
|
90
|
+
if (Object.keys(a.properties).length !== Object.keys(b.properties).length) {
|
|
91
|
+
console.error("Interfaces have different number of properties.");
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const key in a.properties) {
|
|
96
|
+
if (Object.hasOwn(a.properties, key)) {
|
|
97
|
+
if (!(key in b.properties)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const propA = a.properties[key];
|
|
101
|
+
const propB = b.properties[key];
|
|
102
|
+
if (!compareProperties(key, propA, propB, ignoreDocs)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!compareAdditionalProperties(a, b, ignoreDocs)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile, writeFile } from "../utils/filesystem.js";
|
|
2
|
+
import {
|
|
3
|
+
isMiddleBasicVar,
|
|
4
|
+
isMiddleEnum,
|
|
5
|
+
isMiddleObject,
|
|
6
|
+
type MiddleBasicVar,
|
|
7
|
+
type MiddleEnum,
|
|
8
|
+
type MiddleObject,
|
|
9
|
+
type MiddleSchema,
|
|
10
|
+
} from "./schema-types.js";
|
|
11
|
+
|
|
12
|
+
export const TYPESCRIPT_FILE_HEADER = `\
|
|
13
|
+
/**
|
|
14
|
+
* This file was automatically generated by apify-schema-tools.
|
|
15
|
+
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
|
16
|
+
* and run apify-schema-tools' "sync" command to regenerate this file.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
function serializeInterfaceSchema(schema: MiddleObject, indentLevel: number): string {
|
|
22
|
+
const indent = "\t".repeat(indentLevel);
|
|
23
|
+
const properties = Object.entries(schema.properties);
|
|
24
|
+
if (properties.length === 0 && !schema.additionalProperties) {
|
|
25
|
+
return "{}";
|
|
26
|
+
}
|
|
27
|
+
const innerIndentLevel = indentLevel + 1;
|
|
28
|
+
const innerIndent = "\t".repeat(innerIndentLevel);
|
|
29
|
+
let result = "{";
|
|
30
|
+
for (const [key, value] of properties) {
|
|
31
|
+
if (value.doc) {
|
|
32
|
+
result += `\n${innerIndent}/**\n${innerIndent} * ${value.doc}\n${innerIndent} */`;
|
|
33
|
+
}
|
|
34
|
+
result += `\n${innerIndent}${key}${
|
|
35
|
+
value.isRequired ? "" : "?"
|
|
36
|
+
}: ${serializeMiddleSchemaToTypeScript(value, innerIndentLevel)};`;
|
|
37
|
+
}
|
|
38
|
+
if (schema.additionalProperties) {
|
|
39
|
+
result += `\n${innerIndent}[key: string]: ${serializeMiddleSchemaToTypeScript(
|
|
40
|
+
schema.additionalProperties,
|
|
41
|
+
innerIndentLevel,
|
|
42
|
+
)};`;
|
|
43
|
+
}
|
|
44
|
+
result += `\n${indent}}`;
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function serializeEnumSchema(schema: MiddleEnum): string {
|
|
49
|
+
const serializedEnum = JSON.stringify(schema.enum).replace(/\[|\]/g, "").replace(/,/g, " | ");
|
|
50
|
+
return schema.isArray && schema.enum.length > 1 ? `(${serializedEnum})` : serializedEnum;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function serializeBasicVarSchema(schema: MiddleBasicVar): string {
|
|
54
|
+
if (Array.isArray(schema.type)) {
|
|
55
|
+
return schema.isArray ? `(${schema.type.join(" | ")})` : schema.type.join(" | ");
|
|
56
|
+
}
|
|
57
|
+
return schema.type;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function serializeMiddleSchemaToTypeScript(schema: MiddleSchema, indentLevel = 0): string {
|
|
61
|
+
let result = "";
|
|
62
|
+
if (isMiddleObject(schema)) {
|
|
63
|
+
result = serializeInterfaceSchema(schema, indentLevel);
|
|
64
|
+
} else if (isMiddleEnum(schema)) {
|
|
65
|
+
result = serializeEnumSchema(schema);
|
|
66
|
+
} else if (isMiddleBasicVar(schema)) {
|
|
67
|
+
result = serializeBasicVarSchema(schema);
|
|
68
|
+
}
|
|
69
|
+
if (schema.isArray) {
|
|
70
|
+
result += "[]";
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function serializeMiddleObjectToTypeScript(name: string, schema: MiddleObject): string {
|
|
76
|
+
const serializedSchema = serializeMiddleSchemaToTypeScript(schema);
|
|
77
|
+
const docComment = schema.doc ? `/**\n * ${schema.doc}\n */\n` : "";
|
|
78
|
+
return `${docComment}export interface ${name} ${serializedSchema}${schema.isRequired ? "" : " | undefined"}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function writeTypeScriptFile(filePath: string, content: string): void {
|
|
82
|
+
writeFile(filePath, TYPESCRIPT_FILE_HEADER + content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readGeneratedTypeScriptFile(filePath: string): string {
|
|
86
|
+
const content = readFile(filePath);
|
|
87
|
+
return content.replace(TYPESCRIPT_FILE_HEADER, "");
|
|
88
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { JSONSchema4 } from "json-schema";
|
|
2
|
+
import { isArraySchema, isObjectSchema, type ObjectSchema } from "../utils/json-schemas.js";
|
|
3
|
+
import {
|
|
4
|
+
type AllowedType,
|
|
5
|
+
isAllowedType,
|
|
6
|
+
type MiddleBasicVar,
|
|
7
|
+
type MiddleObject,
|
|
8
|
+
type MiddleSchema,
|
|
9
|
+
} from "./schema-types.js";
|
|
10
|
+
|
|
11
|
+
const JSON_SCHEMA_TYPE_TO_TS_TYPE: Record<string, string> = {
|
|
12
|
+
integer: "number",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function generateMiddleObject(
|
|
16
|
+
schema: ObjectSchema,
|
|
17
|
+
doc: string | undefined,
|
|
18
|
+
isArray: boolean,
|
|
19
|
+
isRequired: boolean,
|
|
20
|
+
): MiddleObject {
|
|
21
|
+
const requiredProperties = schema.required ?? [];
|
|
22
|
+
|
|
23
|
+
const properties = Object.fromEntries(
|
|
24
|
+
Object.entries(schema.properties ?? {}).map(([key, value]) => {
|
|
25
|
+
return [key, jsonSchemaToMiddleSchema(value, false, requiredProperties.includes(key))];
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
let additionalProperties: MiddleSchema | undefined;
|
|
30
|
+
if (schema.additionalProperties === true || schema.additionalProperties === undefined) {
|
|
31
|
+
// If additionalProperties is true or undefined, we parse it as an "empty" schema
|
|
32
|
+
additionalProperties = { doc: undefined, isArray: false, isRequired: true, type: "unknown" };
|
|
33
|
+
} else if (schema.additionalProperties !== false) {
|
|
34
|
+
additionalProperties = jsonSchemaToMiddleSchema(schema.additionalProperties, false, true);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { doc, isArray, isRequired, properties, additionalProperties };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generateArrayMiddleSchema(
|
|
41
|
+
schema: JSONSchema4 & { type: "array" },
|
|
42
|
+
doc: string | undefined,
|
|
43
|
+
isRequired: boolean,
|
|
44
|
+
): MiddleSchema {
|
|
45
|
+
if (Array.isArray(schema.items)) {
|
|
46
|
+
throw new Error("Array schema with multiple items is not supported");
|
|
47
|
+
}
|
|
48
|
+
if (!schema.items) {
|
|
49
|
+
return { doc, isArray: true, isRequired, type: "unknown" };
|
|
50
|
+
}
|
|
51
|
+
return jsonSchemaToMiddleSchema(schema.items, true, isRequired, doc);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function generateMiddleBasicVar(
|
|
55
|
+
schema: JSONSchema4,
|
|
56
|
+
doc: string | undefined,
|
|
57
|
+
isArray: boolean,
|
|
58
|
+
isRequired: boolean,
|
|
59
|
+
): MiddleBasicVar {
|
|
60
|
+
const types: AllowedType[] = [];
|
|
61
|
+
for (const type of Array.isArray(schema.type) ? schema.type : [schema.type ?? "unknown"]) {
|
|
62
|
+
const tsType = JSON_SCHEMA_TYPE_TO_TS_TYPE[type] ?? type;
|
|
63
|
+
if (!isAllowedType(tsType)) {
|
|
64
|
+
throw new Error(`Unsupported JSON schema type: ${type}`);
|
|
65
|
+
}
|
|
66
|
+
types.push(tsType);
|
|
67
|
+
}
|
|
68
|
+
if (schema.nullable) {
|
|
69
|
+
types.push("null");
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
doc,
|
|
73
|
+
isArray,
|
|
74
|
+
isRequired,
|
|
75
|
+
type: types.length === 1 ? types[0] : types,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function jsonSchemaToMiddleSchema(
|
|
80
|
+
schema: JSONSchema4,
|
|
81
|
+
isArray: boolean,
|
|
82
|
+
isRequired: boolean,
|
|
83
|
+
inheritedDoc?: string,
|
|
84
|
+
): MiddleSchema {
|
|
85
|
+
const doc = inheritedDoc ?? schema.description;
|
|
86
|
+
|
|
87
|
+
if (isObjectSchema(schema)) {
|
|
88
|
+
return generateMiddleObject(schema, doc, isArray, isRequired);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isArraySchema(schema)) {
|
|
92
|
+
return generateArrayMiddleSchema(schema, doc, isRequired);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (schema.enum) {
|
|
96
|
+
return { doc, isArray, isRequired, enum: schema.enum };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return generateMiddleBasicVar(schema, doc, isArray, isRequired);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function jsonSchemaToMiddleObject(schema: ObjectSchema): MiddleObject {
|
|
103
|
+
return jsonSchemaToMiddleSchema(schema, false, true) as MiddleObject;
|
|
104
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { isAllowedType, type MiddleObject, type MiddleSchema } from "./schema-types.js";
|
|
2
|
+
|
|
3
|
+
const TYPESCRIPT_HEADER_REGEX = /\/\*\*[\s\S]*?\*\//;
|
|
4
|
+
|
|
5
|
+
export function removeTypeScriptHeader(fileContent: string): string {
|
|
6
|
+
// Match the first jsdoc and remove it
|
|
7
|
+
const headerMatch = fileContent.match(TYPESCRIPT_HEADER_REGEX);
|
|
8
|
+
if (!headerMatch || headerMatch.index === undefined) {
|
|
9
|
+
throw new Error("No header found in the TypeScript file");
|
|
10
|
+
}
|
|
11
|
+
const headerEndIndex = headerMatch.index + headerMatch[0].length;
|
|
12
|
+
return fileContent.slice(headerEndIndex).trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const WHITESPACE_REGEX = /\s/;
|
|
16
|
+
|
|
17
|
+
function skipWhitespace(body: string, i: number): number {
|
|
18
|
+
let cursor = i;
|
|
19
|
+
while (cursor < body.length && WHITESPACE_REGEX.test(body[cursor])) {
|
|
20
|
+
cursor++;
|
|
21
|
+
}
|
|
22
|
+
return cursor;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TYPESCRIPT_DOC_DECORATIONS_REGEX = /^\s*\*\s?/;
|
|
26
|
+
const TYPESCRIPT_DOC_PREFIX = "/**";
|
|
27
|
+
|
|
28
|
+
function tryParseJsDoc(body: string, i: number): { doc: string | undefined; nextIndex: number } | null {
|
|
29
|
+
if (body.slice(i, i + TYPESCRIPT_DOC_PREFIX.length) === TYPESCRIPT_DOC_PREFIX) {
|
|
30
|
+
const docEnd = body.indexOf("*/", i);
|
|
31
|
+
if (docEnd !== -1) {
|
|
32
|
+
const docContent = body.slice(i + TYPESCRIPT_DOC_PREFIX.length, docEnd);
|
|
33
|
+
const docLines = docContent.split("\n");
|
|
34
|
+
const cleanDoc = docLines
|
|
35
|
+
.map((line) => line.replace(TYPESCRIPT_DOC_DECORATIONS_REGEX, "").trim())
|
|
36
|
+
.filter((line) => line.length > 0)
|
|
37
|
+
.join(" ");
|
|
38
|
+
return { doc: cleanDoc || undefined, nextIndex: docEnd + 2 };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractTypeString(
|
|
45
|
+
body: string,
|
|
46
|
+
i: number,
|
|
47
|
+
): { typeStr: string; isArray: boolean; isObject: boolean; typeEnd: number } {
|
|
48
|
+
let braceDepth = 0;
|
|
49
|
+
let typeEnd = i;
|
|
50
|
+
|
|
51
|
+
while (typeEnd < body.length) {
|
|
52
|
+
const char = body[typeEnd];
|
|
53
|
+
if (char === "{") {
|
|
54
|
+
braceDepth++;
|
|
55
|
+
} else if (char === "}") {
|
|
56
|
+
braceDepth--;
|
|
57
|
+
if (braceDepth < 0) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
} else if (char === ";" && braceDepth === 0) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
typeEnd++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let typeStr = body.slice(i, typeEnd).trim();
|
|
67
|
+
const isArray = typeStr.endsWith("[]");
|
|
68
|
+
if (isArray) {
|
|
69
|
+
typeStr = typeStr.slice(0, -2).trim();
|
|
70
|
+
}
|
|
71
|
+
const isObject = typeStr.startsWith("{") && typeStr.endsWith("}");
|
|
72
|
+
if (isObject) {
|
|
73
|
+
typeStr = typeStr.slice(1, -1).trim();
|
|
74
|
+
}
|
|
75
|
+
return { typeStr, isArray, isObject, typeEnd };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseTypeScriptValue(text: string) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(text.trim());
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error(`Invalid TypeScript value: ${text}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractSchemaFromText(
|
|
87
|
+
text: string,
|
|
88
|
+
currentDoc: string | undefined,
|
|
89
|
+
isRequired: boolean,
|
|
90
|
+
isArray: boolean,
|
|
91
|
+
isObject: boolean,
|
|
92
|
+
): MiddleSchema {
|
|
93
|
+
let schema: MiddleSchema;
|
|
94
|
+
|
|
95
|
+
if (isObject) {
|
|
96
|
+
const { properties: nestedProperties, additionalProperties: additionalNestedProperties } =
|
|
97
|
+
parseInterfaceProperties(text);
|
|
98
|
+
schema = {
|
|
99
|
+
doc: currentDoc,
|
|
100
|
+
isArray,
|
|
101
|
+
isRequired,
|
|
102
|
+
properties: nestedProperties,
|
|
103
|
+
additionalProperties: additionalNestedProperties,
|
|
104
|
+
};
|
|
105
|
+
} else if (text.includes("|")) {
|
|
106
|
+
const unionBody = isArray ? text.slice(1, -1) : text;
|
|
107
|
+
const unionParts = unionBody.split("|").map((part) => part.trim());
|
|
108
|
+
|
|
109
|
+
// Since we currently do not support complex unions, we check if all parts are allowed types
|
|
110
|
+
if (unionParts.every((part) => isAllowedType(part))) {
|
|
111
|
+
schema = {
|
|
112
|
+
doc: currentDoc,
|
|
113
|
+
isArray,
|
|
114
|
+
isRequired,
|
|
115
|
+
type: unionParts,
|
|
116
|
+
};
|
|
117
|
+
} else {
|
|
118
|
+
schema = {
|
|
119
|
+
doc: currentDoc,
|
|
120
|
+
isArray,
|
|
121
|
+
isRequired,
|
|
122
|
+
enum: unionParts.map((value) => parseTypeScriptValue(value)),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
} else if (isAllowedType(text)) {
|
|
126
|
+
schema = {
|
|
127
|
+
doc: currentDoc,
|
|
128
|
+
isArray,
|
|
129
|
+
isRequired,
|
|
130
|
+
type: text,
|
|
131
|
+
};
|
|
132
|
+
} else {
|
|
133
|
+
schema = {
|
|
134
|
+
doc: currentDoc,
|
|
135
|
+
isArray,
|
|
136
|
+
isRequired,
|
|
137
|
+
enum: [parseTypeScriptValue(text)],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return schema;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const TYPESCRIPT_ADDITIONAL_PROPERTIES_REGEX = /^\s*\[\s*key\s*:\s*string\s*\]\s*:\s*/;
|
|
145
|
+
const TYPESCRIPT_PROPERTY_REGEX = /^(\w+)(\?)?:\s*/;
|
|
146
|
+
|
|
147
|
+
function tryParseProperty(
|
|
148
|
+
body: string,
|
|
149
|
+
i: number,
|
|
150
|
+
currentDoc: string | undefined,
|
|
151
|
+
): { propName?: string; schema: MiddleSchema; nextIndex: number } | null {
|
|
152
|
+
let cursor = i;
|
|
153
|
+
|
|
154
|
+
let propName: string | undefined;
|
|
155
|
+
let isRequired: boolean;
|
|
156
|
+
|
|
157
|
+
const bodySlice = body.slice(cursor);
|
|
158
|
+
const additionalPropertiesMatch = bodySlice.match(TYPESCRIPT_ADDITIONAL_PROPERTIES_REGEX);
|
|
159
|
+
if (additionalPropertiesMatch) {
|
|
160
|
+
cursor += additionalPropertiesMatch[0].length;
|
|
161
|
+
isRequired = true;
|
|
162
|
+
} else {
|
|
163
|
+
const propMatch = bodySlice.match(TYPESCRIPT_PROPERTY_REGEX);
|
|
164
|
+
if (!propMatch) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
propName = propMatch[1];
|
|
169
|
+
isRequired = !propMatch[2];
|
|
170
|
+
cursor += propMatch[0].length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { typeStr, isArray, isObject, typeEnd } = extractTypeString(body, cursor);
|
|
174
|
+
const schema = extractSchemaFromText(typeStr, currentDoc, isRequired, isArray, isObject);
|
|
175
|
+
|
|
176
|
+
let nextIndex = typeEnd;
|
|
177
|
+
if (body[nextIndex] === ";") {
|
|
178
|
+
nextIndex++;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { propName, schema, nextIndex };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseInterfaceProperties(body: string): {
|
|
185
|
+
properties: Record<string, MiddleSchema>;
|
|
186
|
+
additionalProperties?: MiddleSchema;
|
|
187
|
+
} {
|
|
188
|
+
const properties: Record<string, MiddleSchema> = {};
|
|
189
|
+
let additionalProperties: MiddleSchema | undefined;
|
|
190
|
+
let i = 0;
|
|
191
|
+
let currentDoc: string | undefined;
|
|
192
|
+
|
|
193
|
+
while (i < body.length) {
|
|
194
|
+
i = skipWhitespace(body, i);
|
|
195
|
+
|
|
196
|
+
const docResult = tryParseJsDoc(body, i);
|
|
197
|
+
if (docResult) {
|
|
198
|
+
currentDoc = docResult.doc;
|
|
199
|
+
i = docResult.nextIndex;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const propResult = tryParseProperty(body, i, currentDoc);
|
|
204
|
+
if (propResult) {
|
|
205
|
+
if (propResult.propName) {
|
|
206
|
+
properties[propResult.propName] = propResult.schema;
|
|
207
|
+
} else {
|
|
208
|
+
additionalProperties = propResult.schema;
|
|
209
|
+
}
|
|
210
|
+
currentDoc = undefined;
|
|
211
|
+
i = propResult.nextIndex;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
i++;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { properties, additionalProperties };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const TYPESCRIPT_JSDOC_REGEX = /\/\*\*\s*\n\s*\*\s*(.*?)\s*\n\s*\*\//;
|
|
222
|
+
const TYPESCRIPT_INTERFACE_REGEX = /export interface (\w+) \{([\s\S]*)\}$/;
|
|
223
|
+
|
|
224
|
+
export function parseTypeScriptInterface(text: string): MiddleObject {
|
|
225
|
+
// Extract JSDoc comment
|
|
226
|
+
const docMatch = text.match(TYPESCRIPT_JSDOC_REGEX);
|
|
227
|
+
const doc = docMatch ? docMatch[1] : undefined;
|
|
228
|
+
|
|
229
|
+
// Extract interface declaration
|
|
230
|
+
const interfaceMatch = text.match(TYPESCRIPT_INTERFACE_REGEX);
|
|
231
|
+
if (!interfaceMatch) {
|
|
232
|
+
throw new Error("Invalid TypeScript interface format");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const body = interfaceMatch[2];
|
|
236
|
+
const { properties, additionalProperties } = parseInterfaceProperties(body);
|
|
237
|
+
|
|
238
|
+
return { doc, isArray: false, isRequired: true, properties, additionalProperties };
|
|
239
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const ALLOWED_TYPES = ["string", "number", "boolean", "object", "null", "unknown"] as const;
|
|
2
|
+
|
|
3
|
+
export type AllowedType = (typeof ALLOWED_TYPES)[number];
|
|
4
|
+
|
|
5
|
+
export function isAllowedType(type: string): type is AllowedType {
|
|
6
|
+
return ALLOWED_TYPES.includes(type as AllowedType);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MiddleBasicSpecs {
|
|
10
|
+
doc?: string;
|
|
11
|
+
isRequired: boolean;
|
|
12
|
+
isArray: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MiddleBasicVar extends MiddleBasicSpecs {
|
|
16
|
+
type: AllowedType | AllowedType[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isMiddleBasicVar(schema: MiddleSchema): schema is MiddleBasicVar {
|
|
20
|
+
return "type" in schema;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MiddleEnum extends MiddleBasicSpecs {
|
|
24
|
+
enum: unknown[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isMiddleEnum(schema: MiddleSchema): schema is MiddleEnum {
|
|
28
|
+
return "enum" in schema;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MiddleObject extends MiddleBasicSpecs {
|
|
32
|
+
properties: Record<string, MiddleSchema>;
|
|
33
|
+
additionalProperties?: MiddleSchema;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isMiddleObject(schema: MiddleSchema): schema is MiddleObject {
|
|
37
|
+
return "properties" in schema;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type MiddleSchema = MiddleBasicVar | MiddleEnum | MiddleObject;
|