@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.
Files changed (38) hide show
  1. package/dist/generators/build-schema.d.ts +5 -1
  2. package/dist/generators/generate-files.d.ts +15 -2
  3. package/dist/helpers/collect-helpers.d.ts +16 -2
  4. package/dist/index.js +64 -19
  5. package/package.json +3 -2
  6. package/src/generators/build-schema.test.ts +830 -0
  7. package/src/generators/build-schema.ts +280 -0
  8. package/src/generators/generate-default-from-pattern.test.ts +81 -0
  9. package/src/generators/generate-default-from-pattern.ts +39 -0
  10. package/src/generators/generate-discriminated-union-validation.test.ts +84 -0
  11. package/src/generators/generate-discriminated-union-validation.ts +54 -0
  12. package/src/generators/generate-enum-check.test.ts +45 -0
  13. package/src/generators/generate-enum-check.ts +8 -0
  14. package/src/generators/generate-files.test.ts +435 -0
  15. package/src/generators/generate-files.ts +136 -0
  16. package/src/generators/generate-non-discriminated-union-validation.test.ts +74 -0
  17. package/src/generators/generate-non-discriminated-union-validation.ts +40 -0
  18. package/src/generators/generate-parser-function.test.ts +1798 -0
  19. package/src/generators/generate-parser-function.ts +1319 -0
  20. package/src/generators/generate-schema-checks.test.ts +322 -0
  21. package/src/generators/generate-schema-checks.ts +114 -0
  22. package/src/generators/generate-strict-assertion.ts +210 -0
  23. package/src/generators/generate-validation-expression.test.ts +794 -0
  24. package/src/generators/generate-validation-expression.ts +286 -0
  25. package/src/helpers/apply-schema-extensions.test.ts +194 -0
  26. package/src/helpers/apply-schema-extensions.ts +63 -0
  27. package/src/helpers/collect-helpers.test.ts +103 -0
  28. package/src/helpers/collect-helpers.ts +64 -0
  29. package/src/helpers/collect-imports.test.ts +587 -0
  30. package/src/helpers/collect-imports.ts +268 -0
  31. package/src/helpers/find-discriminator.test.ts +88 -0
  32. package/src/helpers/find-discriminator.ts +39 -0
  33. package/src/helpers/get-default-value.test.ts +135 -0
  34. package/src/helpers/get-default-value.ts +89 -0
  35. package/src/helpers/get-discriminator-value.test.ts +83 -0
  36. package/src/helpers/get-discriminator-value.ts +21 -0
  37. package/src/index.ts +1 -0
  38. 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 complete TypeScript code
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) => string;
75
+ export declare const generateFile: (schema: JSONSchema, typeName: string, options?: GenerateFileOptions) => GeneratedFileContent;
63
76
  export {};
@@ -1,2 +1,16 @@
1
- /** A more foolproof way to generate imports from the parserFunction */
2
- export declare const collectHelpers: (parserFunction: string) => string[];
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 collectHelpers = (parserFunction) => {
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("import { validateArray } from '@amritk/helpers/validate-array';");
817
+ imports.push(table["validate-array"]);
818
+ used.add("validate-array");
799
819
  }
800
820
  if (parserFunction.includes("validateRecord")) {
801
- imports.push("import { validateRecord } from '@amritk/helpers/validate-record';");
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("import { isObject } from '@amritk/helpers/is-object';");
826
+ imports.push(table["is-object"]);
827
+ used.add("is-object");
805
828
  }
806
829
  if (parserFunction.includes("hasRef(")) {
807
- imports.push("import { hasRef } from '@amritk/helpers/schema-guards';");
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 imports = [...collectImports(schema, { selfRef, rootSchema }), ...collectHelpers(combinedFunctions)];
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 rootContent = generateFile(extendedRootSchema, rootTypeName, {
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: rootContent
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 content = generateFile(extendedSchema, typeName, {
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.2.1",
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"