@amritk/generate-parsers 0.24.0 → 0.25.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/README.md +37 -463
- package/dist/index.d.ts +22 -2
- package/dist/index.js +18 -1
- package/package.json +15 -40
- package/AI.md +0 -52
- package/dist/generators/assert-supported-keywords.d.ts +0 -30
- package/dist/generators/assert-supported-keywords.js +0 -87
- package/dist/generators/build-schema.d.ts +0 -111
- package/dist/generators/build-schema.js +0 -74
- package/dist/generators/generate-deep-equal-check.d.ts +0 -24
- package/dist/generators/generate-deep-equal-check.js +0 -35
- package/dist/generators/generate-default-from-pattern.d.ts +0 -26
- package/dist/generators/generate-default-from-pattern.js +0 -223
- package/dist/generators/generate-discriminated-union-validation.d.ts +0 -15
- package/dist/generators/generate-discriminated-union-validation.js +0 -28
- package/dist/generators/generate-enum-check.d.ts +0 -35
- package/dist/generators/generate-enum-check.js +0 -31
- package/dist/generators/generate-files.d.ts +0 -138
- package/dist/generators/generate-files.js +0 -89
- package/dist/generators/generate-non-discriminated-union-validation.d.ts +0 -6
- package/dist/generators/generate-non-discriminated-union-validation.js +0 -26
- package/dist/generators/generate-parser-function.d.ts +0 -120
- package/dist/generators/generate-parser-function.js +0 -1973
- package/dist/generators/generate-schema-checks.d.ts +0 -5
- package/dist/generators/generate-schema-checks.js +0 -260
- package/dist/generators/generate-strict-assertion.d.ts +0 -155
- package/dist/generators/generate-strict-assertion.js +0 -759
- package/dist/generators/generate-type-checks.d.ts +0 -148
- package/dist/generators/generate-type-checks.js +0 -454
- package/dist/generators/generate-unique-items-check.d.ts +0 -16
- package/dist/generators/generate-unique-items-check.js +0 -32
- package/dist/generators/generate-validation-expression.d.ts +0 -79
- package/dist/generators/generate-validation-expression.js +0 -416
- package/dist/generators/subschema-match.d.ts +0 -48
- package/dist/generators/subschema-match.js +0 -485
- package/dist/generators/unevaluated-match.d.ts +0 -19
- package/dist/generators/unevaluated-match.js +0 -206
- package/dist/helpers/apply-schema-extensions.d.ts +0 -35
- package/dist/helpers/apply-schema-extensions.js +0 -20
- package/dist/helpers/collect-helpers.d.ts +0 -15
- package/dist/helpers/collect-helpers.js +0 -64
- package/dist/helpers/collect-imports.d.ts +0 -104
- package/dist/helpers/collect-imports.js +0 -227
- package/dist/helpers/find-discriminator.d.ts +0 -6
- package/dist/helpers/find-discriminator.js +0 -27
- package/dist/helpers/get-default-value.d.ts +0 -7
- package/dist/helpers/get-default-value.js +0 -155
- package/dist/helpers/get-discriminator-value.d.ts +0 -6
- package/dist/helpers/get-discriminator-value.js +0 -18
- package/dist/types/schema-extensions.d.ts +0 -34
- package/dist/types/schema-extensions.js +0 -0
package/AI.md
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# @amritk/generate-parsers — notes for AI coding agents
|
|
2
|
-
|
|
3
|
-
Programmatic API: turn a JSON Schema (Draft 2020-12) into TypeScript type
|
|
4
|
-
definitions plus optional runtime parser functions. Full reference is
|
|
5
|
-
[README.md](./README.md). (Most users want the [`mjst` CLI](../cli) instead.)
|
|
6
|
-
|
|
7
|
-
> Pre-alpha: APIs and generated output change pre-1.0.
|
|
8
|
-
|
|
9
|
-
## Minimal example
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { buildSchema } from '@amritk/generate-parsers'
|
|
13
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
14
|
-
|
|
15
|
-
const schema: JSONSchema = { type: 'object', properties: { title: { type: 'string' } } }
|
|
16
|
-
|
|
17
|
-
const files = await buildSchema(schema, 'Document')
|
|
18
|
-
// files → [{ filename: 'document.ts', content: '…' }, { filename: 'index.ts', content: '…' }, …]
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Gotchas — where agents fail
|
|
22
|
-
|
|
23
|
-
1. **`buildSchema` is `async` and takes POSITIONAL args, no options object.** The
|
|
24
|
-
full signature is
|
|
25
|
-
`buildSchema(rootSchema, rootTypeName, extensions?, typesOnly?, logWarnings?, strict?, helpersMode?, helpersImportPrefix?, readonly?, stripUnknown?, typeSuffix?, importExt?, caseInsensitive?, schemas?, unknownKeys?)`.
|
|
26
|
-
To set a later flag you must pass every intervening positional. (The README's
|
|
27
|
-
short form omits the trailing seven.) `unknownKeys` (`'count-keys'` by default,
|
|
28
|
-
`'count-enumerable'` for Node-only output) picks how a closed object's fast
|
|
29
|
-
path counts keys; nothing in the generated code detects its runtime.
|
|
30
|
-
2. **It returns files in memory — it does NOT write to disk.** You write the
|
|
31
|
-
`GeneratedFile[]` (`{ filename, content }`) yourself. `rootTypeName` becomes
|
|
32
|
-
that filename (lowercased) as well as the `export type` name, so it must be a
|
|
33
|
-
TypeScript identifier — `buildSchema` throws otherwise rather than hand you a
|
|
34
|
-
path that escapes your output directory. Derive it with `@amritk/helpers`'
|
|
35
|
-
`deriveRootTypeName`, which always produces one.
|
|
36
|
-
3. **Output is more than one file per `$def`:** always an `index.ts` barrel, and
|
|
37
|
-
(unless `typesOnly`) runtime helper files.
|
|
38
|
-
4. **Default parsers COERCE invalid input to defaults** rather than throwing;
|
|
39
|
-
pass `strict: true` (the 6th positional) to make them throw.
|
|
40
|
-
5. **Strict mode enforces the whole Draft 2020-12 assertion vocabulary** —
|
|
41
|
-
including `unevaluatedProperties` / `unevaluatedItems`, `contains` bounds,
|
|
42
|
-
`propertyNames`, `dependent*`, `patternProperties`, tuple `prefixItems`, and a
|
|
43
|
-
`$ref`'s 2020-12 siblings — and is held against Ajv by differential fuzz. It
|
|
44
|
-
deliberately differs on three points (`format` is an annotation, `multipleOf`
|
|
45
|
-
uses a scaled tolerance, a type-less `properties` schema still requires an
|
|
46
|
-
object); see the README.
|
|
47
|
-
6. **Strict generation FAILS LOUDLY on anything it cannot enforce** (e.g. a
|
|
48
|
-
recursive `$ref` it would have to inline when ref imports are off). Catch the
|
|
49
|
-
error and fall back to a coercing parser if that is not what you want.
|
|
50
|
-
|
|
51
|
-
Only the `.` entry (`buildSchema`, `GeneratedFile`, `ImportExtension`).
|
|
52
|
-
Install: `bun add @amritk/generate-parsers`.
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
-
import { type StrictAssertionContext } from './generate-strict-assertion.js';
|
|
3
|
-
/**
|
|
4
|
-
* Throws when a schema (anywhere in its subtree) uses a keyword the *strict*
|
|
5
|
-
* parser generator cannot enforce but which narrows the set of valid documents.
|
|
6
|
-
* Mirrors generate-validators' `assertNoUnsupportedKeywords`: failing loudly at
|
|
7
|
-
* generation time is strictly better than silently emitting a parser that
|
|
8
|
-
* accepts input the schema forbids — a strict parser promises to "throw on
|
|
9
|
-
* violations".
|
|
10
|
-
*
|
|
11
|
-
* Two families are rejected:
|
|
12
|
-
*
|
|
13
|
-
* - `contains`, `not`, `propertyNames`, or `dependentSchemas` whose subschema
|
|
14
|
-
* uses a form {@link canMatchSubschema} cannot prove inline (a cyclic `$ref`,
|
|
15
|
-
* an unknown keyword). The parser enforces these keywords only for subschemas
|
|
16
|
-
* it can match exactly; anything else would be silently ignored.
|
|
17
|
-
*
|
|
18
|
-
* - A node whose enforcement rests on the whole-schema backstop (see
|
|
19
|
-
* {@link backstopReason} — `unevaluatedProperties` / `unevaluatedItems`, a
|
|
20
|
-
* `$ref` nothing delegates, a type-less constraint) when the matcher cannot
|
|
21
|
-
* build that backstop.
|
|
22
|
-
*
|
|
23
|
-
* Everything the matcher *can* prove is enforced rather than refused, which is
|
|
24
|
-
* why `unevaluated*` and `$ref`-carrying subschemas no longer fail outright.
|
|
25
|
-
*
|
|
26
|
-
* This runs for strict generation only — coercing parsers are documented to be
|
|
27
|
-
* permissive (they repair rather than reject), so ignoring these keywords there
|
|
28
|
-
* is the contract, not a lie.
|
|
29
|
-
*/
|
|
30
|
-
export declare const assertNoUnsupportedKeywords: (schema: JSONSchema, typeName: string, context?: StrictAssertionContext) => void;
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
2
|
-
import { backstopReason, backstopSchema } from "./generate-strict-assertion.js";
|
|
3
|
-
import { canMatchSubschema } from "./subschema-match.js";
|
|
4
|
-
const SUBSCHEMA_KEYS = [
|
|
5
|
-
"items",
|
|
6
|
-
"prefixItems",
|
|
7
|
-
"additionalItems",
|
|
8
|
-
"contains",
|
|
9
|
-
"additionalProperties",
|
|
10
|
-
"propertyNames",
|
|
11
|
-
"unevaluatedProperties",
|
|
12
|
-
"unevaluatedItems",
|
|
13
|
-
"not",
|
|
14
|
-
"if",
|
|
15
|
-
"then",
|
|
16
|
-
"else",
|
|
17
|
-
"allOf",
|
|
18
|
-
"anyOf",
|
|
19
|
-
"oneOf",
|
|
20
|
-
"contentSchema"
|
|
21
|
-
];
|
|
22
|
-
const SUBSCHEMA_MAP_KEYS = [
|
|
23
|
-
"properties",
|
|
24
|
-
"patternProperties",
|
|
25
|
-
"dependentSchemas",
|
|
26
|
-
"$defs",
|
|
27
|
-
"definitions"
|
|
28
|
-
];
|
|
29
|
-
const assertNoUnsupportedKeywords = (schema, typeName, context = {}) => {
|
|
30
|
-
const rootSchema = context.rootSchema ?? (isSchemaObject(schema) ? schema : void 0);
|
|
31
|
-
const provable = (sub) => canMatchSubschema(sub, rootSchema);
|
|
32
|
-
const fail = (keyword, detail) => {
|
|
33
|
-
throw new Error(`[${typeName}] unsupported keyword "${keyword}": the strict parser generator ${detail}, and would silently accept documents the schema rejects. Generate a coercing (non-strict) parser, validate this schema with @amritk/generate-validators, or remove the keyword.`);
|
|
34
|
-
};
|
|
35
|
-
const visit = (node) => {
|
|
36
|
-
if (typeof node !== "object" || node === null || Array.isArray(node))
|
|
37
|
-
return;
|
|
38
|
-
const record = node;
|
|
39
|
-
if ("contains" in record && !provable(record["contains"])) {
|
|
40
|
-
fail("contains", "cannot prove its subschema inline");
|
|
41
|
-
}
|
|
42
|
-
if ("not" in record && !provable(record["not"])) {
|
|
43
|
-
fail("not", "cannot prove its subschema inline");
|
|
44
|
-
}
|
|
45
|
-
if ("propertyNames" in record && isSchemaObject(record["propertyNames"])) {
|
|
46
|
-
if (!provable(record["propertyNames"])) {
|
|
47
|
-
fail("propertyNames", "cannot prove its subschema inline");
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const dependentSchemas = record["dependentSchemas"];
|
|
51
|
-
if (typeof dependentSchemas === "object" && dependentSchemas !== null && !Array.isArray(dependentSchemas)) {
|
|
52
|
-
for (const sub of Object.values(dependentSchemas)) {
|
|
53
|
-
if (typeof sub === "boolean")
|
|
54
|
-
continue;
|
|
55
|
-
if (!provable(sub)) {
|
|
56
|
-
fail("dependentSchemas", "cannot prove one of its subschemas inline");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const reason = backstopReason(node, context);
|
|
61
|
-
if (reason !== null && !provable(backstopSchema(node, reason))) {
|
|
62
|
-
fail(reason, "cannot prove the schema it appears in inline");
|
|
63
|
-
}
|
|
64
|
-
for (const keyword of SUBSCHEMA_KEYS) {
|
|
65
|
-
const value = record[keyword];
|
|
66
|
-
if (value === void 0)
|
|
67
|
-
continue;
|
|
68
|
-
if (Array.isArray(value)) {
|
|
69
|
-
for (const item of value)
|
|
70
|
-
visit(item);
|
|
71
|
-
} else {
|
|
72
|
-
visit(value);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
for (const keyword of SUBSCHEMA_MAP_KEYS) {
|
|
76
|
-
const value = record[keyword];
|
|
77
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
78
|
-
continue;
|
|
79
|
-
for (const sub of Object.values(value))
|
|
80
|
-
visit(sub);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
visit(schema);
|
|
84
|
-
};
|
|
85
|
-
export {
|
|
86
|
-
assertNoUnsupportedKeywords
|
|
87
|
-
};
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { type UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
|
|
2
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
3
|
-
import type { HelpersMode } from '../helpers/collect-helpers.js';
|
|
4
|
-
import type { ImportExtension } from '../helpers/collect-imports.js';
|
|
5
|
-
import type { SchemaExtensions } from '../types/schema-extensions.js';
|
|
6
|
-
/**
|
|
7
|
-
* Represents a generated file with its filename and content.
|
|
8
|
-
*/
|
|
9
|
-
export type GeneratedFile = {
|
|
10
|
-
filename: string;
|
|
11
|
-
content: string;
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* Builds all TypeScript files from a JSON Schema by traversing its entire
|
|
15
|
-
* `$ref` / `$dynamicRef` graph (via the shared `@amritk/helpers/walk-ref-graph`
|
|
16
|
-
* walker, which also seeds `$dynamicAnchor` definitions and rewrites
|
|
17
|
-
* `$dynamicRef` to `$ref`).
|
|
18
|
-
*
|
|
19
|
-
* For the root schema and each reachable definition this function generates a
|
|
20
|
-
* TypeScript file, then emits an `index.ts` barrel re-exporting them all.
|
|
21
|
-
*
|
|
22
|
-
* @param rootSchema - The root JSON Schema to build from
|
|
23
|
-
* @param rootTypeName - The name for the root type (e.g., "Document"). Must be a TypeScript
|
|
24
|
-
* identifier: it is emitted verbatim as `export type <name>` and, lowercased, becomes the
|
|
25
|
-
* output filename, so a name derived from an untrusted document is refused rather than
|
|
26
|
-
* written outside the output directory.
|
|
27
|
-
* @param extensions - Optional map of custom extension properties to add to specific definitions.
|
|
28
|
-
* Keys are definition names (matching $defs keys), values are records of extension property
|
|
29
|
-
* names to their JSON Schema definitions. Extensions are merged as optional properties before
|
|
30
|
-
* type and parser generation.
|
|
31
|
-
* @param typesOnly - When true, only generate TypeScript type definitions without parser functions.
|
|
32
|
-
* @param logWarnings - When true, the generated parsers emit a console.warn for every input key
|
|
33
|
-
* that is not declared in the schema's properties.
|
|
34
|
-
* @param strict - When true, the generated parsers throw on type/shape mismatches
|
|
35
|
-
* (wrong type, missing required property, enum/pattern/min/max violations) instead
|
|
36
|
-
* of coercing invalid input to default values.
|
|
37
|
-
* @param helpersMode - `'package'` (default) emits `import ... from '@amritk/helpers/...'`.
|
|
38
|
-
* `'embedded'` emits `import ... from './_helpers/...'` and appends the helper sources
|
|
39
|
-
* as additional `GeneratedFile` entries so the output directory is self-contained.
|
|
40
|
-
* @param helpersImportPrefix - Relative path prefix to the `_helpers/` directory in
|
|
41
|
-
* embedded mode. Defaults to `'./'`. The recursive multi-schema build passes `'../'`,
|
|
42
|
-
* `'../../'`, etc. so nested parsers can import from a single shared `_helpers/`
|
|
43
|
-
* directory while the helper sources are emitted once at the output root.
|
|
44
|
-
* @param readonly - When true, every property, array, and record in the generated type
|
|
45
|
-
* definitions is emitted as `readonly`.
|
|
46
|
-
* @param stripUnknown - When true, the generated parsers build their result from declared
|
|
47
|
-
* properties only, silently dropping undeclared input keys at every nesting level (zod's
|
|
48
|
-
* `.strip()`) without treating extras as a validation error. Composes with `strict` and
|
|
49
|
-
* yields to `additionalProperties: false` (which rejects rather than strips in strict mode).
|
|
50
|
-
* @param typeSuffix - Suffix appended to every type/parser name derived from a `$ref`
|
|
51
|
-
* (e.g. `'Object'` → `ContactObject`). Defaults to `''` (no suffix). The root type name
|
|
52
|
-
* is used verbatim and is not affected by this suffix.
|
|
53
|
-
* @param importExt - Extension used on every relative import specifier in the generated
|
|
54
|
-
* output (cross-file `$ref` imports, the index barrel, and embedded-helper imports).
|
|
55
|
-
* `'js'` (this function's default) is the standard TS NodeNext form; `'ts'` emits the
|
|
56
|
-
* literal on-disk paths so the generated sources run directly under Node's type
|
|
57
|
-
* stripping. Note the `mjst` CLI defaults this to `'ts'` (falling back to `'js'` only
|
|
58
|
-
* under `--build`, where tsc must compile the sources) so generated output runs under
|
|
59
|
-
* Node without a build step; direct callers of `buildSchema` opt in explicitly.
|
|
60
|
-
* @param caseInsensitive - When true, the generated coercing parsers normalize a mis-cased
|
|
61
|
-
* string to the exact casing of a declared `enum`/`const` member it matches
|
|
62
|
-
* case-insensitively (e.g. `hElLo` → `hello`) instead of coercing to the default. Coerce
|
|
63
|
-
* mode only — strict parsers still reject a casing mismatch. The normalization lives on
|
|
64
|
-
* the coercion failure branch, so correctly-cased input keeps the exact-match fast path
|
|
65
|
-
* and the hot path is unaffected.
|
|
66
|
-
* @param schemas - Other schema documents you have **already loaded**, keyed by the absolute
|
|
67
|
-
* URI a `$ref` names them by. Supplying them makes those URIs resolvable, so the schema can
|
|
68
|
-
* reference a document that is not itself — each one becomes a resource of the generated
|
|
69
|
-
* document, with its own `$id`, anchors and nested resources all resolvable, and a `$ref`
|
|
70
|
-
* from one registered document into another resolving too. Nothing is fetched here: loading
|
|
71
|
-
* is yours to do (or `@amritk/resolve-refs`'), and a `$ref` to a URI nobody registered still
|
|
72
|
-
* stops generation. Only the documents actually referenced get files, so registering more
|
|
73
|
-
* than the schema uses costs nothing in the output.
|
|
74
|
-
* @param unknownKeys - How the generated fast paths prove a closed object
|
|
75
|
-
* (`additionalProperties: false`, or a `stripUnknown` build) carries no undeclared key:
|
|
76
|
-
* `'count-keys'` (the default) compares `Object.keys(input).length` behind a plain-prototype
|
|
77
|
-
* guard, `'count-enumerable'` counts with `for…in`. The first is the faster form on
|
|
78
|
-
* JavaScriptCore (Bun), the second on V8 (Node) — see the README for the measurements.
|
|
79
|
-
* @returns An array of generated TypeScript files
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* ```typescript
|
|
83
|
-
* const schema = {
|
|
84
|
-
* type: "object",
|
|
85
|
-
* properties: {
|
|
86
|
-
* info: { $ref: "#/$defs/info" }
|
|
87
|
-
* },
|
|
88
|
-
* $defs: {
|
|
89
|
-
* info: {
|
|
90
|
-
* type: "object",
|
|
91
|
-
* properties: {
|
|
92
|
-
* title: { type: "string" }
|
|
93
|
-
* }
|
|
94
|
-
* }
|
|
95
|
-
* }
|
|
96
|
-
* };
|
|
97
|
-
*
|
|
98
|
-
* const files = buildSchema(schema, "Document");
|
|
99
|
-
*
|
|
100
|
-
* // Types-only mode — no parser functions or runtime helpers included:
|
|
101
|
-
* const typesFiles = buildSchema(schema, "Document", undefined, true);
|
|
102
|
-
*
|
|
103
|
-
* // With extensions:
|
|
104
|
-
* const filesWithExtensions = buildSchema(schema, "Document", {
|
|
105
|
-
* info: {
|
|
106
|
-
* 'x-internal': { type: 'boolean' },
|
|
107
|
-
* },
|
|
108
|
-
* });
|
|
109
|
-
* ```
|
|
110
|
-
*/
|
|
111
|
-
export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean, stripUnknown?: boolean, typeSuffix?: string, importExt?: ImportExtension, caseInsensitive?: boolean, schemas?: Readonly<Record<string, unknown>>, unknownKeys?: UnknownKeysStrategy) => Promise<GeneratedFile[]>;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { dirname, resolve as resolvePath } from "node:path";
|
|
4
|
-
import { foldNullable } from "@amritk/helpers/fold-nullable";
|
|
5
|
-
import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel";
|
|
6
|
-
import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
|
|
7
|
-
import { walkRefGraph } from "@amritk/helpers/walk-ref-graph";
|
|
8
|
-
import { applySchemaExtensions } from "../helpers/apply-schema-extensions.js";
|
|
9
|
-
import { generateFile } from "./generate-files.js";
|
|
10
|
-
const readHelperSource = async (helper, importExt) => {
|
|
11
|
-
const require2 = createRequire(import.meta.url);
|
|
12
|
-
const helpersRoot = dirname(require2.resolve("@amritk/helpers/package.json"));
|
|
13
|
-
try {
|
|
14
|
-
const source = await readFile(resolvePath(helpersRoot, "src", `${helper}.ts`), "utf-8");
|
|
15
|
-
return { ext: "ts", content: source.replace(/\bfrom '(\.\/[^'".]+)'/g, `from '$1.${importExt}'`) };
|
|
16
|
-
} catch {
|
|
17
|
-
const compiled = await readFile(resolvePath(helpersRoot, "dist", `${helper}.js`), "utf-8");
|
|
18
|
-
if (importExt === "ts") {
|
|
19
|
-
return { ext: "ts", content: compiled.replace(/\bfrom '(\.\/[^'"]+)\.js'/g, "from '$1.ts'") };
|
|
20
|
-
}
|
|
21
|
-
return { ext: "js", content: compiled };
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
const ROOT_TYPE_NAME = /^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u;
|
|
25
|
-
const assertRootTypeName = (rootTypeName) => {
|
|
26
|
-
if (ROOT_TYPE_NAME.test(rootTypeName))
|
|
27
|
-
return;
|
|
28
|
-
throw new Error(`Invalid root type name ${JSON.stringify(rootTypeName)}: expected a TypeScript identifier. The name is emitted as \`export type <name>\` and, lowercased, used as the output filename, so anything else either fails to compile or escapes the output directory. Derive it with @amritk/helpers' \`deriveRootTypeName\`, which always produces one.`);
|
|
29
|
-
};
|
|
30
|
-
const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false, stripUnknown = false, typeSuffix = "", importExt = "js", caseInsensitive = false, schemas, unknownKeys = DEFAULT_UNKNOWN_KEYS) => {
|
|
31
|
-
assertRootTypeName(rootTypeName);
|
|
32
|
-
const files = [];
|
|
33
|
-
const usedHelpers = /* @__PURE__ */ new Set();
|
|
34
|
-
const foldedSchema = foldNullable(rootSchema);
|
|
35
|
-
walkRefGraph(foldedSchema, rootTypeName, { typeSuffix, ...schemas !== void 0 ? { schemas } : {} }, (node) => {
|
|
36
|
-
if (node.filename === "index")
|
|
37
|
-
return;
|
|
38
|
-
const extended = extensions ? applySchemaExtensions(node.schema, node.filename, extensions) : node.schema;
|
|
39
|
-
const result = generateFile(extended, node.typeName, {
|
|
40
|
-
selfFilename: node.filename,
|
|
41
|
-
typesOnly: typesOnly ?? false,
|
|
42
|
-
rootSchema: node.rootSchema,
|
|
43
|
-
helpersMode,
|
|
44
|
-
helpersImportPrefix,
|
|
45
|
-
readonly,
|
|
46
|
-
typeSuffix,
|
|
47
|
-
importExt,
|
|
48
|
-
isRoot: node.isRoot,
|
|
49
|
-
...node.ref !== void 0 ? { selfRef: node.ref } : {},
|
|
50
|
-
...logWarnings !== void 0 ? { logWarnings } : {},
|
|
51
|
-
...strict !== void 0 ? { strict } : {},
|
|
52
|
-
...stripUnknown ? { stripUnknown } : {},
|
|
53
|
-
...caseInsensitive ? { caseInsensitive } : {},
|
|
54
|
-
unknownKeys
|
|
55
|
-
});
|
|
56
|
-
files.push({ filename: `${node.filename}.ts`, content: result.content });
|
|
57
|
-
for (const helper of result.usedHelpers)
|
|
58
|
-
usedHelpers.add(helper);
|
|
59
|
-
});
|
|
60
|
-
if (helpersMode === "embedded" && !typesOnly) {
|
|
61
|
-
for (const helper of usedHelpers) {
|
|
62
|
-
const { ext, content } = await readHelperSource(helper, importExt);
|
|
63
|
-
files.push({ filename: `_helpers/${helper}.${ext}`, content });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
files.push({
|
|
67
|
-
filename: "index.ts",
|
|
68
|
-
content: generateIndexBarrel(files, { typesOnly: typesOnly ?? false, importExt })
|
|
69
|
-
});
|
|
70
|
-
return files;
|
|
71
|
-
};
|
|
72
|
-
export {
|
|
73
|
-
buildSchema
|
|
74
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds a boolean expression that is TRUE when `accessor` is structurally equal
|
|
3
|
-
* to `value` — a literal known at generation time (an `enum` member, a `const`).
|
|
4
|
-
*
|
|
5
|
-
* Why not `===` / `.includes` / `JSON.stringify`, the three forms this replaced:
|
|
6
|
-
*
|
|
7
|
-
* - `.includes(x)` is SameValueZero, i.e. *reference* equality for objects and
|
|
8
|
-
* arrays. `{ enum: [{ a: 1 }] }` could therefore never match a freshly parsed
|
|
9
|
-
* `{ a: 1 }`, so the strict parser threw on a document Ajv accepts and the fast
|
|
10
|
-
* path never fired.
|
|
11
|
-
* - `JSON.stringify(x) !== '{"a":1,"b":2}'` is key-order sensitive, so a
|
|
12
|
-
* reordered-but-equal `{ b: 2, a: 1 }` was rejected — and it serialized the
|
|
13
|
-
* whole value on every call just to answer a yes/no question.
|
|
14
|
-
*
|
|
15
|
-
* Because the literal is known up front the comparison is unrolled into a flat
|
|
16
|
-
* chain of `typeof` / `===` tests against its shape: no serialization, no
|
|
17
|
-
* allocation, and key order cannot matter because keys are compared by name. The
|
|
18
|
-
* `Object.keys(...).length === n` term is what makes it an equality rather than a
|
|
19
|
-
* "has at least these fields" check.
|
|
20
|
-
*
|
|
21
|
-
* @param accessor - Expression yielding the runtime value (may be `unknown`).
|
|
22
|
-
* @param value - The literal to compare against.
|
|
23
|
-
*/
|
|
24
|
-
export declare const generateDeepEqualCheck: (accessor: string, value: unknown) => string;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { safeAccessor } from "@amritk/helpers/safe-accessor";
|
|
2
|
-
const generateDeepEqualCheck = (accessor, value) => {
|
|
3
|
-
if (value === null || typeof value !== "object") {
|
|
4
|
-
if (typeof value === "number" && Number.isNaN(value))
|
|
5
|
-
return `Number.isNaN(${accessor})`;
|
|
6
|
-
return `${accessor} === ${JSON.stringify(value)}`;
|
|
7
|
-
}
|
|
8
|
-
if (Array.isArray(value)) {
|
|
9
|
-
const elements = `(${accessor} as unknown[])`;
|
|
10
|
-
const parts2 = [`Array.isArray(${accessor})`, `${elements}.length === ${value.length}`];
|
|
11
|
-
for (let i = 0; i < value.length; i++) {
|
|
12
|
-
parts2.push(generateDeepEqualCheck(`${elements}[${i}]`, value[i]));
|
|
13
|
-
}
|
|
14
|
-
return parts2.join(" && ");
|
|
15
|
-
}
|
|
16
|
-
const entries = Object.entries(value);
|
|
17
|
-
const record = `(${accessor} as Record<string, unknown>)`;
|
|
18
|
-
const parts = [
|
|
19
|
-
`typeof ${accessor} === "object"`,
|
|
20
|
-
`${accessor} !== null`,
|
|
21
|
-
`!Array.isArray(${accessor})`,
|
|
22
|
-
// Pins the key count, so an object carrying extra keys is not "equal" to one
|
|
23
|
-
// that only has the declared ones. Combined with the per-key value checks
|
|
24
|
-
// below (an absent key reads as `undefined`, which no JSON literal equals)
|
|
25
|
-
// this is exact structural equality.
|
|
26
|
-
`Object.keys(${accessor} as object).length === ${entries.length}`
|
|
27
|
-
];
|
|
28
|
-
for (const [key, sub] of entries) {
|
|
29
|
-
parts.push(generateDeepEqualCheck(safeAccessor(record, key), sub));
|
|
30
|
-
}
|
|
31
|
-
return parts.join(" && ");
|
|
32
|
-
};
|
|
33
|
-
export {
|
|
34
|
-
generateDeepEqualCheck
|
|
35
|
-
};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds a default string for a `pattern`-constrained schema — a value a
|
|
3
|
-
* coercing parser can fall back to when the input cannot be repaired.
|
|
4
|
-
*
|
|
5
|
-
* The one invariant: **a returned default always matches the pattern it was
|
|
6
|
-
* built from** (and any length bounds handed in alongside it). Every candidate,
|
|
7
|
-
* synthesized or recognized, is tested against the real `RegExp` before it is
|
|
8
|
-
* returned, and anything that fails is discarded. That check is the whole point.
|
|
9
|
-
* Without it this returned a value the schema rejects — `'^\\+?\\d{3}\\d{4}$'`
|
|
10
|
-
* was answered with `"+1234567890"`, which has three digits too many — so a
|
|
11
|
-
* coercing parser "repaired" input into a document still invalid against its own
|
|
12
|
-
* schema, and the array or object built around it was invalid for the same
|
|
13
|
-
* reason.
|
|
14
|
-
*
|
|
15
|
-
* Two sources of candidates, in order:
|
|
16
|
-
*
|
|
17
|
-
* 1. {@link synthesize}, which reads the pattern and constructs a matching
|
|
18
|
-
* string from it. This covers the ordinary shapes real schemas use — slugs,
|
|
19
|
-
* identifiers, fixed-width codes — that no fixed list can anticipate.
|
|
20
|
-
* 2. A short list of well-known shapes (email, UUID, URL, semver, ISO date),
|
|
21
|
-
* which read better than a synthesized `"aaa"` when they happen to fit.
|
|
22
|
-
*
|
|
23
|
-
* Returns `null` when nothing verifies, which callers already treat as "no
|
|
24
|
-
* usable default" — the safe answer, and far better than a confident wrong one.
|
|
25
|
-
*/
|
|
26
|
-
export declare const generateDefaultFromPattern: (pattern: string, minLength?: number, maxLength?: number) => string | null;
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
const RECOGNIZED_DEFAULTS = [
|
|
2
|
-
"user@example.com",
|
|
3
|
-
"00000000-0000-0000-0000-000000000000",
|
|
4
|
-
"https://example.com",
|
|
5
|
-
"1.0.0",
|
|
6
|
-
"2000-01-01",
|
|
7
|
-
"+1234567890"
|
|
8
|
-
];
|
|
9
|
-
const UNSUPPORTED = /\(\?[=!<]|\\[1-9]|\\b|\\B/;
|
|
10
|
-
const PREFERRED = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
11
|
-
const escapedChar = (sequence) => {
|
|
12
|
-
switch (sequence) {
|
|
13
|
-
case "d":
|
|
14
|
-
return "0";
|
|
15
|
-
case "w":
|
|
16
|
-
return "a";
|
|
17
|
-
case "s":
|
|
18
|
-
return " ";
|
|
19
|
-
case "D":
|
|
20
|
-
case "S":
|
|
21
|
-
return "a";
|
|
22
|
-
case "W":
|
|
23
|
-
return "-";
|
|
24
|
-
case "n":
|
|
25
|
-
return "\n";
|
|
26
|
-
case "t":
|
|
27
|
-
return " ";
|
|
28
|
-
case "r":
|
|
29
|
-
return "\r";
|
|
30
|
-
default:
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
const ONCE = { min: 1, max: 1 };
|
|
35
|
-
const pickFromClass = (body) => {
|
|
36
|
-
const negated = body.startsWith("^");
|
|
37
|
-
const source = negated ? body.slice(1) : body;
|
|
38
|
-
const members = [];
|
|
39
|
-
for (let i = 0; i < source.length; i++) {
|
|
40
|
-
const char = source[i];
|
|
41
|
-
if (char === "\\") {
|
|
42
|
-
const next = source[++i];
|
|
43
|
-
if (next === void 0)
|
|
44
|
-
return null;
|
|
45
|
-
members.push(escapedChar(next) ?? next);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (source[i + 1] === "-" && i + 2 < source.length && source[i + 2] !== "]") {
|
|
49
|
-
const low = char;
|
|
50
|
-
const high = source[i + 2];
|
|
51
|
-
const preferred = [...PREFERRED].find((candidate) => candidate >= low && candidate <= high);
|
|
52
|
-
members.push(preferred ?? low);
|
|
53
|
-
i += 2;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
members.push(char);
|
|
57
|
-
}
|
|
58
|
-
if (!negated) {
|
|
59
|
-
return [...PREFERRED].find((candidate) => members.includes(candidate)) ?? members[0] ?? null;
|
|
60
|
-
}
|
|
61
|
-
return [...PREFERRED].find((candidate) => !members.includes(candidate)) ?? null;
|
|
62
|
-
};
|
|
63
|
-
const readQuantifier = (pattern, start) => {
|
|
64
|
-
const char = pattern[start];
|
|
65
|
-
let quantifier = ONCE;
|
|
66
|
-
let next = start;
|
|
67
|
-
if (char === "*") {
|
|
68
|
-
quantifier = { min: 0, max: Number.POSITIVE_INFINITY };
|
|
69
|
-
next = start + 1;
|
|
70
|
-
} else if (char === "+") {
|
|
71
|
-
quantifier = { min: 1, max: Number.POSITIVE_INFINITY };
|
|
72
|
-
next = start + 1;
|
|
73
|
-
} else if (char === "?") {
|
|
74
|
-
quantifier = { min: 0, max: 1 };
|
|
75
|
-
next = start + 1;
|
|
76
|
-
} else if (char === "{") {
|
|
77
|
-
const close = pattern.indexOf("}", start);
|
|
78
|
-
const body = close === -1 ? null : pattern.slice(start + 1, close);
|
|
79
|
-
const match = body === null ? null : /^(\d+)(,(\d*)?)?$/.exec(body);
|
|
80
|
-
if (match && close !== -1) {
|
|
81
|
-
const min = Number(match[1]);
|
|
82
|
-
const max = match[2] === void 0 ? min : match[3] ? Number(match[3]) : Number.POSITIVE_INFINITY;
|
|
83
|
-
quantifier = { min, max };
|
|
84
|
-
next = close + 1;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (quantifier !== ONCE && (pattern[next] === "?" || pattern[next] === "+"))
|
|
88
|
-
next += 1;
|
|
89
|
-
return { quantifier, next };
|
|
90
|
-
};
|
|
91
|
-
const parseAlternatives = (pattern, start, stop) => {
|
|
92
|
-
const alternatives = [];
|
|
93
|
-
let current = [];
|
|
94
|
-
let i = start;
|
|
95
|
-
while (i < stop) {
|
|
96
|
-
const char = pattern[i];
|
|
97
|
-
if (char === ")")
|
|
98
|
-
break;
|
|
99
|
-
if (char === "|") {
|
|
100
|
-
alternatives.push(current);
|
|
101
|
-
current = [];
|
|
102
|
-
i += 1;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
let node = null;
|
|
106
|
-
if (char === "(") {
|
|
107
|
-
let bodyStart = i + 1;
|
|
108
|
-
if (pattern.startsWith("(?:", i))
|
|
109
|
-
bodyStart = i + 3;
|
|
110
|
-
else if (pattern.startsWith("(?<", i)) {
|
|
111
|
-
const close = pattern.indexOf(">", i);
|
|
112
|
-
if (close === -1)
|
|
113
|
-
return { alternatives: [], next: stop };
|
|
114
|
-
bodyStart = close + 1;
|
|
115
|
-
}
|
|
116
|
-
const inner = parseAlternatives(pattern, bodyStart, stop);
|
|
117
|
-
if (pattern[inner.next] !== ")")
|
|
118
|
-
return { alternatives: [], next: stop };
|
|
119
|
-
const { quantifier, next } = readQuantifier(pattern, inner.next + 1);
|
|
120
|
-
node = { kind: "group", alternatives: inner.alternatives, quantifier };
|
|
121
|
-
i = next;
|
|
122
|
-
} else if (char === "[") {
|
|
123
|
-
let end = i + 1;
|
|
124
|
-
if (pattern[end] === "^")
|
|
125
|
-
end += 1;
|
|
126
|
-
if (pattern[end] === "]")
|
|
127
|
-
end += 1;
|
|
128
|
-
while (end < stop && pattern[end] !== "]")
|
|
129
|
-
end += pattern[end] === "\\" ? 2 : 1;
|
|
130
|
-
if (end >= stop)
|
|
131
|
-
return { alternatives: [], next: stop };
|
|
132
|
-
const picked = pickFromClass(pattern.slice(i + 1, end));
|
|
133
|
-
if (picked === null)
|
|
134
|
-
return { alternatives: [], next: stop };
|
|
135
|
-
const { quantifier, next } = readQuantifier(pattern, end + 1);
|
|
136
|
-
node = { kind: "class", value: picked, quantifier };
|
|
137
|
-
i = next;
|
|
138
|
-
} else if (char === "\\") {
|
|
139
|
-
const sequence = pattern[i + 1];
|
|
140
|
-
if (sequence === void 0)
|
|
141
|
-
return { alternatives: [], next: stop };
|
|
142
|
-
const { quantifier, next } = readQuantifier(pattern, i + 2);
|
|
143
|
-
node = { kind: "literal", value: escapedChar(sequence) ?? sequence, quantifier };
|
|
144
|
-
i = next;
|
|
145
|
-
} else if (char === ".") {
|
|
146
|
-
const { quantifier, next } = readQuantifier(pattern, i + 1);
|
|
147
|
-
node = { kind: "class", value: "a", quantifier };
|
|
148
|
-
i = next;
|
|
149
|
-
} else if (char === "^" || char === "$") {
|
|
150
|
-
i += 1;
|
|
151
|
-
continue;
|
|
152
|
-
} else {
|
|
153
|
-
const { quantifier, next } = readQuantifier(pattern, i + 1);
|
|
154
|
-
node = { kind: "literal", value: char, quantifier };
|
|
155
|
-
i = next;
|
|
156
|
-
}
|
|
157
|
-
current.push(node);
|
|
158
|
-
}
|
|
159
|
-
alternatives.push(current);
|
|
160
|
-
return { alternatives, next: i };
|
|
161
|
-
};
|
|
162
|
-
const render = (nodes, budget) => {
|
|
163
|
-
let out = "";
|
|
164
|
-
for (const node of nodes) {
|
|
165
|
-
const { min, max } = node.quantifier;
|
|
166
|
-
const count = Math.min(min + (max > min ? budget : 0), max);
|
|
167
|
-
const unit = node.kind === "group" ? render(node.alternatives[0] ?? [], budget) : node.value;
|
|
168
|
-
out += unit.repeat(count);
|
|
169
|
-
}
|
|
170
|
-
return out;
|
|
171
|
-
};
|
|
172
|
-
const synthesize = (pattern) => {
|
|
173
|
-
if (UNSUPPORTED.test(pattern))
|
|
174
|
-
return [];
|
|
175
|
-
const { alternatives } = parseAlternatives(pattern, 0, pattern.length);
|
|
176
|
-
const candidates = [];
|
|
177
|
-
for (const alternative of alternatives) {
|
|
178
|
-
for (let budget = 0; budget <= 64; budget++) {
|
|
179
|
-
const rendered = render(alternative, budget);
|
|
180
|
-
candidates.push(rendered);
|
|
181
|
-
if (rendered.length > 512)
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return candidates;
|
|
186
|
-
};
|
|
187
|
-
const compile = (pattern) => {
|
|
188
|
-
try {
|
|
189
|
-
return new RegExp(pattern, "u");
|
|
190
|
-
} catch {
|
|
191
|
-
try {
|
|
192
|
-
return new RegExp(pattern);
|
|
193
|
-
} catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
const generateDefaultFromPattern = (pattern, minLength, maxLength) => {
|
|
199
|
-
if (pattern === "")
|
|
200
|
-
return null;
|
|
201
|
-
const regex = compile(pattern);
|
|
202
|
-
if (regex === null)
|
|
203
|
-
return null;
|
|
204
|
-
const low = typeof minLength === "number" && minLength > 0 ? minLength : 0;
|
|
205
|
-
const high = typeof maxLength === "number" && maxLength >= 0 ? maxLength : Number.POSITIVE_INFINITY;
|
|
206
|
-
const fits = (candidate) => {
|
|
207
|
-
if (candidate.length < low || candidate.length > high)
|
|
208
|
-
return false;
|
|
209
|
-
return regex.test(candidate);
|
|
210
|
-
};
|
|
211
|
-
for (const candidate of RECOGNIZED_DEFAULTS) {
|
|
212
|
-
if (fits(candidate))
|
|
213
|
-
return JSON.stringify(candidate);
|
|
214
|
-
}
|
|
215
|
-
for (const candidate of synthesize(pattern)) {
|
|
216
|
-
if (fits(candidate))
|
|
217
|
-
return JSON.stringify(candidate);
|
|
218
|
-
}
|
|
219
|
-
return null;
|
|
220
|
-
};
|
|
221
|
-
export {
|
|
222
|
-
generateDefaultFromPattern
|
|
223
|
-
};
|