@amritk/generate-parsers 0.20.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI.md +4 -2
- package/README.md +87 -23
- package/dist/generators/build-schema.d.ts +7 -1
- package/dist/generators/build-schema.js +4 -2
- package/dist/generators/generate-files.d.ts +7 -0
- package/dist/generators/generate-files.js +4 -1
- package/dist/generators/generate-parser-function.d.ts +9 -1
- package/dist/generators/generate-parser-function.js +333 -128
- package/dist/generators/generate-strict-assertion.d.ts +6 -0
- package/package.json +9 -4
package/AI.md
CHANGED
|
@@ -22,9 +22,11 @@ const files = await buildSchema(schema, 'Document')
|
|
|
22
22
|
|
|
23
23
|
1. **`buildSchema` is `async` and takes POSITIONAL args, no options object.** The
|
|
24
24
|
full signature is
|
|
25
|
-
`buildSchema(rootSchema, rootTypeName, extensions?, typesOnly?, logWarnings?, strict?, helpersMode?, helpersImportPrefix?, readonly?, stripUnknown?, typeSuffix?, importExt?, caseInsensitive?)`.
|
|
25
|
+
`buildSchema(rootSchema, rootTypeName, extensions?, typesOnly?, logWarnings?, strict?, helpersMode?, helpersImportPrefix?, readonly?, stripUnknown?, typeSuffix?, importExt?, caseInsensitive?, schemas?, unknownKeys?)`.
|
|
26
26
|
To set a later flag you must pass every intervening positional. (The README's
|
|
27
|
-
short form omits the trailing seven.)
|
|
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.
|
|
28
30
|
2. **It returns files in memory — it does NOT write to disk.** You write the
|
|
29
31
|
`GeneratedFile[]` (`{ filename, content }`) yourself. `rootTypeName` becomes
|
|
30
32
|
that filename (lowercased) as well as the `export type` name, so it must be a
|
package/README.md
CHANGED
|
@@ -95,6 +95,7 @@ Every option after the first two is **positional** — pass them in this order:
|
|
|
95
95
|
| 12 | `importExt` | `'js' \| 'ts'` | `'js'` | Extension emitted on relative import specifiers. `'js'` is the NodeNext form for output you compile; `'ts'` emits the literal on-disk paths so the sources run unbuilt. |
|
|
96
96
|
| 13 | `caseInsensitive` | `boolean` | `false` | Normalize a mis-cased string to the exact casing of an enum/const member it matches case-insensitively. Coerce mode only. |
|
|
97
97
|
| 14 | `schemas` | `Record<string, unknown>` | — | Documents you have **already loaded**, keyed by the absolute URI a `$ref` names them by. See below. |
|
|
98
|
+
| 15 | `unknownKeys` | `'count-keys' \| 'count-enumerable'` | `'count-keys'` | How a fast path proves a closed object (`additionalProperties: false`, or a `stripUnknown` build) has no undeclared key: a prototype-guarded `Object.keys(input).length` (fastest on Bun) or a `for…in` count (fastest on Node). See [Counting keys on the fast path](#counting-keys-on-the-fast-path). |
|
|
98
99
|
|
|
99
100
|
Returns: `Promise<GeneratedFile[]>`.
|
|
100
101
|
|
|
@@ -184,6 +185,14 @@ nobody registered still stops the build, with a message naming the ref.
|
|
|
184
185
|
<tr>
|
|
185
186
|
<td colspan="3">Normalize a mis-cased string to the exact casing of a declared enum/const member it matches case-insensitively (e.g. hElLo → hello) instead of coercing to the default. Coerce mode only — strict parsers still reject a casing mismatch. Correctly-cased input keeps the exact-match fast path, so the hot path is unaffected.</td>
|
|
186
187
|
</tr>
|
|
188
|
+
<tr>
|
|
189
|
+
<td>🔢 <code>unknownKeys</code></td>
|
|
190
|
+
<td><code>string</code></td>
|
|
191
|
+
<td align="center"><code>"count-keys"</code></td>
|
|
192
|
+
</tr>
|
|
193
|
+
<tr>
|
|
194
|
+
<td colspan="3">How a fast path proves a closed object (additionalProperties: false, or a stripUnknown build) carries no undeclared key. 'count-keys' is a prototype-guarded Object.keys(input).length comparison, the faster form on JavaScriptCore (Bun); 'count-enumerable' is a for…in count that allocates nothing; it was the faster form on Node 22, but on Node 26 'count-keys' ties or wins there too. Keep the default unless you are pinned to an older V8 and have measured your own shapes; nothing detects the runtime at runtime.<br><strong>Allowed:</strong> <code>"count-enumerable"</code>, <code>"count-keys"</code></td>
|
|
195
|
+
</tr>
|
|
187
196
|
</tbody>
|
|
188
197
|
</table>
|
|
189
198
|
<!-- config-table-end -->
|
|
@@ -237,9 +246,10 @@ Three deliberate departures:
|
|
|
237
246
|
Ajv accepts `"a string"` against `{ properties: { a: {} } }`; a parser has to
|
|
238
247
|
return the type it declares, and the declared type is an object.
|
|
239
248
|
|
|
240
|
-
If a strict parser cannot enforce something — a `$ref` cycle
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
If a strict parser cannot enforce something — a `$ref` cycle inside a subschema it
|
|
250
|
+
has to match inline (`contains`, `not`, `propertyNames`, `dependentSchemas`, or an
|
|
251
|
+
`unevaluated*` backstop), which the inline matcher will not unroll; a subschema
|
|
252
|
+
using a keyword it cannot prove — generation **fails with an error naming the keyword** rather than
|
|
243
253
|
emitting a parser that quietly accepts what the schema forbids. Coercing
|
|
244
254
|
(non-strict) parsers are documented to repair rather than reject, so they ignore
|
|
245
255
|
the rejecting keywords by design.
|
|
@@ -266,8 +276,9 @@ no I/O to answer the retrieval step. Everything else — applying the base URIs,
|
|
|
266
276
|
walking anchors across documents, emitting a parser per definition and an import
|
|
267
277
|
graph that links — the generator still has to do.
|
|
268
278
|
|
|
269
|
-
Of the 41 that do not, **18**
|
|
270
|
-
|
|
279
|
+
Of the 41 that do not, **18** follow from the third departure above — the
|
|
280
|
+
`ignores a non-object` cases plus three `ref.json` cases where a recursive `$ref`
|
|
281
|
+
lands on a root that declares `properties` — and **12** are a keyword strict mode will not
|
|
271
282
|
approximate (a cyclic `$ref` with siblings, a cyclic `unevaluatedProperties`) —
|
|
272
283
|
those cost a build error naming the cause, never a wrong verdict. The rest: **8**
|
|
273
284
|
`$dynamicRef`s whose binding depends on the evaluation path (a generator emits one
|
|
@@ -296,20 +307,34 @@ keys) and `parseStrict` (assert + reject undeclared keys) halves of
|
|
|
296
307
|
[`moltar/typescript-runtime-type-benchmarks`](https://github.com/moltar/typescript-runtime-type-benchmarks)
|
|
297
308
|
against the other *pure* parsers — the ones that return a new typed value rather
|
|
298
309
|
than mutating in place — [zod](https://zod.dev) and
|
|
299
|
-
[TypeBox](https://github.com/sinclairzx81/typebox).
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
~
|
|
310
|
+
[TypeBox](https://github.com/sinclairzx81/typebox).
|
|
311
|
+
|
|
312
|
+
Both runtimes were measured together on one machine (Linux x64, a 4-vCPU cloud
|
|
313
|
+
box, Bun 1.4.0 and Node 26.8.1 — the same machine and runtimes as every table in
|
|
314
|
+
this repo), each cell the median of three separate runs of the whole suite.
|
|
315
|
+
Within one sitting a cell repeats to within a few percent; the same suite
|
|
316
|
+
measured hours later moved ~60% across every case at once, so read the ratios
|
|
317
|
+
and treat the absolutes as a property of that box.
|
|
318
|
+
|
|
319
|
+
Parsing valid input at steady state:
|
|
320
|
+
|
|
321
|
+
| case | mode | Bun: mjst | Bun: zod | Bun: typebox | Node: mjst | Node: zod | Node: typebox |
|
|
322
|
+
|:--|:--|--:|--:|--:|--:|--:|--:|
|
|
323
|
+
| user (4 fields) | parseSafe | **~163M** | ~3.3M | ~1.7M | **~88M** | ~4.0M | ~0.67M |
|
|
324
|
+
| order (nested + array) | parseSafe | **~7.7M** | ~0.60M | ~0.26M | **~8.2M** | ~0.64M | ~0.15M |
|
|
325
|
+
| assert (moltar shape) | parseSafe | **~120M** | ~3.5M | ~0.87M | **~66M** | ~5.3M | ~0.35M |
|
|
326
|
+
| user (4 fields) | parseStrict | **~43M** | ~1.9M | ~2.2M | **~55M** | ~2.7M | ~1.5M |
|
|
327
|
+
| order (nested + array) | parseStrict | **~13M** | ~0.36M | ~0.43M | **~8.7M** | ~0.55M | ~0.25M |
|
|
328
|
+
| assert (moltar shape) | parseStrict | **~44M** | ~1.4M | ~1.2M | **~34M** | ~3.5M | ~0.80M |
|
|
329
|
+
|
|
330
|
+
<sub>ops/s, higher is better.</sub>
|
|
331
|
+
|
|
332
|
+
Unlike the validator suite — where TypeBox's compiled checker takes the loose
|
|
333
|
+
moltar shape on V8 — the generated parser leads every case on both engines:
|
|
334
|
+
~12–49× over zod on Bun and ~13–22× on Node. The two `user`/`assert` safe cells
|
|
335
|
+
are the ones to read as ratios rather than absolutes: stripping to four declared
|
|
336
|
+
keys builds one small object and nothing else, which is fast enough that the
|
|
337
|
+
engine's inlining, not the parser, sets the number.
|
|
313
338
|
|
|
314
339
|
What is replicated upstream is the *cases* — the two parse modes and their
|
|
315
340
|
shapes — not the harness. These numbers come from this repo's own measurement
|
|
@@ -321,9 +346,9 @@ reproducible run of both harnesses:
|
|
|
321
346
|
[Against the moltar harness](../generate-validators#against-the-moltar-harness).
|
|
322
347
|
|
|
323
348
|
The trade is a one-shot **prepare** cost that only mjst pays — generating the
|
|
324
|
-
parser source — which measures **~0.
|
|
325
|
-
|
|
326
|
-
nothing to time). That is trivially amortized: you generate once at build time
|
|
349
|
+
parser source — which measures **~0.20–0.74 ms** per schema on Bun and
|
|
350
|
+
**~0.16–0.72 ms** on Node (zod and TypeBox author or interpret their parsers
|
|
351
|
+
with no separate build step, so there is nothing to time). That is trivially amortized: you generate once at build time
|
|
327
352
|
and run the emitted code forever.
|
|
328
353
|
|
|
329
354
|
Every library is checked for agreement — same stripped/rejected output, same
|
|
@@ -333,9 +358,48 @@ the optimiser can't hoist the work away. Micro-benchmark figures vary by machine
|
|
|
333
358
|
and runtime — reproduce with:
|
|
334
359
|
|
|
335
360
|
```bash
|
|
336
|
-
bun run bench
|
|
361
|
+
bun run bench # Bun / JavaScriptCore
|
|
362
|
+
bun run bench:node # Node / V8 (builds the package first, then runs it under node)
|
|
337
363
|
```
|
|
338
364
|
|
|
365
|
+
### Counting keys on the fast path
|
|
366
|
+
|
|
367
|
+
A closed object whose declared properties are all required proves "no undeclared
|
|
368
|
+
key" by counting: the typed checks ahead of the count have already proven every
|
|
369
|
+
declared key present, so exactly *n* keys means no extra. `unknownKeys` (the 15th
|
|
370
|
+
positional argument, `--unknown-keys` on the CLI) picks how the count is spelled,
|
|
371
|
+
because the two spellings trade places between the engines:
|
|
372
|
+
|
|
373
|
+
- **`'count-keys'`** (default) — `Object.getPrototypeOf(input) === Object.prototype
|
|
374
|
+
&& Object.keys(input).length === n`, one more term of the fast-path guard. The
|
|
375
|
+
prototype guard keeps an own-key count sound: a crafted prototype could satisfy
|
|
376
|
+
the typed checks through an inherited declared key while an own extra kept the
|
|
377
|
+
count at *n*, so a non-plain object takes the cold path instead, whose `for…in`
|
|
378
|
+
rejection sees the inherited key.
|
|
379
|
+
- **`'count-enumerable'`** — `let c = 0; for (const k in input) c++`, as
|
|
380
|
+
statements behind the guard. It allocates nothing and needs no prototype guard
|
|
381
|
+
(the count sees inherited keys too), and on V8 it is answered from the enum
|
|
382
|
+
cache. On JavaScriptCore a `for…in` over a non-extensible object is the engine's
|
|
383
|
+
slow path, and the strict parse runs at half speed.
|
|
384
|
+
|
|
385
|
+
A nested object's shape is proven on the parent's fast path by the same checks
|
|
386
|
+
its own shape validator makes, spelled out over the cached local rather than
|
|
387
|
+
called — `typeof _nested === "object" && … && Object.keys(_nested).length === n`
|
|
388
|
+
— one level deep and within a size budget, because a call was the one thing
|
|
389
|
+
left on the strict fast path that JavaScriptCore would not see through. Under
|
|
390
|
+
the moltar harness (each case alone in its own process) `parseStrict` reaches
|
|
391
|
+
the harness floor on Bun 1.4.0 (~360M ops/s, the call eliminated) with
|
|
392
|
+
`count-keys` against ~22M with `count-enumerable`.
|
|
393
|
+
|
|
394
|
+
On Node the answer has moved with the engine. On Node 22 the two spellings were
|
|
395
|
+
level (~14M either way, which is where the advice to flip it came from); on
|
|
396
|
+
Node 26.8.1 `count-keys` measures ~29M against `count-enumerable`'s ~26M, so the
|
|
397
|
+
default now ties or wins on both engines. Keep the default unless you are
|
|
398
|
+
pinned to an older V8 and have measured your own shapes. See
|
|
399
|
+
[Choosing how keys are counted](../generate-validators#choosing-how-keys-are-counted)
|
|
400
|
+
in the sister package for the validator numbers and the full trade-off. Whichever
|
|
401
|
+
you pick, every verdict on a value that could have come from JSON is the same.
|
|
402
|
+
|
|
339
403
|
---
|
|
340
404
|
|
|
341
405
|
## Related packages
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
|
|
1
2
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
3
|
import type { HelpersMode } from '../helpers/collect-helpers.js';
|
|
3
4
|
import type { ImportExtension } from '../helpers/collect-imports.js';
|
|
@@ -70,6 +71,11 @@ export type GeneratedFile = {
|
|
|
70
71
|
* is yours to do (or `@amritk/resolve-refs`'), and a `$ref` to a URI nobody registered still
|
|
71
72
|
* stops generation. Only the documents actually referenced get files, so registering more
|
|
72
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.
|
|
73
79
|
* @returns An array of generated TypeScript files
|
|
74
80
|
*
|
|
75
81
|
* @example
|
|
@@ -102,4 +108,4 @@ export type GeneratedFile = {
|
|
|
102
108
|
* });
|
|
103
109
|
* ```
|
|
104
110
|
*/
|
|
105
|
-
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
|
|
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[]>;
|
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { foldNullable } from "@amritk/helpers/fold-nullable";
|
|
5
5
|
import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel";
|
|
6
|
+
import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
|
|
6
7
|
import { walkRefGraph } from "@amritk/helpers/walk-ref-graph";
|
|
7
8
|
import { applySchemaExtensions } from "../helpers/apply-schema-extensions.js";
|
|
8
9
|
import { generateFile } from "./generate-files.js";
|
|
@@ -26,7 +27,7 @@ const assertRootTypeName = (rootTypeName) => {
|
|
|
26
27
|
return;
|
|
27
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.`);
|
|
28
29
|
};
|
|
29
|
-
const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false, stripUnknown = false, typeSuffix = "", importExt = "js", caseInsensitive = false, schemas) => {
|
|
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) => {
|
|
30
31
|
assertRootTypeName(rootTypeName);
|
|
31
32
|
const files = [];
|
|
32
33
|
const usedHelpers = /* @__PURE__ */ new Set();
|
|
@@ -49,7 +50,8 @@ const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logW
|
|
|
49
50
|
...logWarnings !== void 0 ? { logWarnings } : {},
|
|
50
51
|
...strict !== void 0 ? { strict } : {},
|
|
51
52
|
...stripUnknown ? { stripUnknown } : {},
|
|
52
|
-
...caseInsensitive ? { caseInsensitive } : {}
|
|
53
|
+
...caseInsensitive ? { caseInsensitive } : {},
|
|
54
|
+
unknownKeys
|
|
53
55
|
});
|
|
54
56
|
files.push({ filename: `${node.filename}.ts`, content: result.content });
|
|
55
57
|
for (const helper of result.usedHelpers)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
|
|
1
2
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
3
|
import { type HelpersMode, type RuntimeHelperName } from '../helpers/collect-helpers.js';
|
|
3
4
|
import { type ImportExtension } from '../helpers/collect-imports.js';
|
|
@@ -92,6 +93,12 @@ type GenerateFileOptions = {
|
|
|
92
93
|
* already-valid input keeps the exact-match fast path.
|
|
93
94
|
*/
|
|
94
95
|
readonly caseInsensitive?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* How the generated fast paths prove a closed object carries no undeclared
|
|
98
|
+
* key — `Object.keys` (the default) or a `for…in` count. See
|
|
99
|
+
* {@link UnknownKeysStrategy} for the trade-off between the two.
|
|
100
|
+
*/
|
|
101
|
+
readonly unknownKeys?: UnknownKeysStrategy;
|
|
95
102
|
};
|
|
96
103
|
/** Result of generating a single parser file. */
|
|
97
104
|
export type GeneratedFileContent = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateTypeDefinition } from "@amritk/helpers/generate-type-definition";
|
|
2
|
+
import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
|
|
2
3
|
import { collectHelpers } from "../helpers/collect-helpers.js";
|
|
3
4
|
import { collectImports, collectImportTypeNames } from "../helpers/collect-imports.js";
|
|
4
5
|
import { generateParserFunction, generateShapeValidator } from "./generate-parser-function.js";
|
|
@@ -40,12 +41,14 @@ const generateFile = (schema, typeName, options) => {
|
|
|
40
41
|
rootSchema,
|
|
41
42
|
typeSuffix
|
|
42
43
|
});
|
|
43
|
-
const
|
|
44
|
+
const unknownKeys = options?.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
|
|
45
|
+
const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown, reservedNames, unknownKeys);
|
|
44
46
|
const parserFunction = generateParserFunction(schema, typeName, {
|
|
45
47
|
useRefImports: true,
|
|
46
48
|
typeSuffix,
|
|
47
49
|
reservedNames,
|
|
48
50
|
shapeValidatorSource: shapeValidator,
|
|
51
|
+
unknownKeys,
|
|
49
52
|
...options?.isRoot !== void 0 ? { isRoot: options.isRoot } : {},
|
|
50
53
|
...rootSchema !== void 0 ? { rootSchema } : {},
|
|
51
54
|
...options?.logWarnings !== void 0 ? { logWarnings: options.logWarnings } : {},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
|
|
1
2
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
3
|
/**
|
|
3
4
|
* Options for controlling parser function generation behavior.
|
|
@@ -67,6 +68,13 @@ type GenerateParserOptions = {
|
|
|
67
68
|
* exact `===` fast path and the hot path is unaffected.
|
|
68
69
|
*/
|
|
69
70
|
readonly caseInsensitive?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* How the fast paths prove a closed object carries no undeclared key —
|
|
73
|
+
* `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length`
|
|
74
|
+
* (the default) or a `for…in` count. See {@link UnknownKeysStrategy} for the
|
|
75
|
+
* trade-off, and {@link keyCountCheck} for what each form emits.
|
|
76
|
+
*/
|
|
77
|
+
readonly unknownKeys?: UnknownKeysStrategy;
|
|
70
78
|
/**
|
|
71
79
|
* The exact source of the `validate{TypeName}Shape` predicate emitted into
|
|
72
80
|
* the same file as this parser (generate-files produces it for the root
|
|
@@ -108,5 +116,5 @@ export declare const shapeValidatorFunctionName: (typeName: string) => string;
|
|
|
108
116
|
* in additionalProperties). The stub is safe: parents calling it will fall
|
|
109
117
|
* through to the slow path, matching the pre-deep-fast-path behavior.
|
|
110
118
|
*/
|
|
111
|
-
export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean, stripUnknown?: boolean, reservedNames?: ReadonlySet<string
|
|
119
|
+
export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean, stripUnknown?: boolean, reservedNames?: ReadonlySet<string>, unknownKeys?: UnknownKeysStrategy) => string;
|
|
112
120
|
export {};
|
|
@@ -5,6 +5,7 @@ import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
|
5
5
|
import { missingCheck, safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
|
|
6
6
|
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasMaxItems, hasMaxProperties, hasMinItems, hasMinProperties, hasOneOf, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
7
7
|
import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
|
|
8
|
+
import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
|
|
8
9
|
import { findDiscriminator } from "../helpers/find-discriminator.js";
|
|
9
10
|
import { getDefaultValue } from "../helpers/get-default-value.js";
|
|
10
11
|
import { getDiscriminatorValue } from "../helpers/get-discriminator-value.js";
|
|
@@ -177,6 +178,11 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
|
|
|
177
178
|
if (isSchemaObject(schema) && ("if" in schema || "then" in schema || "else" in schema)) {
|
|
178
179
|
return `{} as ${typeName}`;
|
|
179
180
|
}
|
|
181
|
+
if (isSchemaObject(schema) && hasAllOf(schema)) {
|
|
182
|
+
const composesConditional = schema.allOf.some((member) => isSchemaObject(member) && (hasRef(member) || "if" in member || "then" in member || "else" in member));
|
|
183
|
+
if (composesConditional)
|
|
184
|
+
return `{} as ${typeName}`;
|
|
185
|
+
}
|
|
180
186
|
const requiredProps = [];
|
|
181
187
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
182
188
|
const isRequired = isPropertyRequired(key, schema);
|
|
@@ -250,7 +256,8 @@ const strictContext = (ctx) => ({
|
|
|
250
256
|
useRefImports: ctx?.useRefImports ?? false,
|
|
251
257
|
suffix: ctx?.suffix ?? "",
|
|
252
258
|
...ctx?.rootSchema !== void 0 ? { rootSchema: ctx.rootSchema } : {},
|
|
253
|
-
...ctx?.stripUnknown !== void 0 ? { stripUnknown: ctx.stripUnknown } : {}
|
|
259
|
+
...ctx?.stripUnknown !== void 0 ? { stripUnknown: ctx.stripUnknown } : {},
|
|
260
|
+
...ctx?.unknownKeys !== void 0 ? { unknownKeys: ctx.unknownKeys } : {}
|
|
254
261
|
});
|
|
255
262
|
const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
256
263
|
const functionName = generateParserName(typeName);
|
|
@@ -368,11 +375,12 @@ ${delegatedBody(itemParserName)}
|
|
|
368
375
|
const useRefImports = unionCtx?.useRefImports ?? false;
|
|
369
376
|
const suffix = unionCtx?.suffix ?? "";
|
|
370
377
|
const stripUnknown = unionCtx?.stripUnknown ?? false;
|
|
371
|
-
const
|
|
378
|
+
const unknownKeys = unionCtx?.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
|
|
379
|
+
const itemShapeValidator = generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved, unknownKeys);
|
|
372
380
|
const preamble = [
|
|
373
381
|
`type ${itemName} = NonNullable<${typeName}>[number];`,
|
|
374
382
|
itemShapeValidator,
|
|
375
|
-
generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false, itemShapeValidator)
|
|
383
|
+
generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false, itemShapeValidator, unknownKeys)
|
|
376
384
|
].join("\n\n");
|
|
377
385
|
return `${preamble}
|
|
378
386
|
|
|
@@ -548,13 +556,21 @@ const exactKeyCountOf = (schema) => {
|
|
|
548
556
|
return null;
|
|
549
557
|
return declaredCount;
|
|
550
558
|
};
|
|
559
|
+
const KEY_COUNT_VAR = "_keyCount";
|
|
560
|
+
const keyCountCheck = (unknownKeys, count, indent, obj = "input") => unknownKeys === "count-keys" ? {
|
|
561
|
+
statements: [],
|
|
562
|
+
verdict: `Object.getPrototypeOf(${obj}) === Object.prototype && Object.keys(${obj}).length === ${count}`
|
|
563
|
+
} : {
|
|
564
|
+
statements: [`${indent}let ${KEY_COUNT_VAR} = 0;`, `${indent}for (const _k in ${obj}) ${KEY_COUNT_VAR}++;`],
|
|
565
|
+
verdict: `${KEY_COUNT_VAR} === ${count}`
|
|
566
|
+
};
|
|
551
567
|
const shouldCacheVariable = (propSchema) => {
|
|
552
568
|
return isSchemaObject(propSchema);
|
|
553
569
|
};
|
|
554
570
|
const INLINE_NESTED_MAX_PROPERTIES = 12;
|
|
555
571
|
const INLINE_NESTED_MAX_SITES = 4;
|
|
556
572
|
const INLINE_NESTED_MAX_TOTAL_PROPERTIES = 24;
|
|
557
|
-
const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImports, suffix, stripUnknown, reservedNames) => {
|
|
573
|
+
const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImports, suffix, stripUnknown, reservedNames, claimVarName) => {
|
|
558
574
|
if (!isSchemaObject(childSchema) || !hasProperties(childSchema))
|
|
559
575
|
return null;
|
|
560
576
|
if (!stripUnknown)
|
|
@@ -576,6 +592,7 @@ const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImpo
|
|
|
576
592
|
const keys = Object.keys(props);
|
|
577
593
|
if (keys.length === 0 || keys.length > INLINE_NESTED_MAX_PROPERTIES)
|
|
578
594
|
return null;
|
|
595
|
+
const declarations = [];
|
|
579
596
|
const checks = [];
|
|
580
597
|
const fields = [];
|
|
581
598
|
const readRoot = `(${objectExpr} as Record<string, any>)`;
|
|
@@ -584,22 +601,64 @@ const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImpo
|
|
|
584
601
|
if (shouldUseRefImport(propSchema, useRefImports) || shouldUseArrayRefImport(propSchema, useRefImports) || shouldUseRecordRefImport(propSchema, useRefImports)) {
|
|
585
602
|
return null;
|
|
586
603
|
}
|
|
587
|
-
const accessor =
|
|
604
|
+
const accessor = claimVarName(`${objectExpr}_${toVarName(key).slice(1)}`);
|
|
605
|
+
declarations.push(` const ${accessor} = ${safeAccessor(readRoot, key)};`);
|
|
588
606
|
const check = generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
|
|
589
607
|
if (check === null)
|
|
590
608
|
return null;
|
|
591
609
|
checks.push(check);
|
|
592
610
|
fields.push(`${safeLiteralKey(key)}: ${accessor}`);
|
|
593
611
|
}
|
|
594
|
-
const condition =
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
612
|
+
const condition = checks.join(" && ");
|
|
613
|
+
return { declarations, condition, literal: `{ ${fields.join(", ")} }`, propertyCount: keys.length };
|
|
614
|
+
};
|
|
615
|
+
const RESULT_VARIABLE = "out";
|
|
616
|
+
const PROTOTYPE_MEMBERS = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
617
|
+
const resultMember = (key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? `${RESULT_VARIABLE}.${key}` : `${RESULT_VARIABLE}[${JSON.stringify(key)}]`;
|
|
618
|
+
const renderLiteralEntry = (entry, indent) => {
|
|
619
|
+
if (entry.kind === "field")
|
|
620
|
+
return `${indent}${safeLiteralKey(entry.key)}: ${entry.value},`;
|
|
621
|
+
if (entry.kind === "spread")
|
|
622
|
+
return `${indent}...${entry.expression},`;
|
|
623
|
+
return `${indent}...(${entry.read} !== undefined && { ${safeLiteralKey(entry.key)}: ${entry.value} }),`;
|
|
624
|
+
};
|
|
625
|
+
const renderResultObject = (entries, typeName, guard) => {
|
|
626
|
+
const firstOptional = entries.findIndex((entry) => entry.kind === "optional");
|
|
627
|
+
const assignable = entries.every((entry, index) => index < firstOptional || entry.kind === "spread" || !PROTOTYPE_MEMBERS.has(entry.key));
|
|
628
|
+
if (firstOptional === -1 || !assignable) {
|
|
629
|
+
const open = guard === void 0 ? " return {" : ` if (${guard}) return {`;
|
|
630
|
+
return [
|
|
631
|
+
open,
|
|
632
|
+
entries.map((entry) => renderLiteralEntry(entry, " ")).join("\n"),
|
|
633
|
+
` } as unknown as ${typeName};`
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
const indent = guard === void 0 ? " " : " ";
|
|
637
|
+
const lines = [];
|
|
638
|
+
if (guard !== void 0)
|
|
639
|
+
lines.push(` if (${guard}) {`);
|
|
640
|
+
const literal = entries.slice(0, firstOptional);
|
|
641
|
+
if (literal.length === 0) {
|
|
642
|
+
lines.push(`${indent}const ${RESULT_VARIABLE}: Record<string, unknown> = {};`);
|
|
643
|
+
} else {
|
|
644
|
+
lines.push(`${indent}const ${RESULT_VARIABLE}: Record<string, unknown> = {`);
|
|
645
|
+
lines.push(literal.map((entry) => renderLiteralEntry(entry, `${indent} `)).join("\n"));
|
|
646
|
+
lines.push(`${indent}};`);
|
|
647
|
+
}
|
|
648
|
+
for (const entry of entries.slice(firstOptional)) {
|
|
649
|
+
if (entry.kind === "field")
|
|
650
|
+
lines.push(`${indent}${resultMember(entry.key)} = ${entry.value};`);
|
|
651
|
+
else if (entry.kind === "optional") {
|
|
652
|
+
lines.push(`${indent}if (${entry.read} !== undefined) ${resultMember(entry.key)} = ${entry.value};`);
|
|
653
|
+
} else
|
|
654
|
+
lines.push(`${indent}Object.assign(${RESULT_VARIABLE}, ${entry.expression});`);
|
|
655
|
+
}
|
|
656
|
+
lines.push(`${indent}return ${RESULT_VARIABLE} as unknown as ${typeName};`);
|
|
657
|
+
if (guard !== void 0)
|
|
658
|
+
lines.push(" }");
|
|
659
|
+
return lines;
|
|
601
660
|
};
|
|
602
|
-
const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource) => {
|
|
661
|
+
const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource, unknownKeys = DEFAULT_UNKNOWN_KEYS) => {
|
|
603
662
|
const functionName = generateParserName(typeName);
|
|
604
663
|
const exportPrefix = exported ? "export " : "";
|
|
605
664
|
if (!hasProperties(schema)) {
|
|
@@ -618,21 +677,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
618
677
|
for (const [key, subName] of subTypeNames) {
|
|
619
678
|
const propSchema = schemaProps[key];
|
|
620
679
|
preamble.push(`type ${subName} = NonNullable<${typeName}>[${JSON.stringify(key)}];`);
|
|
621
|
-
const subShapeValidator = generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames);
|
|
680
|
+
const subShapeValidator = generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames, unknownKeys);
|
|
622
681
|
preamble.push(subShapeValidator);
|
|
623
|
-
preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, subShapeValidator));
|
|
682
|
+
preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, subShapeValidator, unknownKeys));
|
|
624
683
|
}
|
|
625
684
|
for (const [key, subName] of subItemNames) {
|
|
626
685
|
const propSchema = schemaProps[key];
|
|
627
686
|
const itemSchema = propSchema.items;
|
|
628
687
|
preamble.push(`type ${subName} = NonNullable<NonNullable<${typeName}>[${JSON.stringify(key)}]>[number];`);
|
|
629
|
-
const itemShapeValidator = generateShapeValidator(itemSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames);
|
|
688
|
+
const itemShapeValidator = generateShapeValidator(itemSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames, unknownKeys);
|
|
630
689
|
preamble.push(itemShapeValidator);
|
|
631
690
|
preamble.push(`const _every${subName} = (arr: readonly unknown[]): boolean => {
|
|
632
691
|
for (let i = 0; i < arr.length; i++) if (!${shapeValidatorName(subName)}(arr[i])) return false;
|
|
633
692
|
return true;
|
|
634
693
|
};`);
|
|
635
|
-
preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, itemShapeValidator));
|
|
694
|
+
preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, itemShapeValidator, unknownKeys));
|
|
636
695
|
}
|
|
637
696
|
const strictKeys = hasStrictKeys(schema);
|
|
638
697
|
const stripKeys = strictKeys || stripUnknown;
|
|
@@ -663,7 +722,9 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
663
722
|
let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema) && !(strict && inlineAllOfMembers(schema).length > 0);
|
|
664
723
|
const fastPathChecks = [];
|
|
665
724
|
const fastPathAccessorChecks = [];
|
|
666
|
-
const usedVarNames = /* @__PURE__ */ new Set();
|
|
725
|
+
const usedVarNames = /* @__PURE__ */ new Set([KEY_COUNT_VAR]);
|
|
726
|
+
let inlinedGuardSites = 0;
|
|
727
|
+
let inlinedGuardProperties = 0;
|
|
667
728
|
for (const key of propertyKeys) {
|
|
668
729
|
const propSchema = schemaProps[key];
|
|
669
730
|
const isRequired = isPropertyRequired(key, schema);
|
|
@@ -679,7 +740,13 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
679
740
|
continue;
|
|
680
741
|
}
|
|
681
742
|
const subName = subTypeNames.get(key);
|
|
682
|
-
|
|
743
|
+
const inlined = subName !== void 0 && inlinedGuardSites < INLINE_NESTED_MAX_SITES ? inlineNestedShapeCheck(propSchema, subName, varName, useRefImports, suffix, stripUnknown, reservedNames, unknownKeys) : null;
|
|
744
|
+
const withinBudget = inlined !== null && inlinedGuardProperties + inlined.propertyCount <= INLINE_NESTED_MAX_TOTAL_PROPERTIES;
|
|
745
|
+
if (withinBudget) {
|
|
746
|
+
inlinedGuardSites++;
|
|
747
|
+
inlinedGuardProperties += inlined.propertyCount;
|
|
748
|
+
}
|
|
749
|
+
let check = withinBudget ? inlined.check : subName ? `${shapeValidatorName(subName)}(${varName})` : generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
|
|
683
750
|
const itemSubName = subItemNames.get(key);
|
|
684
751
|
if (check !== null && itemSubName) {
|
|
685
752
|
check = `${check} && _every${itemSubName}(${varName})`;
|
|
@@ -711,32 +778,24 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
711
778
|
fastPathAccessorChecks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
|
|
712
779
|
}
|
|
713
780
|
}
|
|
714
|
-
|
|
715
|
-
|
|
781
|
+
const fastPathKeyCount = fastPathNeedsKnownKeys ? exactKeyCount : null;
|
|
782
|
+
if (fastPathNeedsKnownKeys && exactKeyCount === null) {
|
|
783
|
+
fastPathChecks.push(`_hasOnlyKnownKeys${typeName}(input)`);
|
|
716
784
|
}
|
|
717
|
-
const deepGuard = canFastPath && fastPathChecks.length > 0 ? fastPathChecks.join(" && ") : null;
|
|
785
|
+
const deepGuard = canFastPath && (fastPathChecks.length > 0 || fastPathKeyCount !== null) ? fastPathChecks.join(" && ") : null;
|
|
718
786
|
const literalReturnUsesVars = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema)) && exported;
|
|
719
787
|
let deepGuardExpr = deepGuard;
|
|
720
788
|
if (deepGuard !== null && shapeValidatorSource !== void 0 && !literalReturnUsesVars && !droppedKnownKeysTerm) {
|
|
721
789
|
const shapeFnName = shapeValidatorName(typeName);
|
|
722
790
|
const shapeStrictKeysGuard = stripKeys && exactKeyCount === null ? `
|
|
723
791
|
if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
|
|
724
|
-
const
|
|
725
|
-
if (stripKeys && exactKeyCount !== null) {
|
|
726
|
-
shapeChecks.push(`Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}`);
|
|
727
|
-
}
|
|
728
|
-
const expectedShapeValidator = shapeChecks.length === 0 ? shapeStrictKeysGuard ? `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => {
|
|
729
|
-
if (!isObject(input)) return false;${shapeStrictKeysGuard}
|
|
730
|
-
return true;
|
|
731
|
-
};` : `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => isObject(input);` : `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => {
|
|
732
|
-
if (!isObject(input)) return false;${shapeStrictKeysGuard}
|
|
733
|
-
return ${shapeChecks.join("\n && ")};
|
|
734
|
-
};`;
|
|
792
|
+
const expectedShapeValidator = renderShapeValidator(exportPrefix, shapeFnName, fastPathAccessorChecks, stripKeys ? exactKeyCount : null, shapeStrictKeysGuard, unknownKeys);
|
|
735
793
|
if (shapeValidatorSource === expectedShapeValidator) {
|
|
736
794
|
deepGuardExpr = `${shapeFnName}(input)`;
|
|
737
795
|
}
|
|
738
796
|
}
|
|
739
797
|
const deepGuardCallsShape = deepGuardExpr !== deepGuard;
|
|
798
|
+
const inlineKeyCount = deepGuardCallsShape ? null : fastPathKeyCount;
|
|
740
799
|
let canShallowGuard = strict && stripUnknown && !strictKeys && !hasFastPathBlockingKeyword(schema);
|
|
741
800
|
const shallowChecks = [];
|
|
742
801
|
if (canShallowGuard) {
|
|
@@ -776,18 +835,25 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
776
835
|
warnLines.push(` }`);
|
|
777
836
|
warnLines.push(` }`);
|
|
778
837
|
}
|
|
779
|
-
const
|
|
780
|
-
|
|
838
|
+
const claimNestedVarName = (base) => {
|
|
839
|
+
let name = base;
|
|
840
|
+
while (usedVarNames.has(name))
|
|
841
|
+
name = `${name}_`;
|
|
842
|
+
usedVarNames.add(name);
|
|
843
|
+
return name;
|
|
844
|
+
};
|
|
845
|
+
const buildObjectLines = (directAssign, hoistLines) => {
|
|
846
|
+
const entries = [];
|
|
781
847
|
let inlinedSites = 0;
|
|
782
848
|
let inlinedProperties = 0;
|
|
783
849
|
if (!stripKeys) {
|
|
784
|
-
|
|
850
|
+
entries.push({ kind: "spread", expression: "input" });
|
|
785
851
|
}
|
|
786
852
|
if (useRefImports && isSchemaObject(schema) && hasAllOf(schema)) {
|
|
787
853
|
for (const entry of schema.allOf) {
|
|
788
854
|
if (isSchemaObject(entry) && hasRef(entry)) {
|
|
789
855
|
const parserName = generateParserName(refToName(entry.$ref, suffix));
|
|
790
|
-
|
|
856
|
+
entries.push({ kind: "spread", expression: `(${parserName}(input) as Record<string, unknown>)` });
|
|
791
857
|
}
|
|
792
858
|
}
|
|
793
859
|
}
|
|
@@ -798,18 +864,23 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
798
864
|
if (subName) {
|
|
799
865
|
const subParserName = generateParserName(subName);
|
|
800
866
|
if (isRequired) {
|
|
801
|
-
const inlined = directAssign && !logWarnings && inlinedSites < INLINE_NESTED_MAX_SITES ? inlineNestedFastPath(propSchema, subName, accessor, useRefImports, suffix, stripUnknown, reservedNames) : null;
|
|
867
|
+
const inlined = hoistLines !== void 0 && directAssign && !logWarnings && inlinedSites < INLINE_NESTED_MAX_SITES ? inlineNestedFastPath(propSchema, subName, accessor, useRefImports, suffix, stripUnknown, reservedNames, claimNestedVarName) : null;
|
|
802
868
|
if (inlined !== null && inlinedProperties + inlined.propertyCount <= INLINE_NESTED_MAX_TOTAL_PROPERTIES) {
|
|
803
869
|
inlinedSites++;
|
|
804
870
|
inlinedProperties += inlined.propertyCount;
|
|
805
|
-
|
|
871
|
+
hoistLines?.push(...inlined.declarations);
|
|
872
|
+
entries.push({
|
|
873
|
+
kind: "field",
|
|
874
|
+
key,
|
|
875
|
+
value: `(${inlined.condition})
|
|
806
876
|
? ${inlined.literal}
|
|
807
|
-
: ${subParserName}(${accessor})
|
|
877
|
+
: ${subParserName}(${accessor})`
|
|
878
|
+
});
|
|
808
879
|
continue;
|
|
809
880
|
}
|
|
810
|
-
|
|
881
|
+
entries.push({ kind: "field", key, value: `${subParserName}(${accessor})` });
|
|
811
882
|
} else {
|
|
812
|
-
|
|
883
|
+
entries.push({ kind: "optional", key, read: accessor, value: `${subParserName}(${accessor})` });
|
|
813
884
|
}
|
|
814
885
|
continue;
|
|
815
886
|
}
|
|
@@ -817,9 +888,14 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
817
888
|
if (itemSubName) {
|
|
818
889
|
const itemParserName = generateParserName(itemSubName);
|
|
819
890
|
if (isRequired) {
|
|
820
|
-
|
|
891
|
+
entries.push({ kind: "field", key, value: `validateArray(${accessor}, ${itemParserName})` });
|
|
821
892
|
} else {
|
|
822
|
-
|
|
893
|
+
entries.push({
|
|
894
|
+
kind: "optional",
|
|
895
|
+
key,
|
|
896
|
+
read: accessor,
|
|
897
|
+
value: `validateArray(${accessor}, ${itemParserName})`
|
|
898
|
+
});
|
|
823
899
|
}
|
|
824
900
|
continue;
|
|
825
901
|
}
|
|
@@ -827,9 +903,9 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
827
903
|
const ref = propSchema.$ref;
|
|
828
904
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
829
905
|
if (isRequired) {
|
|
830
|
-
|
|
906
|
+
entries.push({ kind: "field", key, value: `${parserName}(${accessor})` });
|
|
831
907
|
} else {
|
|
832
|
-
|
|
908
|
+
entries.push({ kind: "optional", key, read: accessor, value: `${parserName}(${accessor})` });
|
|
833
909
|
}
|
|
834
910
|
continue;
|
|
835
911
|
}
|
|
@@ -838,9 +914,9 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
838
914
|
const ref = items.$ref;
|
|
839
915
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
840
916
|
if (isRequired) {
|
|
841
|
-
|
|
917
|
+
entries.push({ kind: "field", key, value: `validateArray(${accessor}, ${parserName})` });
|
|
842
918
|
} else {
|
|
843
|
-
|
|
919
|
+
entries.push({ kind: "optional", key, read: accessor, value: `validateArray(${accessor}, ${parserName})` });
|
|
844
920
|
}
|
|
845
921
|
continue;
|
|
846
922
|
}
|
|
@@ -849,21 +925,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
849
925
|
const ref = additionalProps.$ref;
|
|
850
926
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
851
927
|
if (isRequired) {
|
|
852
|
-
|
|
928
|
+
entries.push({ kind: "field", key, value: `validateRecord(${accessor}, ${parserName})` });
|
|
853
929
|
} else {
|
|
854
|
-
|
|
930
|
+
entries.push({ kind: "optional", key, read: accessor, value: `validateRecord(${accessor}, ${parserName})` });
|
|
855
931
|
}
|
|
856
932
|
continue;
|
|
857
933
|
}
|
|
858
934
|
if (!isSchemaObject(propSchema)) {
|
|
859
935
|
if (propSchema === true) {
|
|
860
936
|
if (isRequired) {
|
|
861
|
-
|
|
937
|
+
entries.push({ kind: "field", key, value: accessor });
|
|
862
938
|
} else if (stripKeys) {
|
|
863
|
-
|
|
939
|
+
entries.push({ kind: "optional", key, read: accessor, value: accessor });
|
|
864
940
|
}
|
|
865
941
|
} else if (isRequired) {
|
|
866
|
-
|
|
942
|
+
entries.push({ kind: "field", key, value: "undefined" });
|
|
867
943
|
}
|
|
868
944
|
continue;
|
|
869
945
|
}
|
|
@@ -876,25 +952,47 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
876
952
|
valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, void 0, void 0, shouldCache ? varName : void 0, knownNotUndefined, caseInsensitive);
|
|
877
953
|
}
|
|
878
954
|
if (isRequired) {
|
|
879
|
-
|
|
955
|
+
entries.push({ kind: "field", key, value: valueExpr });
|
|
880
956
|
} else {
|
|
881
|
-
|
|
957
|
+
entries.push({ kind: "optional", key, read: accessor, value: valueExpr });
|
|
882
958
|
}
|
|
883
959
|
}
|
|
884
|
-
return
|
|
960
|
+
return entries;
|
|
885
961
|
};
|
|
886
|
-
const emitReturn = (lines2,
|
|
887
|
-
lines2.push(
|
|
888
|
-
|
|
889
|
-
|
|
962
|
+
const emitReturn = (lines2, entries) => {
|
|
963
|
+
lines2.push(...renderResultObject(entries, typeName));
|
|
964
|
+
};
|
|
965
|
+
const withKeyCount = (lines2, guarded) => {
|
|
966
|
+
if (inlineKeyCount === null) {
|
|
967
|
+
lines2.push(...guarded(deepGuardExpr));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const count = keyCountCheck(unknownKeys, inlineKeyCount, " ");
|
|
971
|
+
if (count.statements.length === 0) {
|
|
972
|
+
lines2.push(...guarded(deepGuardExpr === "" ? count.verdict : `${deepGuardExpr} && ${count.verdict}`));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const counted = guarded(count.verdict);
|
|
976
|
+
if (deepGuardExpr === "") {
|
|
977
|
+
lines2.push(...keyCountCheck(unknownKeys, inlineKeyCount, " ").statements, ...counted);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const nested = counted.map((line) => line.split("\n").map((part) => ` ${part}`).join("\n"));
|
|
981
|
+
lines2.push(` if (${deepGuardExpr}) {`, ...count.statements, ...nested, ` }`);
|
|
982
|
+
};
|
|
983
|
+
const emitGuardedReturn = (lines2, statement) => {
|
|
984
|
+
withKeyCount(lines2, (guard) => [` if (${guard}) ${statement}`]);
|
|
985
|
+
};
|
|
986
|
+
const emitGuardedResult = (lines2, fields) => {
|
|
987
|
+
withKeyCount(lines2, (guard) => renderResultObject(fields, typeName, guard));
|
|
890
988
|
};
|
|
891
989
|
const emitDeepGuardReturn = (lines2) => {
|
|
892
990
|
if (!fastPathBuildsDeclaredLiteral) {
|
|
893
|
-
lines2
|
|
991
|
+
emitGuardedReturn(lines2, `return { ...input } as ${typeName};`);
|
|
894
992
|
return;
|
|
895
993
|
}
|
|
896
994
|
if (!exported && fastPathNeedsKnownKeys) {
|
|
897
|
-
lines2
|
|
995
|
+
emitGuardedReturn(lines2, `return input as ${typeName};`);
|
|
898
996
|
return;
|
|
899
997
|
}
|
|
900
998
|
const fields = [];
|
|
@@ -902,33 +1000,90 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
902
1000
|
if (!isSchemaObject(propSchema)) {
|
|
903
1001
|
if (propSchema === true) {
|
|
904
1002
|
const acc = safeAccessor("input", key);
|
|
905
|
-
fields.push(isRequired ?
|
|
1003
|
+
fields.push(isRequired ? { kind: "field", key, value: acc } : { kind: "optional", key, read: acc, value: acc });
|
|
906
1004
|
} else if (isRequired) {
|
|
907
|
-
fields.push(
|
|
1005
|
+
fields.push({ kind: "field", key, value: "undefined" });
|
|
908
1006
|
}
|
|
909
1007
|
continue;
|
|
910
1008
|
}
|
|
911
1009
|
const accessor = shouldCacheVariable(propSchema) ? varName : safeAccessor("input", key);
|
|
912
|
-
fields.push(isRequired ?
|
|
1010
|
+
fields.push(isRequired ? { kind: "field", key, value: accessor } : { kind: "optional", key, read: accessor, value: accessor });
|
|
913
1011
|
}
|
|
914
|
-
lines2
|
|
915
|
-
lines2.push(fields.join("\n"));
|
|
916
|
-
lines2.push(` } as unknown as ${typeName};`);
|
|
1012
|
+
emitGuardedResult(lines2, fields);
|
|
917
1013
|
};
|
|
1014
|
+
const assertionLines = strict ? generateObjectStrictAssertion(schema, typeName, {
|
|
1015
|
+
useRefImports,
|
|
1016
|
+
suffix,
|
|
1017
|
+
stripUnknown,
|
|
1018
|
+
...rootSchema !== void 0 ? { rootSchema } : {}
|
|
1019
|
+
}) : [];
|
|
1020
|
+
const unknownKeyThrowLines = () => rejectsUnknownKeys ? [
|
|
1021
|
+
` for (const _k in input) {`,
|
|
1022
|
+
` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`,
|
|
1023
|
+
` }`
|
|
1024
|
+
] : [];
|
|
1025
|
+
const varsAfterGuard = deepGuardCallsShape && shallowGuard === null;
|
|
1026
|
+
const assertBodyLines = [...assertionLines.slice(1), ...shallowGuard === null ? unknownKeyThrowLines() : []];
|
|
1027
|
+
const canSplit = !logWarnings && strict === true && (deepGuard !== null || shallowGuard !== null) && assertBodyLines.length > 0;
|
|
1028
|
+
const assertName = `_assert${typeName}`;
|
|
1029
|
+
const assertObjectName = `_assert${typeName}Object`;
|
|
1030
|
+
const emitAssertObjectFunction = () => `const ${assertObjectName}: (input: unknown) => asserts input is Record<string, unknown> = (input) => {
|
|
1031
|
+
${assertionLines[0]}
|
|
1032
|
+
};`;
|
|
1033
|
+
const emitAssertFunction = () => `const ${assertName} = (input: Record<string, unknown>): void => {
|
|
1034
|
+
${assertBodyLines.join("\n")}
|
|
1035
|
+
};`;
|
|
918
1036
|
const lines = [];
|
|
919
1037
|
lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
|
|
920
|
-
|
|
921
|
-
|
|
1038
|
+
if (canSplit) {
|
|
1039
|
+
preamble.push(emitAssertObjectFunction());
|
|
1040
|
+
preamble.push(emitAssertFunction());
|
|
1041
|
+
lines.push(` if (!isObject(input)) ${assertObjectName}(input);`);
|
|
1042
|
+
if (!varsAfterGuard)
|
|
1043
|
+
lines.push(...varDeclLines);
|
|
1044
|
+
if (shallowGuard) {
|
|
1045
|
+
lines.push(` if (!(${shallowGuard})) ${assertName}(input);`);
|
|
1046
|
+
if (!exported && deepGuard !== null && fastPathNeedsKnownKeys) {
|
|
1047
|
+
const residual = [];
|
|
1048
|
+
for (let i = 0; i < fastPathChecks.length; i++) {
|
|
1049
|
+
const deep = fastPathChecks[i];
|
|
1050
|
+
const shallow = shallowChecks[i];
|
|
1051
|
+
if (deep === shallow)
|
|
1052
|
+
continue;
|
|
1053
|
+
residual.push(shallow !== void 0 && deep.startsWith(`${shallow} && `) ? deep.slice(shallow.length + 4) : deep);
|
|
1054
|
+
}
|
|
1055
|
+
const returnInput = `return input as ${typeName};`;
|
|
1056
|
+
if (fastPathKeyCount === null) {
|
|
1057
|
+
lines.push(residual.length > 0 ? ` if (${residual.join(" && ")}) ${returnInput}` : ` ${returnInput}`);
|
|
1058
|
+
} else {
|
|
1059
|
+
const count = keyCountCheck(unknownKeys, fastPathKeyCount, residual.length === 0 ? " " : " ");
|
|
1060
|
+
if (count.statements.length === 0) {
|
|
1061
|
+
lines.push(` if (${[...residual, count.verdict].join(" && ")}) ${returnInput}`);
|
|
1062
|
+
} else if (residual.length === 0) {
|
|
1063
|
+
lines.push(...count.statements, ` if (${count.verdict}) ${returnInput}`);
|
|
1064
|
+
} else {
|
|
1065
|
+
lines.push(` if (${residual.join(" && ")}) {`);
|
|
1066
|
+
lines.push(...count.statements, ` if (${count.verdict}) ${returnInput}`);
|
|
1067
|
+
lines.push(` }`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const hoisted = [];
|
|
1072
|
+
const objectLines = buildObjectLines(true, hoisted);
|
|
1073
|
+
lines.push(...hoisted);
|
|
1074
|
+
emitReturn(lines, objectLines);
|
|
1075
|
+
} else {
|
|
1076
|
+
emitDeepGuardReturn(lines);
|
|
1077
|
+
if (varsAfterGuard)
|
|
1078
|
+
lines.push(...varDeclLines);
|
|
1079
|
+
lines.push(` ${assertName}(input);`);
|
|
1080
|
+
emitReturn(lines, buildObjectLines(true));
|
|
1081
|
+
}
|
|
1082
|
+
} else if (strict) {
|
|
922
1083
|
lines.push(` if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`);
|
|
923
1084
|
if (!varsAfterGuard)
|
|
924
1085
|
lines.push(...varDeclLines);
|
|
925
1086
|
lines.push(...warnLines);
|
|
926
|
-
const assertionLines = generateObjectStrictAssertion(schema, typeName, {
|
|
927
|
-
useRefImports,
|
|
928
|
-
suffix,
|
|
929
|
-
stripUnknown,
|
|
930
|
-
...rootSchema !== void 0 ? { rootSchema } : {}
|
|
931
|
-
}).slice(1);
|
|
932
1087
|
if (shallowGuard) {
|
|
933
1088
|
if (!exported && deepGuard && fastPathNeedsKnownKeys) {
|
|
934
1089
|
const residual = [];
|
|
@@ -940,34 +1095,41 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
940
1095
|
residual.push(shallow !== void 0 && deep.startsWith(`${shallow} && `) ? deep.slice(shallow.length + 4) : deep);
|
|
941
1096
|
}
|
|
942
1097
|
lines.push(` if (${shallowGuard}) {`);
|
|
943
|
-
|
|
1098
|
+
const returnInput = `return input as ${typeName};`;
|
|
1099
|
+
if (fastPathKeyCount === null) {
|
|
1100
|
+
lines.push(residual.length > 0 ? ` if (${residual.join(" && ")}) ${returnInput}` : ` ${returnInput}`);
|
|
1101
|
+
} else {
|
|
1102
|
+
const count = keyCountCheck(unknownKeys, fastPathKeyCount, residual.length === 0 ? " " : " ");
|
|
1103
|
+
if (count.statements.length === 0) {
|
|
1104
|
+
lines.push(` if (${[...residual, count.verdict].join(" && ")}) ${returnInput}`);
|
|
1105
|
+
} else if (residual.length === 0) {
|
|
1106
|
+
lines.push(...count.statements, ` if (${count.verdict}) ${returnInput}`);
|
|
1107
|
+
} else {
|
|
1108
|
+
lines.push(` if (${residual.join(" && ")}) {`);
|
|
1109
|
+
lines.push(...count.statements, ` if (${count.verdict}) ${returnInput}`);
|
|
1110
|
+
lines.push(` }`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
944
1113
|
lines.push(` } else {`);
|
|
945
|
-
for (const assertionLine of assertionLines)
|
|
1114
|
+
for (const assertionLine of assertionLines.slice(1))
|
|
946
1115
|
lines.push(` ${assertionLine}`);
|
|
947
|
-
}
|
|
948
1116
|
lines.push(` }`);
|
|
949
1117
|
} else {
|
|
950
1118
|
lines.push(` if (!(${shallowGuard})) {`);
|
|
951
|
-
for (const assertionLine of assertionLines)
|
|
1119
|
+
for (const assertionLine of assertionLines.slice(1))
|
|
952
1120
|
lines.push(` ${assertionLine}`);
|
|
953
|
-
}
|
|
954
1121
|
lines.push(` }`);
|
|
955
1122
|
}
|
|
956
1123
|
emitReturn(lines, buildObjectLines(true));
|
|
957
1124
|
} else {
|
|
958
|
-
if (deepGuard) {
|
|
1125
|
+
if (deepGuard !== null) {
|
|
959
1126
|
emitDeepGuardReturn(lines);
|
|
960
1127
|
}
|
|
961
1128
|
if (varsAfterGuard)
|
|
962
1129
|
lines.push(...varDeclLines);
|
|
963
|
-
for (const assertionLine of assertionLines)
|
|
1130
|
+
for (const assertionLine of assertionLines.slice(1))
|
|
964
1131
|
lines.push(assertionLine);
|
|
965
|
-
|
|
966
|
-
if (rejectsUnknownKeys) {
|
|
967
|
-
lines.push(` for (const _k in input) {`);
|
|
968
|
-
lines.push(` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
|
|
969
|
-
lines.push(` }`);
|
|
970
|
-
}
|
|
1132
|
+
lines.push(...unknownKeyThrowLines());
|
|
971
1133
|
emitReturn(lines, buildObjectLines(true));
|
|
972
1134
|
}
|
|
973
1135
|
} else {
|
|
@@ -975,7 +1137,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
975
1137
|
if (!varsAfterGuard)
|
|
976
1138
|
lines.push(...varDeclLines);
|
|
977
1139
|
lines.push(...warnLines);
|
|
978
|
-
if (deepGuard) {
|
|
1140
|
+
if (deepGuard !== null) {
|
|
979
1141
|
emitDeepGuardReturn(lines);
|
|
980
1142
|
}
|
|
981
1143
|
if (varsAfterGuard)
|
|
@@ -1034,6 +1196,7 @@ ${loopLines.join("\n")}
|
|
|
1034
1196
|
};
|
|
1035
1197
|
const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false, caseInsensitive = false, context = {}) => {
|
|
1036
1198
|
const functionName = generateParserName(typeName);
|
|
1199
|
+
const unknownKeys = context.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
|
|
1037
1200
|
const entries = generatePropertyEntries(schema, useRefImports, suffix, caseInsensitive);
|
|
1038
1201
|
const propertyLines = entries.map((entry) => {
|
|
1039
1202
|
if (entry.isOptional) {
|
|
@@ -1042,7 +1205,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
|
|
|
1042
1205
|
return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
|
|
1043
1206
|
});
|
|
1044
1207
|
if (!isSchemaObject(schema) || !("patternProperties" in schema)) {
|
|
1045
|
-
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
|
|
1208
|
+
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
|
|
1046
1209
|
}
|
|
1047
1210
|
const patternProps = schema.patternProperties;
|
|
1048
1211
|
const patterns = Object.entries(patternProps);
|
|
@@ -1055,7 +1218,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
|
|
|
1055
1218
|
return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, false, context);
|
|
1056
1219
|
}
|
|
1057
1220
|
if (!refPattern || !useRefImports) {
|
|
1058
|
-
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
|
|
1221
|
+
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
|
|
1059
1222
|
}
|
|
1060
1223
|
const [pattern, patternSchema] = refPattern;
|
|
1061
1224
|
const ref = patternSchema.$ref;
|
|
@@ -1231,7 +1394,7 @@ ${notObjectBranch}
|
|
|
1231
1394
|
return result;
|
|
1232
1395
|
};`;
|
|
1233
1396
|
};
|
|
1234
|
-
const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false, caseInsensitive = false) => {
|
|
1397
|
+
const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false, caseInsensitive = false, unknownKeys = DEFAULT_UNKNOWN_KEYS) => {
|
|
1235
1398
|
const functionName = generateParserName(typeName);
|
|
1236
1399
|
const ifSchema = schema.if;
|
|
1237
1400
|
const thenSchema = schema.then;
|
|
@@ -1241,7 +1404,7 @@ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = fals
|
|
|
1241
1404
|
if (!thenHasRef || !elseHasRef) {
|
|
1242
1405
|
const mergedSchema = getConditionalObjectSchema(schema);
|
|
1243
1406
|
if (mergedSchema) {
|
|
1244
|
-
return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
|
|
1407
|
+
return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
|
|
1245
1408
|
}
|
|
1246
1409
|
return generateEmptyObjectParser(typeName);
|
|
1247
1410
|
}
|
|
@@ -1249,7 +1412,7 @@ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = fals
|
|
|
1249
1412
|
if (!isRefCondition) {
|
|
1250
1413
|
const mergedSchema = getConditionalObjectSchema(schema);
|
|
1251
1414
|
if (mergedSchema) {
|
|
1252
|
-
return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
|
|
1415
|
+
return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
|
|
1253
1416
|
}
|
|
1254
1417
|
return generateEmptyObjectParser(typeName);
|
|
1255
1418
|
}
|
|
@@ -1333,11 +1496,13 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1333
1496
|
const strict = options?.strict ?? false;
|
|
1334
1497
|
const stripUnknown = options?.stripUnknown ?? false;
|
|
1335
1498
|
const caseInsensitive = options?.caseInsensitive ?? false;
|
|
1499
|
+
const unknownKeys = options?.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
|
|
1336
1500
|
const suffix = options?.typeSuffix ?? "";
|
|
1337
1501
|
const context = {
|
|
1338
1502
|
useRefImports,
|
|
1339
1503
|
suffix,
|
|
1340
1504
|
stripUnknown,
|
|
1505
|
+
unknownKeys,
|
|
1341
1506
|
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {}
|
|
1342
1507
|
};
|
|
1343
1508
|
const copyLoopContext = { ...context, subParsers: false };
|
|
@@ -1352,6 +1517,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1352
1517
|
stripUnknown,
|
|
1353
1518
|
caseInsensitive,
|
|
1354
1519
|
logWarnings,
|
|
1520
|
+
unknownKeys,
|
|
1355
1521
|
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
|
|
1356
1522
|
...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
|
|
1357
1523
|
});
|
|
@@ -1366,12 +1532,13 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1366
1532
|
stripUnknown,
|
|
1367
1533
|
caseInsensitive,
|
|
1368
1534
|
logWarnings,
|
|
1535
|
+
unknownKeys,
|
|
1369
1536
|
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
|
|
1370
1537
|
...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
|
|
1371
1538
|
});
|
|
1372
1539
|
}
|
|
1373
1540
|
if (hasProperties(schema)) {
|
|
1374
|
-
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive, options?.shapeValidatorSource);
|
|
1541
|
+
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive, options?.shapeValidatorSource, unknownKeys);
|
|
1375
1542
|
}
|
|
1376
1543
|
if (strict && isSchemaObject(schema) && ("if" in schema || "then" in schema || "else" in schema)) {
|
|
1377
1544
|
return generateNonObjectParser(typeName, schema, strict, {
|
|
@@ -1380,16 +1547,17 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1380
1547
|
stripUnknown,
|
|
1381
1548
|
caseInsensitive,
|
|
1382
1549
|
logWarnings,
|
|
1550
|
+
unknownKeys,
|
|
1383
1551
|
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
|
|
1384
1552
|
...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
|
|
1385
1553
|
});
|
|
1386
1554
|
}
|
|
1387
1555
|
if (isSchemaObject(schema) && "if" in schema && "then" in schema && "else" in schema) {
|
|
1388
|
-
return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive);
|
|
1556
|
+
return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive, unknownKeys);
|
|
1389
1557
|
}
|
|
1390
1558
|
const conditionalObjectSchema = getConditionalObjectSchema(schema);
|
|
1391
1559
|
if (conditionalObjectSchema) {
|
|
1392
|
-
return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
|
|
1560
|
+
return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
|
|
1393
1561
|
}
|
|
1394
1562
|
if (!isObjectLikeSchema) {
|
|
1395
1563
|
return generateNonObjectParser(typeName, schema, strict, {
|
|
@@ -1398,6 +1566,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1398
1566
|
stripUnknown,
|
|
1399
1567
|
caseInsensitive,
|
|
1400
1568
|
logWarnings,
|
|
1569
|
+
unknownKeys,
|
|
1401
1570
|
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
|
|
1402
1571
|
...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
|
|
1403
1572
|
});
|
|
@@ -1430,7 +1599,7 @@ const generateParserFunction = (schema, typeName, rawOptions) => {
|
|
|
1430
1599
|
return selectParserStrategy(schema, typeName, options);
|
|
1431
1600
|
};
|
|
1432
1601
|
const shapeValidatorFunctionName = (typeName) => shapeValidatorName(typeName);
|
|
1433
|
-
const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", exported = true, stripUnknown = false, reservedNames = NO_RESERVED_NAMES) => {
|
|
1602
|
+
const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", exported = true, stripUnknown = false, reservedNames = NO_RESERVED_NAMES, unknownKeys = DEFAULT_UNKNOWN_KEYS) => {
|
|
1434
1603
|
const fnName = shapeValidatorName(typeName);
|
|
1435
1604
|
const exportPrefix = exported ? "export " : "";
|
|
1436
1605
|
const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
|
|
@@ -1455,25 +1624,34 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
|
|
|
1455
1624
|
}
|
|
1456
1625
|
return stub;
|
|
1457
1626
|
}
|
|
1458
|
-
|
|
1627
|
+
const shape = shapeChecksOf(schema, typeName, "input", useRefImports, suffix, stripUnknown, reservedNames);
|
|
1628
|
+
if (shape === null)
|
|
1459
1629
|
return stub;
|
|
1630
|
+
const strictKeysGuard = shape.walksKeys ? `
|
|
1631
|
+
if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
|
|
1632
|
+
return renderShapeValidator(exportPrefix, fnName, shape.checks, shape.keyCount, strictKeysGuard, unknownKeys);
|
|
1633
|
+
};
|
|
1634
|
+
const shapeChecksOf = (schema, typeName, root, useRefImports, suffix, stripUnknown, reservedNames) => {
|
|
1635
|
+
if (!isSchemaObject(schema))
|
|
1636
|
+
return null;
|
|
1637
|
+
if (hasFastPathBlockingKeyword(schema))
|
|
1638
|
+
return null;
|
|
1639
|
+
if (!hasProperties(schema))
|
|
1640
|
+
return null;
|
|
1460
1641
|
if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema || "patternProperties" in schema || "if" in schema || "then" in schema || "else" in schema) {
|
|
1461
|
-
return
|
|
1642
|
+
return null;
|
|
1462
1643
|
}
|
|
1463
1644
|
const schemaProps = schema.properties;
|
|
1464
1645
|
const propertyKeys = Object.keys(schemaProps);
|
|
1465
1646
|
const { objects: subTypeNames, arrayItems: subItemNames } = collectInlineSubTypes(schema, typeName, reservedNames);
|
|
1466
1647
|
const validatorStripKeys = hasStrictKeys(schema) || stripUnknown;
|
|
1467
|
-
const
|
|
1468
|
-
const strictKeysGuard = validatorStripKeys && validatorKeyCount === null ? `
|
|
1469
|
-
if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
|
|
1648
|
+
const keyCount = validatorStripKeys ? exactKeyCountOf(schema) : null;
|
|
1470
1649
|
const checks = [];
|
|
1471
1650
|
for (const key of propertyKeys) {
|
|
1472
1651
|
const propSchema = schemaProps[key];
|
|
1473
|
-
if (shouldUseRecordRefImport(propSchema, useRefImports))
|
|
1474
|
-
return
|
|
1475
|
-
|
|
1476
|
-
const accessor = safeAccessor("input", key);
|
|
1652
|
+
if (shouldUseRecordRefImport(propSchema, useRefImports))
|
|
1653
|
+
return null;
|
|
1654
|
+
const accessor = safeAccessor(root, key);
|
|
1477
1655
|
const isRequired = isPropertyRequired(key, schema);
|
|
1478
1656
|
const subName = subTypeNames.get(key);
|
|
1479
1657
|
let check = subName ? `${shapeValidatorName(subName)}(${accessor})` : generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
|
|
@@ -1481,9 +1659,8 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
|
|
|
1481
1659
|
if (check !== null && itemSubName) {
|
|
1482
1660
|
check = `${check} && _every${itemSubName}(${accessor})`;
|
|
1483
1661
|
}
|
|
1484
|
-
if (check === null)
|
|
1485
|
-
return
|
|
1486
|
-
}
|
|
1662
|
+
if (check === null)
|
|
1663
|
+
return null;
|
|
1487
1664
|
if (isRequired) {
|
|
1488
1665
|
checks.push(check);
|
|
1489
1666
|
} else {
|
|
@@ -1491,28 +1668,56 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
|
|
|
1491
1668
|
}
|
|
1492
1669
|
}
|
|
1493
1670
|
if (hasMinProperties(schema))
|
|
1494
|
-
checks.push(`Object.keys(
|
|
1671
|
+
checks.push(`Object.keys(${root}).length >= ${schema.minProperties}`);
|
|
1495
1672
|
if (hasMaxProperties(schema))
|
|
1496
|
-
checks.push(`Object.keys(
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1673
|
+
checks.push(`Object.keys(${root}).length <= ${schema.maxProperties}`);
|
|
1674
|
+
return { checks, keyCount, walksKeys: validatorStripKeys && keyCount === null, propertyCount: propertyKeys.length };
|
|
1675
|
+
};
|
|
1676
|
+
const inlineNestedShapeCheck = (childSchema, childTypeName, local, useRefImports, suffix, stripUnknown, reservedNames, unknownKeys) => {
|
|
1677
|
+
const readRoot = `(${local} as Record<string, any>)`;
|
|
1678
|
+
const shape = shapeChecksOf(childSchema, childTypeName, readRoot, useRefImports, suffix, stripUnknown, reservedNames);
|
|
1679
|
+
if (shape === null || shape.walksKeys)
|
|
1680
|
+
return null;
|
|
1681
|
+
if (shape.propertyCount === 0 || shape.propertyCount > INLINE_NESTED_MAX_PROPERTIES)
|
|
1682
|
+
return null;
|
|
1683
|
+
const count = shape.keyCount === null ? null : keyCountCheck(unknownKeys, shape.keyCount, "", local);
|
|
1684
|
+
if (count !== null && count.statements.length > 0)
|
|
1685
|
+
return null;
|
|
1686
|
+
const parts = [
|
|
1687
|
+
`typeof ${local} === "object"`,
|
|
1688
|
+
`${local} !== null`,
|
|
1689
|
+
`!Array.isArray(${local})`,
|
|
1690
|
+
...shape.checks,
|
|
1691
|
+
...count === null ? [] : [count.verdict]
|
|
1692
|
+
];
|
|
1693
|
+
return { check: `(${parts.join(" && ")})`, propertyCount: shape.propertyCount };
|
|
1694
|
+
};
|
|
1695
|
+
const renderShapeValidator = (exportPrefix, fnName, checks, keyCount, strictKeysGuard, unknownKeys) => {
|
|
1696
|
+
const head = `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
|
|
1697
|
+
if (!isObject(input)) return false;${strictKeysGuard}`;
|
|
1698
|
+
const chain = checks.join("\n && ");
|
|
1699
|
+
if (keyCount === null) {
|
|
1700
|
+
if (checks.length === 0) {
|
|
1701
|
+
return strictKeysGuard ? `${head}
|
|
1504
1702
|
return true;
|
|
1505
|
-
};`;
|
|
1703
|
+
};` : `${exportPrefix}const ${fnName} = (input: unknown): boolean => isObject(input);`;
|
|
1506
1704
|
}
|
|
1507
|
-
return `${
|
|
1705
|
+
return `${head}
|
|
1706
|
+
return ${chain};
|
|
1707
|
+
};`;
|
|
1508
1708
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1709
|
+
const count = keyCountCheck(unknownKeys, keyCount, " ");
|
|
1710
|
+
if (count.statements.length === 0) {
|
|
1711
|
+
return `${head}
|
|
1712
|
+
return ${checks.length === 0 ? count.verdict : `${chain}
|
|
1713
|
+
&& ${count.verdict}`};
|
|
1714
|
+
};`;
|
|
1512
1715
|
}
|
|
1513
|
-
|
|
1514
|
-
if (!
|
|
1515
|
-
return ${
|
|
1716
|
+
const gate = checks.length === 0 ? "" : `
|
|
1717
|
+
if (!(${chain})) return false;`;
|
|
1718
|
+
return `${head}${gate}
|
|
1719
|
+
${count.statements.join("\n")}
|
|
1720
|
+
return ${count.verdict};
|
|
1516
1721
|
};`;
|
|
1517
1722
|
};
|
|
1518
1723
|
export {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
|
|
1
2
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
3
|
/**
|
|
3
4
|
* Context for assertions that reach beyond the property's own schema: union
|
|
@@ -13,6 +14,11 @@ export type StrictAssertionContext = {
|
|
|
13
14
|
readonly suffix?: string;
|
|
14
15
|
readonly rootSchema?: Record<string, unknown>;
|
|
15
16
|
readonly stripUnknown?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The parser's `unknownKeys` option, carried so every object parser one of
|
|
19
|
+
* these contexts reaches counts keys on its fast path the same way.
|
|
20
|
+
*/
|
|
21
|
+
readonly unknownKeys?: UnknownKeysStrategy;
|
|
16
22
|
/**
|
|
17
23
|
* Whether the parser these assertions run inside also emits the private
|
|
18
24
|
* sub-parsers for its inline object properties (and inline-object array
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-parsers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -41,7 +41,11 @@
|
|
|
41
41
|
"types:check": "tsgo -p . --noEmit",
|
|
42
42
|
"test": "NODE_ENV=production vitest run --root ../.. generate-parsers",
|
|
43
43
|
"generate-readme": "bun run ./scripts/generate-readme.ts",
|
|
44
|
-
"
|
|
44
|
+
"prebench": "bun run --filter='@amritk/helpers' build && bun run build",
|
|
45
|
+
"bench": "bun --conditions development ./bench/run.ts",
|
|
46
|
+
"prebench:node": "bun run --filter='@amritk/helpers' build && bun run build",
|
|
47
|
+
"bench:node": "node ./bench/run.ts",
|
|
48
|
+
"bench:moltar": "bun --conditions development ./bench/moltar.ts"
|
|
45
49
|
},
|
|
46
50
|
"imports": {
|
|
47
51
|
"#generators/*": "./src/generators/*.ts",
|
|
@@ -56,12 +60,13 @@
|
|
|
56
60
|
},
|
|
57
61
|
"dependencies": {
|
|
58
62
|
"json-schema-typed": "^8.0.1",
|
|
59
|
-
"@amritk/helpers": "^0.
|
|
63
|
+
"@amritk/helpers": "^0.19.0"
|
|
60
64
|
},
|
|
61
65
|
"devDependencies": {
|
|
62
|
-
"@amritk/generate-markdown": "^0.
|
|
66
|
+
"@amritk/generate-markdown": "^0.6.0",
|
|
63
67
|
"@sinclair/typebox": "^0.34.49",
|
|
64
68
|
"ajv": "^8.17.1",
|
|
69
|
+
"benny": "^3.7.1",
|
|
65
70
|
"zod": "^4.4.3"
|
|
66
71
|
}
|
|
67
72
|
}
|