@amritk/generate-parsers 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generators/build-schema.d.ts +5 -1
- package/dist/generators/generate-files.d.ts +15 -2
- package/dist/helpers/collect-helpers.d.ts +16 -2
- package/dist/index.js +64 -19
- package/package.json +3 -2
- package/src/generators/build-schema.test.ts +830 -0
- package/src/generators/build-schema.ts +280 -0
- package/src/generators/generate-default-from-pattern.test.ts +81 -0
- package/src/generators/generate-default-from-pattern.ts +39 -0
- package/src/generators/generate-discriminated-union-validation.test.ts +84 -0
- package/src/generators/generate-discriminated-union-validation.ts +54 -0
- package/src/generators/generate-enum-check.test.ts +45 -0
- package/src/generators/generate-enum-check.ts +8 -0
- package/src/generators/generate-files.test.ts +435 -0
- package/src/generators/generate-files.ts +136 -0
- package/src/generators/generate-non-discriminated-union-validation.test.ts +74 -0
- package/src/generators/generate-non-discriminated-union-validation.ts +40 -0
- package/src/generators/generate-parser-function.test.ts +1798 -0
- package/src/generators/generate-parser-function.ts +1319 -0
- package/src/generators/generate-schema-checks.test.ts +322 -0
- package/src/generators/generate-schema-checks.ts +114 -0
- package/src/generators/generate-strict-assertion.ts +210 -0
- package/src/generators/generate-validation-expression.test.ts +794 -0
- package/src/generators/generate-validation-expression.ts +286 -0
- package/src/helpers/apply-schema-extensions.test.ts +194 -0
- package/src/helpers/apply-schema-extensions.ts +63 -0
- package/src/helpers/collect-helpers.test.ts +103 -0
- package/src/helpers/collect-helpers.ts +64 -0
- package/src/helpers/collect-imports.test.ts +587 -0
- package/src/helpers/collect-imports.ts +268 -0
- package/src/helpers/find-discriminator.test.ts +88 -0
- package/src/helpers/find-discriminator.ts +39 -0
- package/src/helpers/get-default-value.test.ts +135 -0
- package/src/helpers/get-default-value.ts +89 -0
- package/src/helpers/get-discriminator-value.test.ts +83 -0
- package/src/helpers/get-discriminator-value.ts +21 -0
- package/src/index.ts +1 -0
- package/src/types/schema-extensions.ts +35 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
import type { HelpersMode } from '#helpers/collect-helpers';
|
|
2
3
|
import type { SchemaExtensions } from '#types/schema-extensions';
|
|
3
4
|
/**
|
|
4
5
|
* Represents a generated file with its filename and content.
|
|
@@ -30,6 +31,9 @@ export type GeneratedFile = {
|
|
|
30
31
|
* @param strict - When true, the generated parsers throw on type/shape mismatches
|
|
31
32
|
* (wrong type, missing required property, enum/pattern/min/max violations) instead
|
|
32
33
|
* of coercing invalid input to default values.
|
|
34
|
+
* @param helpersMode - `'package'` (default) emits `import ... from '@amritk/helpers/...'`.
|
|
35
|
+
* `'embedded'` emits `import ... from './_helpers/...'` and appends the helper sources
|
|
36
|
+
* as additional `GeneratedFile` entries so the output directory is self-contained.
|
|
33
37
|
* @returns An array of generated TypeScript files
|
|
34
38
|
*
|
|
35
39
|
* @example
|
|
@@ -62,4 +66,4 @@ export type GeneratedFile = {
|
|
|
62
66
|
* });
|
|
63
67
|
* ```
|
|
64
68
|
*/
|
|
65
|
-
export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean) => Promise<GeneratedFile[]>;
|
|
69
|
+
export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode) => Promise<GeneratedFile[]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
import { type HelpersMode, type RuntimeHelperName } from '#helpers/collect-helpers';
|
|
2
3
|
/**
|
|
3
4
|
* Options for controlling what gets generated in a file.
|
|
4
5
|
*/
|
|
@@ -29,6 +30,18 @@ type GenerateFileOptions = {
|
|
|
29
30
|
* of coercing invalid input to default values.
|
|
30
31
|
*/
|
|
31
32
|
readonly strict?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Controls how runtime helpers are referenced.
|
|
35
|
+
* - `'package'` (default): emit imports from `@amritk/helpers/...`.
|
|
36
|
+
* - `'embedded'`: emit imports from `./_helpers/...` so the helper sources can
|
|
37
|
+
* be shipped alongside the generated files.
|
|
38
|
+
*/
|
|
39
|
+
readonly helpersMode?: HelpersMode;
|
|
40
|
+
};
|
|
41
|
+
/** Result of generating a single parser file. */
|
|
42
|
+
export type GeneratedFileContent = {
|
|
43
|
+
readonly content: string;
|
|
44
|
+
readonly usedHelpers: ReadonlySet<RuntimeHelperName>;
|
|
32
45
|
};
|
|
33
46
|
/**
|
|
34
47
|
* Generates a complete TypeScript file from a JSON Schema.
|
|
@@ -39,7 +52,7 @@ type GenerateFileOptions = {
|
|
|
39
52
|
* @param schema - The JSON Schema to generate code from
|
|
40
53
|
* @param typeName - The name to use for the generated TypeScript type
|
|
41
54
|
* @param options - Optional settings to control what gets generated
|
|
42
|
-
* @returns The
|
|
55
|
+
* @returns The generated file content and the set of runtime helpers it references
|
|
43
56
|
*
|
|
44
57
|
* @example
|
|
45
58
|
* ```typescript
|
|
@@ -59,5 +72,5 @@ type GenerateFileOptions = {
|
|
|
59
72
|
* // typesOnly contains only type-only imports and type User (no parser)
|
|
60
73
|
* ```
|
|
61
74
|
*/
|
|
62
|
-
export declare const generateFile: (schema: JSONSchema, typeName: string, options?: GenerateFileOptions) =>
|
|
75
|
+
export declare const generateFile: (schema: JSONSchema, typeName: string, options?: GenerateFileOptions) => GeneratedFileContent;
|
|
63
76
|
export {};
|
|
@@ -1,2 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
1
|
+
/** Names (without `.ts`) of runtime helpers that can be embedded into generated output. */
|
|
2
|
+
export type RuntimeHelperName = 'is-object' | 'validate-array' | 'validate-record' | 'has-ref';
|
|
3
|
+
/** Controls how generated parsers reference their runtime helpers. */
|
|
4
|
+
export type HelpersMode = 'package' | 'embedded';
|
|
5
|
+
export type CollectedHelpers = {
|
|
6
|
+
/** Import lines to splice into the generated file. */
|
|
7
|
+
readonly imports: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Helper names that need to ship alongside the generated file in embedded mode.
|
|
10
|
+
* Includes transitive deps (e.g. `validate-record` pulls in `is-object`) so the
|
|
11
|
+
* orchestrator can emit a complete `_helpers/` directory.
|
|
12
|
+
*/
|
|
13
|
+
readonly used: Set<RuntimeHelperName>;
|
|
14
|
+
};
|
|
15
|
+
/** Detects which runtime helpers a generated parser body references. */
|
|
16
|
+
export declare const collectHelpers: (parserFunction: string, mode: HelpersMode) => CollectedHelpers;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/generators/build-schema.ts
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, resolve as resolvePath } from "node:path";
|
|
5
|
+
|
|
1
6
|
// ../helpers/dist/build-dynamic-ref-map.js
|
|
2
7
|
var isSchemaObject = (schema) => {
|
|
3
8
|
return typeof schema === "object" && schema !== null && typeof schema !== "boolean";
|
|
@@ -792,24 +797,46 @@ var generateTypeDefinition = (schema, typeName) => {
|
|
|
792
797
|
};
|
|
793
798
|
|
|
794
799
|
// src/helpers/collect-helpers.ts
|
|
795
|
-
var
|
|
800
|
+
var PACKAGE_IMPORTS = {
|
|
801
|
+
"is-object": "import { isObject } from '@amritk/helpers/is-object';",
|
|
802
|
+
"validate-array": "import { validateArray } from '@amritk/helpers/validate-array';",
|
|
803
|
+
"validate-record": "import { validateRecord } from '@amritk/helpers/validate-record';",
|
|
804
|
+
"has-ref": "import { hasRef } from '@amritk/helpers/schema-guards';"
|
|
805
|
+
};
|
|
806
|
+
var EMBEDDED_IMPORTS = {
|
|
807
|
+
"is-object": "import { isObject } from './_helpers/is-object';",
|
|
808
|
+
"validate-array": "import { validateArray } from './_helpers/validate-array';",
|
|
809
|
+
"validate-record": "import { validateRecord } from './_helpers/validate-record';",
|
|
810
|
+
"has-ref": "import { hasRef } from './_helpers/has-ref';"
|
|
811
|
+
};
|
|
812
|
+
var collectHelpers = (parserFunction, mode) => {
|
|
796
813
|
const imports = [];
|
|
814
|
+
const used = new Set;
|
|
815
|
+
const table = mode === "embedded" ? EMBEDDED_IMPORTS : PACKAGE_IMPORTS;
|
|
797
816
|
if (parserFunction.includes("validateArray")) {
|
|
798
|
-
imports.push("
|
|
817
|
+
imports.push(table["validate-array"]);
|
|
818
|
+
used.add("validate-array");
|
|
799
819
|
}
|
|
800
820
|
if (parserFunction.includes("validateRecord")) {
|
|
801
|
-
imports.push("
|
|
821
|
+
imports.push(table["validate-record"]);
|
|
822
|
+
used.add("validate-record");
|
|
823
|
+
used.add("is-object");
|
|
802
824
|
}
|
|
803
825
|
if (parserFunction.includes("isObject")) {
|
|
804
|
-
imports.push("
|
|
826
|
+
imports.push(table["is-object"]);
|
|
827
|
+
used.add("is-object");
|
|
805
828
|
}
|
|
806
829
|
if (parserFunction.includes("hasRef(")) {
|
|
807
|
-
imports.push("
|
|
830
|
+
imports.push(table["has-ref"]);
|
|
831
|
+
used.add("has-ref");
|
|
808
832
|
}
|
|
809
|
-
return imports;
|
|
833
|
+
return { imports, used };
|
|
810
834
|
};
|
|
811
835
|
|
|
812
836
|
// ../helpers/dist/schema-guards.js
|
|
837
|
+
var hasRef = (value) => {
|
|
838
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string";
|
|
839
|
+
};
|
|
813
840
|
var isSchemaObject3 = (schema) => {
|
|
814
841
|
return typeof schema === "object" && schema !== null && typeof schema !== "boolean";
|
|
815
842
|
};
|
|
@@ -891,9 +918,6 @@ var hasMinProperties = (schema) => {
|
|
|
891
918
|
var hasMaxProperties = (schema) => {
|
|
892
919
|
return isSchemaObject3(schema) && "maxProperties" in schema && typeof schema.maxProperties === "number";
|
|
893
920
|
};
|
|
894
|
-
var hasRef = (schema) => {
|
|
895
|
-
return isSchemaObject3(schema) && "$ref" in schema && typeof schema.$ref === "string";
|
|
896
|
-
};
|
|
897
921
|
|
|
898
922
|
// src/helpers/collect-imports.ts
|
|
899
923
|
var getImportPathForFilename = (filename) => `./${filename}`;
|
|
@@ -2418,6 +2442,7 @@ var generateFile = (schema, typeName, options) => {
|
|
|
2418
2442
|
const typesOnly = options?.typesOnly === true;
|
|
2419
2443
|
const selfRef = options?.selfRef;
|
|
2420
2444
|
const rootSchema = options?.rootSchema;
|
|
2445
|
+
const helpersMode = options?.helpersMode ?? "package";
|
|
2421
2446
|
const typeDefinition = generateTypeDefinition(schema, typeName);
|
|
2422
2447
|
if (typesOnly) {
|
|
2423
2448
|
const imports2 = collectImports(schema, { typesOnly: true, selfRef, rootSchema });
|
|
@@ -2432,7 +2457,7 @@ var generateFile = (schema, typeName, options) => {
|
|
|
2432
2457
|
|
|
2433
2458
|
`;
|
|
2434
2459
|
}
|
|
2435
|
-
return result2 + typeDefinition;
|
|
2460
|
+
return { content: result2 + typeDefinition, usedHelpers: new Set };
|
|
2436
2461
|
}
|
|
2437
2462
|
const parserFunction = generateParserFunction(schema, typeName, {
|
|
2438
2463
|
useRefImports: true,
|
|
@@ -2443,7 +2468,8 @@ var generateFile = (schema, typeName, options) => {
|
|
|
2443
2468
|
const combinedFunctions = `${shapeValidator}
|
|
2444
2469
|
|
|
2445
2470
|
${parserFunction}`;
|
|
2446
|
-
const
|
|
2471
|
+
const helpers = collectHelpers(combinedFunctions, helpersMode);
|
|
2472
|
+
const imports = [...collectImports(schema, { selfRef, rootSchema }), ...helpers.imports];
|
|
2447
2473
|
let result = "";
|
|
2448
2474
|
if (imports.length > 0) {
|
|
2449
2475
|
result += imports[0];
|
|
@@ -2455,12 +2481,18 @@ ${parserFunction}`;
|
|
|
2455
2481
|
|
|
2456
2482
|
`;
|
|
2457
2483
|
}
|
|
2458
|
-
return result + typeDefinition + `
|
|
2484
|
+
return { content: result + typeDefinition + `
|
|
2459
2485
|
|
|
2460
|
-
` + combinedFunctions;
|
|
2486
|
+
` + combinedFunctions, usedHelpers: helpers.used };
|
|
2461
2487
|
};
|
|
2462
2488
|
|
|
2463
2489
|
// src/generators/build-schema.ts
|
|
2490
|
+
var readHelperSource = async (helper) => {
|
|
2491
|
+
const require2 = createRequire(import.meta.url);
|
|
2492
|
+
const helpersPkgPath = require2.resolve("@amritk/helpers/package.json");
|
|
2493
|
+
const helpersRoot = dirname(helpersPkgPath);
|
|
2494
|
+
return readFile(resolvePath(helpersRoot, "src", `${helper}.ts`), "utf-8");
|
|
2495
|
+
};
|
|
2464
2496
|
var extractDynamicAnchorDefs = (schema) => {
|
|
2465
2497
|
const refs = [];
|
|
2466
2498
|
if (typeof schema !== "object" || schema === null) {
|
|
@@ -2480,18 +2512,20 @@ var extractDynamicAnchorDefs = (schema) => {
|
|
|
2480
2512
|
}
|
|
2481
2513
|
return refs;
|
|
2482
2514
|
};
|
|
2483
|
-
var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict) => {
|
|
2515
|
+
var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package") => {
|
|
2484
2516
|
rootSchema = upgradeDraft07Schema(rootSchema);
|
|
2485
2517
|
const files = [];
|
|
2486
2518
|
const processedRefs = new Set;
|
|
2487
2519
|
const processedFilenames = new Set;
|
|
2488
2520
|
const refsToProcess = [];
|
|
2521
|
+
const usedHelpers = new Set;
|
|
2489
2522
|
const dynamicRefMap = buildDynamicRefMap(rootSchema);
|
|
2490
2523
|
const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap);
|
|
2491
2524
|
const extendedRootSchema = extensions ? applySchemaExtensions(processedRootSchema, rootTypeName.toLowerCase(), extensions) : processedRootSchema;
|
|
2492
|
-
const
|
|
2525
|
+
const rootResult = generateFile(extendedRootSchema, rootTypeName, {
|
|
2493
2526
|
typesOnly: typesOnly ?? false,
|
|
2494
2527
|
rootSchema,
|
|
2528
|
+
helpersMode,
|
|
2495
2529
|
...logWarnings !== undefined ? { logWarnings } : {},
|
|
2496
2530
|
...strict !== undefined ? { strict } : {}
|
|
2497
2531
|
});
|
|
@@ -2499,8 +2533,10 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2499
2533
|
processedFilenames.add(rootFilename);
|
|
2500
2534
|
files.push({
|
|
2501
2535
|
filename: `${rootFilename}.ts`,
|
|
2502
|
-
content:
|
|
2536
|
+
content: rootResult.content
|
|
2503
2537
|
});
|
|
2538
|
+
for (const helper of rootResult.usedHelpers)
|
|
2539
|
+
usedHelpers.add(helper);
|
|
2504
2540
|
const rootRefs = extractRefs(rootSchema);
|
|
2505
2541
|
refsToProcess.push(...rootRefs);
|
|
2506
2542
|
const dynamicAnchorRefs = extractDynamicAnchorDefs(rootSchema);
|
|
@@ -2520,10 +2556,11 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2520
2556
|
const filename = refToFilename(ref);
|
|
2521
2557
|
const processedSchema = resolveDynamicRefs(resolvedSchema, dynamicRefMap);
|
|
2522
2558
|
const extendedSchema = extensions ? applySchemaExtensions(processedSchema, filename, extensions) : processedSchema;
|
|
2523
|
-
const
|
|
2559
|
+
const fileResult = generateFile(extendedSchema, typeName, {
|
|
2524
2560
|
typesOnly: typesOnly ?? false,
|
|
2525
2561
|
selfRef: ref,
|
|
2526
2562
|
rootSchema,
|
|
2563
|
+
helpersMode,
|
|
2527
2564
|
...logWarnings !== undefined ? { logWarnings } : {},
|
|
2528
2565
|
...strict !== undefined ? { strict } : {}
|
|
2529
2566
|
});
|
|
@@ -2531,9 +2568,11 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2531
2568
|
processedFilenames.add(filename);
|
|
2532
2569
|
files.push({
|
|
2533
2570
|
filename: `${filename}.ts`,
|
|
2534
|
-
content
|
|
2571
|
+
content: fileResult.content
|
|
2535
2572
|
});
|
|
2536
2573
|
}
|
|
2574
|
+
for (const helper of fileResult.usedHelpers)
|
|
2575
|
+
usedHelpers.add(helper);
|
|
2537
2576
|
const nestedRefs = extractRefs(resolvedSchema);
|
|
2538
2577
|
for (const nestedRef of nestedRefs) {
|
|
2539
2578
|
if (!processedRefs.has(nestedRef)) {
|
|
@@ -2541,9 +2580,15 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2541
2580
|
}
|
|
2542
2581
|
}
|
|
2543
2582
|
}
|
|
2583
|
+
if (helpersMode === "embedded" && !typesOnly) {
|
|
2584
|
+
for (const helper of usedHelpers) {
|
|
2585
|
+
const source = await readHelperSource(helper);
|
|
2586
|
+
files.push({ filename: `_helpers/${helper}.ts`, content: source });
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2544
2589
|
const TYPE_EXPORT_RE = /^export type (\w+)/gm;
|
|
2545
2590
|
const CONST_EXPORT_RE = /^export const (\w+)/gm;
|
|
2546
|
-
const sortedFiles = [...files].sort((a, b) => a.filename.localeCompare(b.filename));
|
|
2591
|
+
const sortedFiles = [...files].filter((f) => !f.filename.startsWith("_helpers/")).sort((a, b) => a.filename.localeCompare(b.filename));
|
|
2547
2592
|
let indexContent = "";
|
|
2548
2593
|
for (const file of sortedFiles) {
|
|
2549
2594
|
const moduleName = file.filename.replace(/\.ts$/, "");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-parsers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"url": "https://github.com/amritk/mjst/issues"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"dist"
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
27
28
|
],
|
|
28
29
|
"publishConfig": {
|
|
29
30
|
"access": "public"
|