@amritk/generate-parsers 0.16.3 → 0.17.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/AI.md +36 -0
- package/dist/generators/assert-supported-keywords.js +45 -72
- package/dist/generators/build-schema.js +56 -168
- package/dist/generators/generate-default-from-pattern.js +24 -31
- package/dist/generators/generate-discriminated-union-validation.js +27 -47
- package/dist/generators/generate-enum-check.js +27 -61
- package/dist/generators/generate-files.js +54 -83
- package/dist/generators/generate-non-discriminated-union-validation.js +24 -26
- package/dist/generators/generate-parser-function.d.ts +12 -0
- package/dist/generators/generate-parser-function.js +1132 -1711
- package/dist/generators/generate-schema-checks.js +216 -250
- package/dist/generators/generate-strict-assertion.js +311 -447
- package/dist/generators/generate-type-checks.js +287 -460
- package/dist/generators/generate-validation-expression.js +300 -439
- package/dist/generators/subschema-match.js +201 -266
- package/dist/helpers/apply-schema-extensions.js +18 -49
- package/dist/helpers/collect-helpers.js +58 -71
- package/dist/helpers/collect-imports.js +161 -237
- package/dist/helpers/find-discriminator.js +23 -29
- package/dist/helpers/get-default-value.js +76 -96
- package/dist/helpers/get-discriminator-value.js +16 -17
- package/dist/index.js +4 -1
- package/package.json +6 -5
package/AI.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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?)`.
|
|
26
|
+
To set a later flag you must pass every intervening positional. (The README's
|
|
27
|
+
short form omits the trailing seven.)
|
|
28
|
+
2. **It returns files in memory — it does NOT write to disk.** You write the
|
|
29
|
+
`GeneratedFile[]` (`{ filename, content }`) yourself.
|
|
30
|
+
3. **Output is more than one file per `$def`:** always an `index.ts` barrel, and
|
|
31
|
+
(unless `typesOnly`) runtime helper files.
|
|
32
|
+
4. **Default parsers COERCE invalid input to defaults** rather than throwing;
|
|
33
|
+
pass `strict: true` (the 6th positional) to make them throw.
|
|
34
|
+
|
|
35
|
+
Only the `.` entry (`buildSchema`, `GeneratedFile`, `ImportExtension`).
|
|
36
|
+
Install: `bun add @amritk/generate-parsers`.
|
|
@@ -1,74 +1,47 @@
|
|
|
1
|
-
import { isSchemaObject } from
|
|
2
|
-
import { canMatchSubschema } from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const item of node)
|
|
38
|
-
visit(item);
|
|
39
|
-
return;
|
|
1
|
+
import { isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
2
|
+
import { canMatchSubschema } from "./subschema-match.js";
|
|
3
|
+
const assertNoUnsupportedKeywords = (schema, typeName) => {
|
|
4
|
+
const fail = (keyword, detail) => {
|
|
5
|
+
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.`);
|
|
6
|
+
};
|
|
7
|
+
const visit = (node) => {
|
|
8
|
+
if (typeof node !== "object" || node === null)
|
|
9
|
+
return;
|
|
10
|
+
if (Array.isArray(node)) {
|
|
11
|
+
for (const item of node)
|
|
12
|
+
visit(item);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const record = node;
|
|
16
|
+
if ("unevaluatedProperties" in record && record["unevaluatedProperties"] !== true) {
|
|
17
|
+
fail("unevaluatedProperties", "does not implement it");
|
|
18
|
+
}
|
|
19
|
+
if ("unevaluatedItems" in record && record["unevaluatedItems"] !== true) {
|
|
20
|
+
fail("unevaluatedItems", "does not implement it");
|
|
21
|
+
}
|
|
22
|
+
if ("contains" in record && !canMatchSubschema(record["contains"])) {
|
|
23
|
+
fail("contains", "cannot prove its subschema inline");
|
|
24
|
+
}
|
|
25
|
+
if ("propertyNames" in record && isSchemaObject(record["propertyNames"])) {
|
|
26
|
+
if (!canMatchSubschema(record["propertyNames"])) {
|
|
27
|
+
fail("propertyNames", "cannot prove its subschema inline");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const dependentSchemas = record["dependentSchemas"];
|
|
31
|
+
if (typeof dependentSchemas === "object" && dependentSchemas !== null && !Array.isArray(dependentSchemas)) {
|
|
32
|
+
for (const sub of Object.values(dependentSchemas)) {
|
|
33
|
+
if (typeof sub === "boolean")
|
|
34
|
+
continue;
|
|
35
|
+
if (!canMatchSubschema(sub)) {
|
|
36
|
+
fail("dependentSchemas", "cannot prove one of its subschemas inline");
|
|
40
37
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
fail('contains', 'cannot prove its subschema inline');
|
|
51
|
-
}
|
|
52
|
-
if ('propertyNames' in record && isSchemaObject(record['propertyNames'])) {
|
|
53
|
-
if (!canMatchSubschema(record['propertyNames'])) {
|
|
54
|
-
fail('propertyNames', 'cannot prove its subschema inline');
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const dependentSchemas = record['dependentSchemas'];
|
|
58
|
-
if (typeof dependentSchemas === 'object' && dependentSchemas !== null && !Array.isArray(dependentSchemas)) {
|
|
59
|
-
for (const sub of Object.values(dependentSchemas)) {
|
|
60
|
-
// Boolean subschemas are enforced directly (see generateDependentSchemasChecks).
|
|
61
|
-
if (typeof sub === 'boolean')
|
|
62
|
-
continue;
|
|
63
|
-
if (!canMatchSubschema(sub)) {
|
|
64
|
-
fail('dependentSchemas', 'cannot prove one of its subschemas inline');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// `for..in` (not `Object.values`) to recurse without allocating an array per
|
|
69
|
-
// node — this walk runs for every strict parser generated.
|
|
70
|
-
for (const key in record)
|
|
71
|
-
visit(record[key]);
|
|
72
|
-
};
|
|
73
|
-
visit(schema);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const key in record)
|
|
41
|
+
visit(record[key]);
|
|
42
|
+
};
|
|
43
|
+
visit(schema);
|
|
44
|
+
};
|
|
45
|
+
export {
|
|
46
|
+
assertNoUnsupportedKeywords
|
|
74
47
|
};
|
|
@@ -1,174 +1,62 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import { createRequire } from
|
|
3
|
-
import { dirname, resolve as resolvePath } from
|
|
4
|
-
import { generateIndexBarrel } from
|
|
5
|
-
import { walkRefGraph } from
|
|
6
|
-
import { applySchemaExtensions } from
|
|
7
|
-
import { generateFile } from
|
|
8
|
-
/**
|
|
9
|
-
* Loads a runtime helper's source to copy into `_helpers/` in embedded mode.
|
|
10
|
-
*
|
|
11
|
-
* Prefers the TypeScript source (`src/<helper>.ts`), rewriting any extensionless
|
|
12
|
-
* sibling import (e.g. `validate-record` → `./is-object`) to `./is-object.js` so
|
|
13
|
-
* the embedded copy resolves under Node ESM, not only Bun. When the resolved
|
|
14
|
-
* `@amritk/helpers` is a published/cached tarball that ships only `dist/` (its
|
|
15
|
-
* `files` historically excluded `src/`, which is exactly why `bunx mjst` crashed
|
|
16
|
-
* on this lookup), it falls back to the always-published compiled `dist/<helper>.js`
|
|
17
|
-
* — which tsc already emits with `.js`-extension imports — and writes it verbatim.
|
|
18
|
-
*/
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel";
|
|
5
|
+
import { walkRefGraph } from "@amritk/helpers/walk-ref-graph";
|
|
6
|
+
import { applySchemaExtensions } from "../helpers/apply-schema-extensions.js";
|
|
7
|
+
import { generateFile } from "./generate-files.js";
|
|
19
8
|
const readHelperSource = async (helper, importExt) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// crashed the CLI on load. The `\b` blocks that scan (in the emitted text,
|
|
30
|
-
// `from` is preceded by the word character `b`, so tsc-alias's own `\bfrom`
|
|
31
|
-
// never matches) and is also semantically correct: an import keyword always
|
|
32
|
-
// sits at a word boundary. The dist smoke test (scripts/dist-smoke.test.ts)
|
|
33
|
-
// fails the build if this ever regresses.
|
|
34
|
-
return { ext: 'ts', content: source.replace(/\bfrom '(\.\/[^'".]+)'/g, `from '$1.${importExt}'`) };
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
const compiled = await readFile(resolvePath(helpersRoot, 'dist', `${helper}.js`), 'utf-8');
|
|
38
|
-
// With `.ts` import specifiers the on-disk file must literally be `.ts`, so
|
|
39
|
-
// ship the compiled JS (valid TS) under that name and retarget its own
|
|
40
|
-
// sibling imports to match. The pattern keeps the same `\bfrom` shape as
|
|
41
|
-
// above so tsc-alias's scanner can't corrupt it (see the comment there).
|
|
42
|
-
if (importExt === 'ts') {
|
|
43
|
-
return { ext: 'ts', content: compiled.replace(/\bfrom '(\.\/[^'"]+)\.js'/g, "from '$1.ts'") };
|
|
44
|
-
}
|
|
45
|
-
return { ext: 'js', content: compiled };
|
|
9
|
+
const require2 = createRequire(import.meta.url);
|
|
10
|
+
const helpersRoot = dirname(require2.resolve("@amritk/helpers/package.json"));
|
|
11
|
+
try {
|
|
12
|
+
const source = await readFile(resolvePath(helpersRoot, "src", `${helper}.ts`), "utf-8");
|
|
13
|
+
return { ext: "ts", content: source.replace(/\bfrom '(\.\/[^'".]+)'/g, `from '$1.${importExt}'`) };
|
|
14
|
+
} catch {
|
|
15
|
+
const compiled = await readFile(resolvePath(helpersRoot, "dist", `${helper}.js`), "utf-8");
|
|
16
|
+
if (importExt === "ts") {
|
|
17
|
+
return { ext: "ts", content: compiled.replace(/\bfrom '(\.\/[^'"]+)\.js'/g, "from '$1.ts'") };
|
|
46
18
|
}
|
|
19
|
+
return { ext: "js", content: compiled };
|
|
20
|
+
}
|
|
47
21
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* @param helpersMode - `'package'` (default) emits `import ... from '@amritk/helpers/...'`.
|
|
70
|
-
* `'embedded'` emits `import ... from './_helpers/...'` and appends the helper sources
|
|
71
|
-
* as additional `GeneratedFile` entries so the output directory is self-contained.
|
|
72
|
-
* @param helpersImportPrefix - Relative path prefix to the `_helpers/` directory in
|
|
73
|
-
* embedded mode. Defaults to `'./'`. The recursive multi-schema build passes `'../'`,
|
|
74
|
-
* `'../../'`, etc. so nested parsers can import from a single shared `_helpers/`
|
|
75
|
-
* directory while the helper sources are emitted once at the output root.
|
|
76
|
-
* @param readonly - When true, every property, array, and record in the generated type
|
|
77
|
-
* definitions is emitted as `readonly`.
|
|
78
|
-
* @param stripUnknown - When true, the generated parsers build their result from declared
|
|
79
|
-
* properties only, silently dropping undeclared input keys at every nesting level (zod's
|
|
80
|
-
* `.strip()`) without treating extras as a validation error. Composes with `strict` and
|
|
81
|
-
* yields to `additionalProperties: false` (which rejects rather than strips in strict mode).
|
|
82
|
-
* @param typeSuffix - Suffix appended to every type/parser name derived from a `$ref`
|
|
83
|
-
* (e.g. `'Object'` → `ContactObject`). Defaults to `''` (no suffix). The root type name
|
|
84
|
-
* is used verbatim and is not affected by this suffix.
|
|
85
|
-
* @param importExt - Extension used on every relative import specifier in the generated
|
|
86
|
-
* output (cross-file `$ref` imports, the index barrel, and embedded-helper imports).
|
|
87
|
-
* `'js'` (this function's default) is the standard TS NodeNext form; `'ts'` emits the
|
|
88
|
-
* literal on-disk paths so the generated sources run directly under Node's type
|
|
89
|
-
* stripping. Note the `mjst` CLI defaults this to `'ts'` (falling back to `'js'` only
|
|
90
|
-
* under `--build`, where tsc must compile the sources) so generated output runs under
|
|
91
|
-
* Node without a build step; direct callers of `buildSchema` opt in explicitly.
|
|
92
|
-
* @param caseInsensitive - When true, the generated coercing parsers normalize a mis-cased
|
|
93
|
-
* string to the exact casing of a declared `enum`/`const` member it matches
|
|
94
|
-
* case-insensitively (e.g. `hElLo` → `hello`) instead of coercing to the default. Coerce
|
|
95
|
-
* mode only — strict parsers still reject a casing mismatch. The normalization lives on
|
|
96
|
-
* the coercion failure branch, so correctly-cased input keeps the exact-match fast path
|
|
97
|
-
* and the hot path is unaffected.
|
|
98
|
-
* @returns An array of generated TypeScript files
|
|
99
|
-
*
|
|
100
|
-
* @example
|
|
101
|
-
* ```typescript
|
|
102
|
-
* const schema = {
|
|
103
|
-
* type: "object",
|
|
104
|
-
* properties: {
|
|
105
|
-
* info: { $ref: "#/$defs/info" }
|
|
106
|
-
* },
|
|
107
|
-
* $defs: {
|
|
108
|
-
* info: {
|
|
109
|
-
* type: "object",
|
|
110
|
-
* properties: {
|
|
111
|
-
* title: { type: "string" }
|
|
112
|
-
* }
|
|
113
|
-
* }
|
|
114
|
-
* }
|
|
115
|
-
* };
|
|
116
|
-
*
|
|
117
|
-
* const files = buildSchema(schema, "Document");
|
|
118
|
-
*
|
|
119
|
-
* // Types-only mode — no parser functions or runtime helpers included:
|
|
120
|
-
* const typesFiles = buildSchema(schema, "Document", undefined, true);
|
|
121
|
-
*
|
|
122
|
-
* // With extensions:
|
|
123
|
-
* const filesWithExtensions = buildSchema(schema, "Document", {
|
|
124
|
-
* info: {
|
|
125
|
-
* 'x-internal': { type: 'boolean' },
|
|
126
|
-
* },
|
|
127
|
-
* });
|
|
128
|
-
* ```
|
|
129
|
-
*/
|
|
130
|
-
export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = 'package', helpersImportPrefix = './', readonly = false, stripUnknown = false, typeSuffix = '', importExt = 'js', caseInsensitive = false) => {
|
|
131
|
-
const files = [];
|
|
132
|
-
const usedHelpers = new Set();
|
|
133
|
-
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
|
|
134
|
-
// `index` is reserved for the barrel below, so never let a definition of
|
|
135
|
-
// that name overwrite it.
|
|
136
|
-
if (node.filename === 'index')
|
|
137
|
-
return;
|
|
138
|
-
// Extensions are keyed by definition name, which is the node's filename for
|
|
139
|
-
// both the root (the lowercased root type name) and every `$ref` target.
|
|
140
|
-
const extended = extensions ? applySchemaExtensions(node.schema, node.filename, extensions) : node.schema;
|
|
141
|
-
const result = generateFile(extended, node.typeName, {
|
|
142
|
-
typesOnly: typesOnly ?? false,
|
|
143
|
-
rootSchema: node.rootSchema,
|
|
144
|
-
helpersMode,
|
|
145
|
-
helpersImportPrefix,
|
|
146
|
-
readonly,
|
|
147
|
-
typeSuffix,
|
|
148
|
-
importExt,
|
|
149
|
-
isRoot: node.isRoot,
|
|
150
|
-
...(node.ref !== undefined ? { selfRef: node.ref } : {}),
|
|
151
|
-
...(logWarnings !== undefined ? { logWarnings } : {}),
|
|
152
|
-
...(strict !== undefined ? { strict } : {}),
|
|
153
|
-
...(stripUnknown ? { stripUnknown } : {}),
|
|
154
|
-
...(caseInsensitive ? { caseInsensitive } : {}),
|
|
155
|
-
});
|
|
156
|
-
files.push({ filename: `${node.filename}.ts`, content: result.content });
|
|
157
|
-
for (const helper of result.usedHelpers)
|
|
158
|
-
usedHelpers.add(helper);
|
|
22
|
+
const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false, stripUnknown = false, typeSuffix = "", importExt = "js", caseInsensitive = false) => {
|
|
23
|
+
const files = [];
|
|
24
|
+
const usedHelpers = /* @__PURE__ */ new Set();
|
|
25
|
+
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
|
|
26
|
+
if (node.filename === "index")
|
|
27
|
+
return;
|
|
28
|
+
const extended = extensions ? applySchemaExtensions(node.schema, node.filename, extensions) : node.schema;
|
|
29
|
+
const result = generateFile(extended, node.typeName, {
|
|
30
|
+
typesOnly: typesOnly ?? false,
|
|
31
|
+
rootSchema: node.rootSchema,
|
|
32
|
+
helpersMode,
|
|
33
|
+
helpersImportPrefix,
|
|
34
|
+
readonly,
|
|
35
|
+
typeSuffix,
|
|
36
|
+
importExt,
|
|
37
|
+
isRoot: node.isRoot,
|
|
38
|
+
...node.ref !== void 0 ? { selfRef: node.ref } : {},
|
|
39
|
+
...logWarnings !== void 0 ? { logWarnings } : {},
|
|
40
|
+
...strict !== void 0 ? { strict } : {},
|
|
41
|
+
...stripUnknown ? { stripUnknown } : {},
|
|
42
|
+
...caseInsensitive ? { caseInsensitive } : {}
|
|
159
43
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
44
|
+
files.push({ filename: `${node.filename}.ts`, content: result.content });
|
|
45
|
+
for (const helper of result.usedHelpers)
|
|
46
|
+
usedHelpers.add(helper);
|
|
47
|
+
});
|
|
48
|
+
if (helpersMode === "embedded" && !typesOnly) {
|
|
49
|
+
for (const helper of usedHelpers) {
|
|
50
|
+
const { ext, content } = await readHelperSource(helper, importExt);
|
|
51
|
+
files.push({ filename: `_helpers/${helper}.${ext}`, content });
|
|
168
52
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
})
|
|
173
|
-
|
|
53
|
+
}
|
|
54
|
+
files.push({
|
|
55
|
+
filename: "index.ts",
|
|
56
|
+
content: generateIndexBarrel(files, { typesOnly: typesOnly ?? false, importExt })
|
|
57
|
+
});
|
|
58
|
+
return files;
|
|
59
|
+
};
|
|
60
|
+
export {
|
|
61
|
+
buildSchema
|
|
174
62
|
};
|
|
@@ -1,33 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return '"user@example.com"';
|
|
1
|
+
const generateDefaultFromPattern = (pattern) => {
|
|
2
|
+
if (pattern.includes("@") || pattern.includes("[a-zA-Z0-9]") && pattern.includes("\\.")) {
|
|
3
|
+
return '"user@example.com"';
|
|
4
|
+
}
|
|
5
|
+
if (pattern.includes("[0-9a-f]") && pattern.includes("{8}") || pattern.includes("\\-")) {
|
|
6
|
+
if (pattern.match(/[0-9a-f].*\{8\}/i) || pattern.match(/[0-9a-fA-F]{8}/)) {
|
|
7
|
+
return '"00000000-0000-0000-0000-000000000000"';
|
|
9
8
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
// Phone number patterns
|
|
29
|
-
if (pattern.includes('\\d{3}') && (pattern.includes('\\d{4}') || pattern.includes('\\d{7}'))) {
|
|
30
|
-
return '"+1234567890"';
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
9
|
+
}
|
|
10
|
+
if (pattern.includes("https?") || pattern.includes("http")) {
|
|
11
|
+
return '"https://example.com"';
|
|
12
|
+
}
|
|
13
|
+
if (pattern.match(/\d.*\\\.\.*\d/)) {
|
|
14
|
+
return '"1.0.0"';
|
|
15
|
+
}
|
|
16
|
+
if (pattern.includes("\\d{4}") && pattern.includes("\\d{2}")) {
|
|
17
|
+
return '"2000-01-01"';
|
|
18
|
+
}
|
|
19
|
+
if (pattern.includes("\\d{3}") && (pattern.includes("\\d{4}") || pattern.includes("\\d{7}"))) {
|
|
20
|
+
return '"+1234567890"';
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
export {
|
|
25
|
+
generateDefaultFromPattern
|
|
33
26
|
};
|
|
@@ -1,49 +1,29 @@
|
|
|
1
|
-
import { safeAccessor } from
|
|
2
|
-
import { getDiscriminatorValue } from
|
|
3
|
-
import { generateSchemaChecks } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* also evaluated branches in reverse with the wrong per-case default.
|
|
16
|
-
*/
|
|
17
|
-
export const generateDiscriminatedUnionValidation = (accessor, schemas, discriminatorKey, defaultValue, isRequired) => {
|
|
18
|
-
// A non-identifier discriminator key (e.g. `x-type`) must use bracket access;
|
|
19
|
-
// `${accessor}?.${key}` would emit broken TS. `safeAccessor` picks dot vs
|
|
20
|
-
// bracket notation and JSON.stringifies the literal so quotes-in-keys are safe.
|
|
21
|
-
const discAccessor = safeAccessor(`${accessor}?`, discriminatorKey);
|
|
22
|
-
// Each branch contributes its guard condition and the value to yield on a
|
|
23
|
-
// match; the value is `accessor` because `generateSchemaChecks` has already
|
|
24
|
-
// proven the branch's shape (no coercion happens here).
|
|
25
|
-
const branches = [];
|
|
26
|
-
for (const schema of schemas) {
|
|
27
|
-
const discriminatorValue = getDiscriminatorValue(schema, discriminatorKey);
|
|
28
|
-
if (discriminatorValue === null)
|
|
29
|
-
continue;
|
|
30
|
-
let condition = `${discAccessor} === ${JSON.stringify(discriminatorValue)}`;
|
|
31
|
-
const checks = generateSchemaChecks(accessor, schema);
|
|
32
|
-
for (const check of checks) {
|
|
33
|
-
condition += ' && ' + check;
|
|
34
|
-
}
|
|
35
|
-
branches.push(condition);
|
|
1
|
+
import { safeAccessor } from "@amritk/helpers/safe-accessor";
|
|
2
|
+
import { getDiscriminatorValue } from "../helpers/get-discriminator-value.js";
|
|
3
|
+
import { generateSchemaChecks } from "./generate-schema-checks.js";
|
|
4
|
+
const generateDiscriminatedUnionValidation = (accessor, schemas, discriminatorKey, defaultValue, isRequired) => {
|
|
5
|
+
const discAccessor = safeAccessor(`${accessor}?`, discriminatorKey);
|
|
6
|
+
const branches = [];
|
|
7
|
+
for (const schema of schemas) {
|
|
8
|
+
const discriminatorValue = getDiscriminatorValue(schema, discriminatorKey);
|
|
9
|
+
if (discriminatorValue === null)
|
|
10
|
+
continue;
|
|
11
|
+
let condition = `${discAccessor} === ${JSON.stringify(discriminatorValue)}`;
|
|
12
|
+
const checks = generateSchemaChecks(accessor, schema);
|
|
13
|
+
for (const check of checks) {
|
|
14
|
+
condition += " && " + check;
|
|
36
15
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
16
|
+
branches.push(condition);
|
|
17
|
+
}
|
|
18
|
+
if (branches.length === 0) {
|
|
19
|
+
return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
|
|
20
|
+
}
|
|
21
|
+
let result = isRequired ? defaultValue : accessor;
|
|
22
|
+
for (let i = branches.length - 1; i >= 0; i--) {
|
|
23
|
+
result = `${branches[i]} ? ${accessor} : ${result}`;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
export {
|
|
28
|
+
generateDiscriminatedUnionValidation
|
|
49
29
|
};
|
|
@@ -1,63 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* `===` chain is allocation-free and matches the validators package's
|
|
9
|
-
* `enumMembershipExpr`. It falls back to `.includes` when a member is an
|
|
10
|
-
* object/array (reference equality) or `NaN` (where `includes`'s SameValueZero
|
|
11
|
-
* differs from `===`), so the verdict is unchanged.
|
|
12
|
-
*/
|
|
13
|
-
export const generateEnumCheck = (accessor, enumValues) => {
|
|
14
|
-
const allPrimitive = enumValues.length > 0 &&
|
|
15
|
-
enumValues.every((v) => (v === null || typeof v !== 'object') && typeof v !== 'function') &&
|
|
16
|
-
!enumValues.some((v) => typeof v === 'number' && Number.isNaN(v));
|
|
17
|
-
if (allPrimitive) {
|
|
18
|
-
return `(${enumValues.map((v) => `${accessor} === ${JSON.stringify(v)}`).join(' || ')})`;
|
|
19
|
-
}
|
|
20
|
-
const serializedEnum = JSON.stringify(enumValues);
|
|
21
|
-
return `${serializedEnum}.includes(${accessor} as never)`;
|
|
1
|
+
const generateEnumCheck = (accessor, enumValues) => {
|
|
2
|
+
const allPrimitive = enumValues.length > 0 && enumValues.every((v) => (v === null || typeof v !== "object") && typeof v !== "function") && !enumValues.some((v) => typeof v === "number" && Number.isNaN(v));
|
|
3
|
+
if (allPrimitive) {
|
|
4
|
+
return `(${enumValues.map((v) => `${accessor} === ${JSON.stringify(v)}`).join(" || ")})`;
|
|
5
|
+
}
|
|
6
|
+
const serializedEnum = JSON.stringify(enumValues);
|
|
7
|
+
return `${serializedEnum}.includes(${accessor} as never)`;
|
|
22
8
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (typeof value !== 'string')
|
|
44
|
-
continue;
|
|
45
|
-
const lower = value.toLowerCase();
|
|
46
|
-
// First writer wins, so declaration order breaks ties between members that
|
|
47
|
-
// fold to the same key (e.g. `"on"` and `"ON"`). `Set` membership is used
|
|
48
|
-
// instead of `lower in obj` so a member folding to an inherited name like
|
|
49
|
-
// `constructor` or `toString` is not wrongly treated as already-present.
|
|
50
|
-
if (seen.has(lower))
|
|
51
|
-
continue;
|
|
52
|
-
seen.add(lower);
|
|
53
|
-
entries.push([lower, value]);
|
|
54
|
-
}
|
|
55
|
-
if (entries.length === 0)
|
|
56
|
-
return null;
|
|
57
|
-
// A `Map` — not a plain object — so a folded key that collides with an
|
|
58
|
-
// inherited member (`constructor`, `toString`, `__proto__`, …) resolves to
|
|
59
|
-
// `undefined` (→ `fallback`) rather than an `Object.prototype` value. A
|
|
60
|
-
// plain-object lookup would return the inherited function/object for such a key.
|
|
61
|
-
const map = `new Map(${JSON.stringify(entries)})`;
|
|
62
|
-
return `(typeof ${accessor} === "string" ? (${map}.get((${accessor} as string).toLowerCase()) ?? ${fallback}) : ${fallback})`;
|
|
9
|
+
const generateEnumCaseInsensitiveCoercion = (accessor, enumValues, fallback) => {
|
|
10
|
+
const entries = [];
|
|
11
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12
|
+
for (const value of enumValues) {
|
|
13
|
+
if (typeof value !== "string")
|
|
14
|
+
continue;
|
|
15
|
+
const lower = value.toLowerCase();
|
|
16
|
+
if (seen.has(lower))
|
|
17
|
+
continue;
|
|
18
|
+
seen.add(lower);
|
|
19
|
+
entries.push([lower, value]);
|
|
20
|
+
}
|
|
21
|
+
if (entries.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
const map = `new Map(${JSON.stringify(entries)})`;
|
|
24
|
+
return `(typeof ${accessor} === "string" ? (${map}.get((${accessor} as string).toLowerCase()) ?? ${fallback}) : ${fallback})`;
|
|
25
|
+
};
|
|
26
|
+
export {
|
|
27
|
+
generateEnumCaseInsensitiveCoercion,
|
|
28
|
+
generateEnumCheck
|
|
63
29
|
};
|