@cyanheads/mcp-ts-core 0.13.12 → 0.13.14
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/AGENTS.md +9 -8
- package/CLAUDE.md +9 -8
- package/README.md +1 -1
- package/changelog/0.13.x/0.13.13.md +93 -0
- package/changelog/0.13.x/0.13.14.md +68 -0
- package/dist/core/app.d.ts +4 -3
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +12 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +59 -14
- package/dist/core/context.js.map +1 -1
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +21 -8
- package/dist/core/worker.js.map +1 -1
- package/dist/mcp-server/inputRequired.d.ts +18 -9
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +29 -15
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +22 -12
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/prompts/utils/promptDefinition.d.ts +4 -1
- package/dist/mcp-server/prompts/utils/promptDefinition.d.ts.map +1 -1
- package/dist/mcp-server/prompts/utils/promptDefinition.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +22 -9
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +309 -26
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +870 -106
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
- package/dist/mcp-server/tools/utils/schemaShape.d.ts +127 -5
- package/dist/mcp-server/tools/utils/schemaShape.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/schemaShape.js +432 -4
- package/dist/mcp-server/tools/utils/schemaShape.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolDefinition.d.ts +7 -3
- package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +53 -23
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +811 -255
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.d.ts +3 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js +4 -2
- package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +48 -21
- package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/types-global/errors.js +31 -16
- package/dist/types-global/errors.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +29 -12
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +198 -100
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/helpers.d.ts +75 -4
- package/dist/utils/internal/error-handler/helpers.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/helpers.js +232 -33
- package/dist/utils/internal/error-handler/helpers.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +17 -7
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/logValue.d.ts +33 -0
- package/dist/utils/internal/logValue.d.ts.map +1 -0
- package/dist/utils/internal/logValue.js +539 -0
- package/dist/utils/internal/logValue.js.map +1 -0
- package/dist/utils/internal/logger.d.ts +38 -9
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +228 -118
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +9 -11
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/internal/requestContext.d.ts +3 -3
- package/dist/utils/internal/requestContext.js +1 -1
- package/dist/utils/network/fetchWithTimeout.d.ts +18 -10
- package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
- package/dist/utils/network/fetchWithTimeout.js +48 -20
- package/dist/utils/network/fetchWithTimeout.js.map +1 -1
- package/dist/utils/network/httpError.d.ts +8 -6
- package/dist/utils/network/httpError.d.ts.map +1 -1
- package/dist/utils/network/httpError.js +23 -7
- package/dist/utils/network/httpError.js.map +1 -1
- package/dist/utils/network/retry.d.ts +7 -4
- package/dist/utils/network/retry.d.ts.map +1 -1
- package/dist/utils/network/retry.js +24 -10
- package/dist/utils/network/retry.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +24 -28
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +25 -84
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/security/sensitiveFields.d.ts +32 -4
- package/dist/utils/security/sensitiveFields.d.ts.map +1 -1
- package/dist/utils/security/sensitiveFields.js +85 -4
- package/dist/utils/security/sensitiveFields.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +3 -2
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +3 -2
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/dist/utils/telemetry/trace.d.ts +3 -1
- package/dist/utils/telemetry/trace.d.ts.map +1 -1
- package/dist/utils/telemetry/trace.js +6 -6
- package/dist/utils/telemetry/trace.js.map +1 -1
- package/framework-skills/add-tool/SKILL.md +15 -7
- package/framework-skills/api-config/SKILL.md +2 -1
- package/framework-skills/api-context/SKILL.md +3 -3
- package/framework-skills/api-errors/SKILL.md +23 -21
- package/framework-skills/api-linter/SKILL.md +11 -10
- package/framework-skills/api-telemetry/SKILL.md +9 -7
- package/framework-skills/api-testing/SKILL.md +2 -2
- package/framework-skills/api-utils/SKILL.md +6 -6
- package/framework-skills/api-utils/references/security.md +4 -2
- package/framework-skills/design-mcp-server/SKILL.md +3 -2
- package/framework-skills/field-test/SKILL.md +3 -2
- package/package.json +8 -7
- package/scripts/check-framework-antipatterns.ts +3 -2
- package/templates/.env.example +2 -0
- package/templates/tests/tools/echo.tool.test.ts +19 -1
|
@@ -14,9 +14,17 @@
|
|
|
14
14
|
* in another case style (`max_results` for `maxResults`). Rewritten to the
|
|
15
15
|
* canonical key before parsing. A one-to-one mapping fixed ahead of time, not
|
|
16
16
|
* the nearest-key guess #232 rejected.
|
|
17
|
-
* 3. **Representation repair (#234, #479, #487
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* 3. **Representation repair (#234, #479, #487, #707, #602, #616, #570, #599,
|
|
18
|
+
* #714)** — a JSON-stringified array or object where one was declared, an
|
|
19
|
+
* integer where a string was, a string spelling a number or boolean where
|
|
20
|
+
* only those are accepted, a lone string where an array was, or `null` for
|
|
21
|
+
* an optional field, which is deleted — at any path the rejection renders,
|
|
22
|
+
* inside the one union branch that survives selection included, at a
|
|
23
|
+
* discriminator or literal tag no variant accepts, and inside what a
|
|
24
|
+
* `z.preprocess()` made of the argument — tried first written in place at
|
|
25
|
+
* the issue path, then written into the output, which takes the argument's
|
|
26
|
+
* place only if the preprocess returns it unchanged. Applied only
|
|
27
|
+
* *after* the parse has already failed, and kept only when it flips the
|
|
20
28
|
* arguments from invalid to valid: the author's schema is the sole arbiter,
|
|
21
29
|
* so a repair can never touch input that was already valid.
|
|
22
30
|
*
|
|
@@ -39,9 +47,18 @@
|
|
|
39
47
|
* debug log and one counter increment instead — for the attempt whose
|
|
40
48
|
* arguments the handler receives, never one the parse discarded. A call they
|
|
41
49
|
* cannot rescue is rejected with the rewrites and underscore-rule drops of the
|
|
42
|
-
* attempt whose rejection it carries reported (#468), since without them the
|
|
43
|
-
* tell a bad value from a key that was moved or discarded
|
|
44
|
-
*
|
|
50
|
+
* attempt whose rejection it carries reported (#468), since without them the
|
|
51
|
+
* caller cannot tell a bad value from a key that was moved or discarded, and
|
|
52
|
+
* with each alias sent beside its target named as one in the hint (#639),
|
|
53
|
+
* since an unknown or dropped key would send the caller looking for a typo. Its
|
|
54
|
+
* issues come from a parse with only the repairs that held applied (#706), so
|
|
55
|
+
* a value the schema accepted once repaired is not reported and one it refused
|
|
56
|
+
* is reported as sent — unless that parse validates, which only a union or a
|
|
57
|
+
* cross-field refinement allows, and the first parse is reported instead; the
|
|
58
|
+
* rejection itself never mentions a repair. A value that held only written in
|
|
59
|
+
* place below a transform that reorders or rewrites it is reported as sent
|
|
60
|
+
* too: the issues name positions in the transform's output, where a repair
|
|
61
|
+
* written in place is not.
|
|
45
62
|
*
|
|
46
63
|
* The split between log and counter is deliberate. Counter attributes are
|
|
47
64
|
* bounded and author- or framework-defined — the ignore-list entry that
|
|
@@ -50,8 +67,8 @@
|
|
|
50
67
|
* a client invents (#114). The raw key and alias go to the debug log, which is
|
|
51
68
|
* where an operator looks when a counter shows a new client artifact and where
|
|
52
69
|
* cardinality costs nothing, at most their first 1,024 characters bounding its size
|
|
53
|
-
* (#631) — and, on a rejection, back to the caller who sent them,
|
|
54
|
-
* an error payload is per call rather than a permanent series.
|
|
70
|
+
* (#631) — and, on a rejection, back to the caller who sent them, cut the same
|
|
71
|
+
* way (#648), since an error payload is per call rather than a permanent series.
|
|
55
72
|
*
|
|
56
73
|
* @module src/mcp-server/tools/utils/inputPrevalidation
|
|
57
74
|
*/
|
|
@@ -61,7 +78,7 @@ import { requestContextService, withExtra, } from '../../../utils/internal/reque
|
|
|
61
78
|
import { ATTR_MCP_INPUT_ALIAS_KIND, ATTR_MCP_INPUT_COERCION, ATTR_MCP_INPUT_IGNORE_RULE, ATTR_MCP_INPUT_TARGET, ATTR_MCP_TOOL_NAME, } from '../../../utils/telemetry/attributes.js';
|
|
62
79
|
import { createCounter } from '../../../utils/telemetry/metrics.js';
|
|
63
80
|
import { scanHeaderDesignations } from './headerParam.js';
|
|
64
|
-
import { inputVariants, isDiscriminatedUnionSchema, zodDef } from './schemaShape.js';
|
|
81
|
+
import { argumentAt, inputVariants, isDiscriminatedUnionSchema, objectSchemaAt, routesThroughPipeAt, stepInto, zodDef, } from './schemaShape.js';
|
|
65
82
|
/**
|
|
66
83
|
* Root keys known to be added by a client rather than written by the model, so
|
|
67
84
|
* rejecting them fails a call whose arguments were all correct. `_meta` belongs
|
|
@@ -77,6 +94,10 @@ const COERCION_KINDS = {
|
|
|
77
94
|
stringified_array: 'a stringified array',
|
|
78
95
|
stringified_object: 'a stringified object',
|
|
79
96
|
integer_as_string: 'an integer sent for a string',
|
|
97
|
+
string_as_number: 'a string sent for a number',
|
|
98
|
+
string_as_boolean: 'a string sent for a boolean',
|
|
99
|
+
string_as_array: 'a string sent for an array',
|
|
100
|
+
null_as_absent: 'null sent for an optional field',
|
|
80
101
|
};
|
|
81
102
|
/**
|
|
82
103
|
* What the ignored-key counter reports for a key the underscore heuristic
|
|
@@ -122,8 +143,9 @@ function countAliased(toolName, target, kind) {
|
|
|
122
143
|
* Emits one counter increment and one debug log per change an attempt made, in
|
|
123
144
|
* the order its stages made them. {@link parseToolArguments} calls it once per
|
|
124
145
|
* call, for the attempt whose arguments the handler receives — or, on a
|
|
125
|
-
* rejection, the
|
|
126
|
-
* alias-first retry rewrote never also
|
|
146
|
+
* rejection, the attempt the rejection reports, which is the alias-first
|
|
147
|
+
* retry's when it ran — so a key the alias-first retry rewrote never also
|
|
148
|
+
* counts as dropped (#563).
|
|
127
149
|
*
|
|
128
150
|
* The key a record names is the caller's, of whatever length the caller wrote,
|
|
129
151
|
* so the message and the field carry at most its first 1,024 characters, with
|
|
@@ -298,13 +320,14 @@ function ignoreListFor(options) {
|
|
|
298
320
|
* declared `inputAliases`, then — unless `caseStyleAliases: false` — any
|
|
299
321
|
* undeclared key whose case-folded form names exactly one declared key.
|
|
300
322
|
*
|
|
301
|
-
* A rewrite applies only when the target key is absent
|
|
302
|
-
* arguments pass through and the strict rejection fires
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
323
|
+
* A rewrite applies only when the target key is absent. With both present the
|
|
324
|
+
* arguments pass through and the strict rejection fires, and the alias is
|
|
325
|
+
* recorded as declined so the rejection can name it as an alias rather than an
|
|
326
|
+
* unknown key (#639). An author-opened root is never rewritten (an unknown key
|
|
327
|
+
* there is already accepted verbatim), and a `headerParam`-designated target
|
|
328
|
+
* is never rewritten *to*: the SDK cross-checks the `Mcp-Param-<Name>` header
|
|
329
|
+
* against the raw body before dispatch, so a later rewrite would hand the
|
|
330
|
+
* handler a value no intermediary attested.
|
|
308
331
|
*
|
|
309
332
|
* `unfolded` names keys the case-style pass never folds. The alias-first retry
|
|
310
333
|
* passes the ignore list, since there the drop has not yet removed a client
|
|
@@ -313,25 +336,32 @@ function ignoreListFor(options) {
|
|
|
313
336
|
* declaring the key itself is (#563).
|
|
314
337
|
*
|
|
315
338
|
* Returns the rewrites in the order they were made, and again in argument order
|
|
316
|
-
* for the rejection report.
|
|
339
|
+
* for the rejection report, with each declined alias mapped to its target.
|
|
340
|
+
* `heldTarget` reads the same rules for a key that never reached this stage —
|
|
341
|
+
* an underscore-rule drop of the drop-first order — and names the target the
|
|
342
|
+
* key stands for when the arguments already hold it: such a key is an alias
|
|
343
|
+
* sent beside its target, which the alias-first order would decline. It runs
|
|
344
|
+
* only when a rejection asks ({@link collisionsIn}), since folding every key a
|
|
345
|
+
* call carries that the drop discarded costs each one's length.
|
|
317
346
|
*/
|
|
318
347
|
function applyAliases(def, args, plan, options, unfolded) {
|
|
319
348
|
const changes = [];
|
|
320
349
|
const declared = def.inputAliases;
|
|
321
350
|
const caseStyle = options?.caseStyleAliases !== false;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return { args, aliased: [], changes };
|
|
351
|
+
const variant = plan.anyOpen || (!declared && !caseStyle) ? undefined : selectVariant(plan, args);
|
|
352
|
+
if (!variant) {
|
|
353
|
+
return { args, aliased: [], changes, declined: NO_DECLINES, heldTarget: NO_TARGET };
|
|
354
|
+
}
|
|
327
355
|
let rewritten;
|
|
328
356
|
const current = () => rewritten ?? args;
|
|
329
357
|
const moved = new Map();
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
!
|
|
358
|
+
let declined;
|
|
359
|
+
const reaches = (alias, target) => !variant.keys.has(alias) && variant.keys.has(target) && !plan.headerTargets.has(target);
|
|
360
|
+
const decline = (alias, target) => {
|
|
361
|
+
declined ??= new Map();
|
|
362
|
+
if (!declined.has(alias))
|
|
363
|
+
declined.set(alias, target);
|
|
364
|
+
};
|
|
335
365
|
const move = (alias, target, kind) => {
|
|
336
366
|
// `target` is a declared key read off the Zod shape, never caller text — an
|
|
337
367
|
// object literal resolves `__proto__` to the prototype setter rather than a
|
|
@@ -343,12 +373,19 @@ function applyAliases(def, args, plan, options, unfolded) {
|
|
|
343
373
|
rewritten[target] = rewritten[alias];
|
|
344
374
|
delete rewritten[alias];
|
|
345
375
|
moved.set(alias, target);
|
|
376
|
+
declined?.delete(alias);
|
|
346
377
|
changes.push({ kind: 'aliased', alias, target, aliasKind: kind });
|
|
347
378
|
};
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
379
|
+
const resolve = (alias, target, kind) => {
|
|
380
|
+
if (!Object.hasOwn(current(), alias) || !reaches(alias, target))
|
|
381
|
+
return;
|
|
382
|
+
if (Object.hasOwn(current(), target))
|
|
383
|
+
decline(alias, target);
|
|
384
|
+
else
|
|
385
|
+
move(alias, target, kind);
|
|
386
|
+
};
|
|
387
|
+
for (const [alias, target] of Object.entries(declared ?? {}))
|
|
388
|
+
resolve(alias, target, 'declared');
|
|
352
389
|
if (caseStyle) {
|
|
353
390
|
// Snapshot first: `move` only ever removes an undeclared key and adds a
|
|
354
391
|
// declared one, so the remaining candidates are unaffected.
|
|
@@ -356,15 +393,66 @@ function applyAliases(def, args, plan, options, unfolded) {
|
|
|
356
393
|
if (variant.keys.has(key) || unfolded.has(key))
|
|
357
394
|
continue;
|
|
358
395
|
const target = variant.folded.get(foldArgumentKey(key));
|
|
359
|
-
if (target
|
|
360
|
-
|
|
396
|
+
if (target)
|
|
397
|
+
resolve(key, target, 'case_style');
|
|
361
398
|
}
|
|
362
399
|
}
|
|
400
|
+
const heldTarget = (key) => {
|
|
401
|
+
// The alias-first order tries a declared alias first, then the case fold.
|
|
402
|
+
const named = declared && Object.hasOwn(declared, key) ? declared[key] : undefined;
|
|
403
|
+
const target = named !== undefined && reaches(key, named)
|
|
404
|
+
? named
|
|
405
|
+
: (caseStyle && variant.folded.get(foldArgumentKey(key))) || undefined;
|
|
406
|
+
return target !== undefined && reaches(key, target) && Object.hasOwn(current(), target)
|
|
407
|
+
? target
|
|
408
|
+
: undefined;
|
|
409
|
+
};
|
|
363
410
|
const aliased = Object.keys(args).flatMap((alias) => {
|
|
364
411
|
const target = moved.get(alias);
|
|
365
412
|
return target === undefined ? [] : [{ alias, target }];
|
|
366
413
|
});
|
|
367
|
-
return {
|
|
414
|
+
return {
|
|
415
|
+
args: rewritten ?? args,
|
|
416
|
+
aliased,
|
|
417
|
+
changes,
|
|
418
|
+
declined: declined ?? NO_DECLINES,
|
|
419
|
+
heldTarget,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/** No alias declined. */
|
|
423
|
+
const NO_DECLINES = new Map();
|
|
424
|
+
/** No key stands for a target. */
|
|
425
|
+
const NO_TARGET = () => undefined;
|
|
426
|
+
/**
|
|
427
|
+
* Groups an attempt's declined aliases by target, each with every key the
|
|
428
|
+
* caller sent for that target in argument order — `undefined` when none was
|
|
429
|
+
* declined. `args` is the caller's own object, so a key the drop discarded
|
|
430
|
+
* keeps its place. `dropped` are the underscore-rule drops that ran before the
|
|
431
|
+
* alias stage, each declined when it stands for a target the arguments hold.
|
|
432
|
+
*/
|
|
433
|
+
function collisionsIn(args, rewrite, dropped) {
|
|
434
|
+
const { aliased, heldTarget } = rewrite;
|
|
435
|
+
const declined = new Map(rewrite.declined);
|
|
436
|
+
for (const key of dropped) {
|
|
437
|
+
const target = heldTarget(key);
|
|
438
|
+
if (target !== undefined)
|
|
439
|
+
declined.set(key, target);
|
|
440
|
+
}
|
|
441
|
+
if (declined.size === 0)
|
|
442
|
+
return;
|
|
443
|
+
const moved = new Map(aliased.map(({ alias, target }) => [alias, target]));
|
|
444
|
+
const byTarget = new Map();
|
|
445
|
+
for (const target of declined.values())
|
|
446
|
+
byTarget.set(target, { declined: [], keys: [] });
|
|
447
|
+
for (const key of Object.keys(args)) {
|
|
448
|
+
const collision = byTarget.get(declined.get(key) ?? moved.get(key) ?? key);
|
|
449
|
+
if (!collision)
|
|
450
|
+
continue;
|
|
451
|
+
collision.keys.push(key);
|
|
452
|
+
if (declined.has(key))
|
|
453
|
+
collision.declined.push(key);
|
|
454
|
+
}
|
|
455
|
+
return [...byTarget].map(([target, collision]) => ({ ...collision, target }));
|
|
368
456
|
}
|
|
369
457
|
// ---------------------------------------------------------------------------
|
|
370
458
|
// Stage — drop client-added keys (#453)
|
|
@@ -431,25 +519,47 @@ function runStages(def, args, options, order) {
|
|
|
431
519
|
const plan = planFor(def.input);
|
|
432
520
|
if (plan.variants.length === 0)
|
|
433
521
|
return { args, changes: [] };
|
|
434
|
-
const ignoreList = ignoreListFor(options);
|
|
435
522
|
const record = args;
|
|
523
|
+
if (namesOnlyDeclaredKeys(plan, record))
|
|
524
|
+
return { args, changes: NO_CHANGES };
|
|
525
|
+
const ignoreList = ignoreListFor(options);
|
|
436
526
|
if (order === 'drop-first') {
|
|
437
527
|
// The drop has already removed every undeclared ignore-listed key, so the
|
|
438
528
|
// case-style pass folds whatever is left, exactly as it always has.
|
|
439
529
|
const drop = dropIgnoredKeys(record, plan, options, ignoreList);
|
|
440
530
|
const rewrite = applyAliases(def, drop.args, plan, options, NO_KEYS);
|
|
441
531
|
const changes = [...drop.changes, ...rewrite.changes];
|
|
442
|
-
return attempt(rewrite.args, changes, rewrite.aliased, drop.ignored);
|
|
532
|
+
return attempt(rewrite.args, changes, rewrite.aliased, drop.ignored, () => collisionsIn(record, rewrite, drop.ignored));
|
|
443
533
|
}
|
|
444
534
|
const rewrite = applyAliases(def, record, plan, options, ignoreList);
|
|
445
535
|
const drop = dropIgnoredKeys(rewrite.args, plan, options, ignoreList);
|
|
446
|
-
|
|
536
|
+
const changes = [...rewrite.changes, ...drop.changes];
|
|
537
|
+
return attempt(drop.args, changes, rewrite.aliased, drop.ignored, () => collisionsIn(record, rewrite, []));
|
|
538
|
+
}
|
|
539
|
+
/** No key rewritten or dropped. */
|
|
540
|
+
const NO_CHANGES = [];
|
|
541
|
+
/**
|
|
542
|
+
* Whether every key of `args` is one the variant they select declares — the
|
|
543
|
+
* shape of nearly every call. Neither stage then has anything to do: the drop
|
|
544
|
+
* skips a declared key, and an alias, declared or case-style, is a key the
|
|
545
|
+
* variant does not declare. Checked first, so such a call allocates nothing
|
|
546
|
+
* here and runs neither stage.
|
|
547
|
+
*/
|
|
548
|
+
function namesOnlyDeclaredKeys(plan, args) {
|
|
549
|
+
const variant = plan.variants.length === 1 ? plan.variants[0]?.plan : selectVariant(plan, args);
|
|
550
|
+
if (!variant)
|
|
551
|
+
return false;
|
|
552
|
+
for (const key in args) {
|
|
553
|
+
if (!variant.keys.has(key))
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
return true;
|
|
447
557
|
}
|
|
448
558
|
/** One attempt, carrying a report only when a rewrite or an underscore-rule drop happened. */
|
|
449
|
-
function attempt(args, changes, aliased, ignored) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
559
|
+
function attempt(args, changes, aliased, ignored, collisions) {
|
|
560
|
+
return aliased.length === 0 && ignored.length === 0
|
|
561
|
+
? { args, changes, collisions }
|
|
562
|
+
: { args, changes, collisions, report: { aliased, ignored } };
|
|
453
563
|
}
|
|
454
564
|
/**
|
|
455
565
|
* Runs the pre-parse half of the step in its first order — drop client-added
|
|
@@ -495,23 +605,62 @@ function sameArguments(a, b) {
|
|
|
495
605
|
/**
|
|
496
606
|
* Undoes a representation slip at each path the failed parse's issues named:
|
|
497
607
|
* a string whose trimmed form is a JSON array or object is parsed back into
|
|
498
|
-
* what it encodes (#234, #479),
|
|
499
|
-
* becomes its decimal string (#487)
|
|
500
|
-
*
|
|
608
|
+
* what it encodes (#234, #479), a safe integer where a string was expected
|
|
609
|
+
* becomes its decimal string (#487), a string where only numbers or booleans
|
|
610
|
+
* are accepted becomes the one it spells (#707), a lone string where an array
|
|
611
|
+
* was expected becomes its only element (#602), and `null` at a key the
|
|
612
|
+
* enclosing `z.object()` of `input` declares optional is deleted (#616). That
|
|
613
|
+
* deletion is the only key change: no key is added or renamed, none is dropped
|
|
614
|
+
* but a `null`-valued one, and a value no issue points at is returned
|
|
615
|
+
* untouched.
|
|
616
|
+
*
|
|
617
|
+
* **`null` means unset only where the schema says so.** The key must be one
|
|
618
|
+
* the enclosing object declares — resolved by {@link objectSchemaAt}, through
|
|
619
|
+
* wrappers and into the variant a discriminator selects — and its field must
|
|
620
|
+
* refuse `null` and accept `undefined`. A required field's `null` keeps its
|
|
621
|
+
* rejection, a `.nullable()` field never raises the issue, and a `z.record()`
|
|
622
|
+
* entry, a key under an author-opened catchall, and an array element are data
|
|
623
|
+
* whose key set is the meaning, so none is deleted. The value, not the issue
|
|
624
|
+
* code, is the trigger: `null` on an optional enum fails as `invalid_value`,
|
|
625
|
+
* whose message never says `null`.
|
|
501
626
|
*
|
|
502
627
|
* **Targeted, not a full walk.** A rejection names exactly the values the schema
|
|
503
628
|
* could not accept, and repairing anything else loses repairs that should have
|
|
504
629
|
* succeeded: one call carrying a stringified array for an array field *and* a
|
|
505
630
|
* free-text field legitimately holding `"[1,2]"` would have both rewritten, the
|
|
506
631
|
* re-parse would fail on the free-text field, and the whole call would be
|
|
507
|
-
* rejected over a value that was valid all along.
|
|
508
|
-
*
|
|
509
|
-
*
|
|
632
|
+
* rejected over a value that was valid all along.
|
|
633
|
+
*
|
|
634
|
+
* **The issues the rejection renders.** {@link parseToolArguments} passes the
|
|
635
|
+
* list the rejection's message renders, each at its full path: a union whose
|
|
636
|
+
* selection leaves one branch failing below its root contributes that branch's
|
|
637
|
+
* issues under the union's path, recursively (#570), and every other union
|
|
638
|
+
* keeps its one issue at its own path — the value the union rejected, read as
|
|
639
|
+
* a whole — and is also read at each path below it where every branch listed
|
|
640
|
+
* the values it takes (#714, {@link everyBranchRefuses}). That is where a
|
|
641
|
+
* literal tag no branch takes sits, read as a plain field holding every
|
|
642
|
+
* branch's literal reads it: `"1"` against tags `1` and `2` reads as it does
|
|
643
|
+
* against `z.union([z.literal(1), z.literal(2)])`. A discriminated union's
|
|
644
|
+
* unmatched discriminator, which Zod reports with no branches, is read from
|
|
645
|
+
* the tags it lists the same way ({@link unmatchedDiscriminator}). Inside the
|
|
646
|
+
* lifted branch each kind repairs exactly as it would in a non-union field,
|
|
647
|
+
* with its own gate. Only one branch looks inside the value there: every other
|
|
648
|
+
* failed at its root or on one literal, and so rejects the value whatever it
|
|
649
|
+
* holds — except where that literal sits at the very path being repaired,
|
|
650
|
+
* which the entry carries as a rival, and the gate then reads the value as a
|
|
651
|
+
* plain union field of those branches (`6` beside a `z.literal(5)` branch
|
|
652
|
+
* stays a number, so it stays rejected). A branch that accepts a number at the
|
|
653
|
+
* path is reported by Zod as its own check (`too_big`), which no gate takes.
|
|
654
|
+
* Two branches failing below their roots are never lifted, so neither is read
|
|
655
|
+
* below its root but where each lists the values it takes.
|
|
510
656
|
*
|
|
511
657
|
* **One pass, no stacking.** Only a value the first parse rejected is repaired,
|
|
512
|
-
* and at most once
|
|
513
|
-
*
|
|
514
|
-
*
|
|
658
|
+
* and at most once — the first issue at a path that yields a repair claims it —
|
|
659
|
+
* so a value one repair produced is never repaired again: an integer inside a
|
|
660
|
+
* stringified array, a stringified field inside a stringified object. The
|
|
661
|
+
* caller re-parses once. A repaired discriminator selects a variant Zod never
|
|
662
|
+
* parsed, so a second slip inside that variant is not repaired with it: the
|
|
663
|
+
* rejection reports it, the tag's repair held (#706).
|
|
515
664
|
*
|
|
516
665
|
* `JSON.parse` is the exact inverse of the `JSON.stringify` that produced a
|
|
517
666
|
* string, and `String(n)` of a safe integer is the digits the caller sent, so
|
|
@@ -521,35 +670,423 @@ function sameArguments(a, b) {
|
|
|
521
670
|
* after validation has already failed and keeps the result only when it then
|
|
522
671
|
* passes.
|
|
523
672
|
*
|
|
673
|
+
* **A string is read as another type only where the field types it itself.**
|
|
674
|
+
* A field routed through a `.transform()`, `.pipe()`, or `z.preprocess()`
|
|
675
|
+
* can reject its transform's output rather than the string sent — `"15"` an
|
|
676
|
+
* ID-or-name lookup does not know — so a string there is never turned into a
|
|
677
|
+
* number, boolean, or one-element list. Nor is one at a path that could land on
|
|
678
|
+
* such a field ({@link routesThroughPipeAt}): one option of a plain union
|
|
679
|
+
* declaring it while another declares or catches the same key, one side of an
|
|
680
|
+
* intersection, or an option whose own tag the caller sent wrong — the tag
|
|
681
|
+
* itself included, so `"1"` stays a string where one branch types its tag
|
|
682
|
+
* through a `z.preprocess()`.
|
|
683
|
+
*
|
|
684
|
+
* **A value inside a transform's output (#599).** Below a `z.preprocess()` or
|
|
685
|
+
* a `.transform().pipe()`, an issue path runs through what the transform made
|
|
686
|
+
* of the argument — `items.0.year` inside the list a preprocess wrapped a lone
|
|
687
|
+
* object in — so each path is read through {@link argumentAt}, which re-applies
|
|
688
|
+
* the transform in-process. The repair is written into that output, and the
|
|
689
|
+
* output is put in the argument's place only when the transform, applied to
|
|
690
|
+
* it, returns it structurally unchanged ({@link applyRepairs}); a transform
|
|
691
|
+
* that would reorder it, rewrite a value in it, or reject it leaves the
|
|
692
|
+
* argument as sent. The field the value sits in still decides the string
|
|
693
|
+
* kinds: a plain `z.string()` inside a wrapped list takes them, a field that
|
|
694
|
+
* is itself a pipe does not.
|
|
695
|
+
*
|
|
696
|
+
* **The same values written in place, tried first.** A repair can also be read
|
|
697
|
+
* and written at its issue path in the arguments as sent, leaving the re-parse
|
|
698
|
+
* to run the transform over it again. That validates calls the substitution
|
|
699
|
+
* refuses — `[1, 2]` under a preprocess that reverses a list reaches the
|
|
700
|
+
* handler as `["2", "1"]` — and gives some a different value: `[5]` under one
|
|
701
|
+
* that doubles numbers reaches it as `["5"]`, where the substitution would
|
|
702
|
+
* hand on `["10"]`. The original repair ({@link repairAsSent}) already places
|
|
703
|
+
* every value Zod's own issues name that way, so each call it validated keeps
|
|
704
|
+
* its value; `inPlace` carries the placement where that repair does not reach
|
|
705
|
+
* — inside a union's one surviving branch, and beside this pass's other
|
|
706
|
+
* repairs — so such a call gets the value the field gets on its own. It is
|
|
707
|
+
* built whenever a value below a transform, or behind one that throws when
|
|
708
|
+
* re-applied, is repairable that way, with the kinds that placement carries: a
|
|
709
|
+
* stringified array or object, and an integer sent for a string.
|
|
710
|
+
* {@link parseToolArguments} parses it after the original repair and before
|
|
711
|
+
* `args`, and keeps it when it validates. Each repair of `original` that no
|
|
712
|
+
* rendered issue reaches and that sits at no transform — a union field's
|
|
713
|
+
* value read whole, a stringified list beside a branch that splits a string —
|
|
714
|
+
* joins `repairs` too ({@link withOriginal}), so a call needing it beside a
|
|
715
|
+
* repair only this pass makes validates, and a rejection reports it as held.
|
|
716
|
+
* The repairs without it come back as `unaided`, parsed when `args` fails:
|
|
717
|
+
* integers inside the list a branch decodes from JSON text need the lifted
|
|
718
|
+
* branch's repairs, and the original reading of the text refuses them.
|
|
719
|
+
*
|
|
720
|
+
* Every value is read beside the caller's arguments, each transform at most
|
|
721
|
+
* once per value through `transforms`, and the repairs found are written in
|
|
722
|
+
* one {@link applyRepairs} pass per placement, so the work is linear in the
|
|
723
|
+
* issues however many values one array holds. They come back as `repairs`,
|
|
724
|
+
* one per path.
|
|
725
|
+
*
|
|
524
726
|
* `args` is the argument itself when nothing was repairable, which is the
|
|
525
727
|
* signal the caller uses to skip the second parse.
|
|
526
728
|
*/
|
|
527
|
-
export function repairRepresentations(args, issues) {
|
|
528
|
-
let
|
|
529
|
-
|
|
729
|
+
export function repairRepresentations(args, issues, input, transforms, original = []) {
|
|
730
|
+
let repairs = [];
|
|
731
|
+
let inPlace = [];
|
|
732
|
+
const claimed = new Set();
|
|
733
|
+
const placed = new Set();
|
|
734
|
+
const strayed = [];
|
|
735
|
+
for (const { issue, path, tag } of valueReads(issues)) {
|
|
736
|
+
if (path.length === 0)
|
|
737
|
+
continue;
|
|
738
|
+
const key = JSON.stringify(path);
|
|
739
|
+
if (claimed.has(key) && placed.has(key))
|
|
740
|
+
continue;
|
|
741
|
+
const at = argumentAt(input, path, args, transforms);
|
|
742
|
+
if (at.known && at.sites.length === 0) {
|
|
743
|
+
// The path names the caller's own value, so both placements are one.
|
|
744
|
+
placed.add(key);
|
|
745
|
+
}
|
|
746
|
+
else if (!placed.has(key)) {
|
|
747
|
+
const repair = inPlaceRepair(args, path, issue);
|
|
748
|
+
if (repair) {
|
|
749
|
+
inPlace.push(repair);
|
|
750
|
+
placed.add(key);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (claimed.has(key) || !at.known)
|
|
754
|
+
continue;
|
|
755
|
+
const within = at.sites.length > 0 ? { within: at.sites } : {};
|
|
756
|
+
const sent = at.value;
|
|
757
|
+
if (sent === null) {
|
|
758
|
+
if (!unsetAt(input, args, path, transforms))
|
|
759
|
+
continue;
|
|
760
|
+
repairs.push({ kind: 'null_as_absent', path, sent, ...within });
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
const repair = repairValue(sent, issue);
|
|
764
|
+
if (!repair)
|
|
765
|
+
continue;
|
|
766
|
+
if (READS_A_STRING.has(repair.kind) && routesThroughPipeAt(input, path, args, transforms)) {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
repairs.push({ kind: repair.kind, path, sent, value: repair.value, ...within });
|
|
770
|
+
if (tag && selectsRival(tag, repair.value))
|
|
771
|
+
strayed.push({ at: tag.at, path });
|
|
772
|
+
}
|
|
773
|
+
claimed.add(key);
|
|
774
|
+
}
|
|
775
|
+
if (strayed.length > 0) {
|
|
776
|
+
const leftBehind = belowStrayedBranch(strayed);
|
|
777
|
+
repairs = repairs.filter((repair) => !leftBehind(repair.path));
|
|
778
|
+
inPlace = inPlace.filter((repair) => !leftBehind(repair.path));
|
|
779
|
+
}
|
|
780
|
+
const rendered = repairs;
|
|
781
|
+
repairs = withOriginal(repairs, original, claimed, (path) => {
|
|
782
|
+
const at = argumentAt(input, path, args, transforms);
|
|
783
|
+
return at.known && at.sites.length === 0;
|
|
784
|
+
});
|
|
785
|
+
const repaired = repairs.length === 0 ? args : applyRepairs(args, repairs);
|
|
786
|
+
const result = {
|
|
787
|
+
args: repaired,
|
|
788
|
+
kinds: kindsOf(repairs),
|
|
789
|
+
repairs,
|
|
790
|
+
...unaidedBy(args, rendered, repairs),
|
|
791
|
+
};
|
|
792
|
+
if (inPlace.length === 0)
|
|
793
|
+
return result;
|
|
794
|
+
const placedRepairs = [...repairs.filter(({ within }) => !within), ...inPlace];
|
|
795
|
+
const writtenInPlace = applyRepairs(args, placedRepairs);
|
|
796
|
+
// Where the transform leaves every value at its own position, both placements are one parse.
|
|
797
|
+
if (sameValue(writtenInPlace, repaired))
|
|
798
|
+
return result;
|
|
799
|
+
return { ...result, inPlace: { args: writtenInPlace, kinds: kindsOf(placedRepairs) } };
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* `repairs` with each repair of `original` ({@link repairAsSent}) added that
|
|
803
|
+
* no repair here claimed and that names the caller's own value, `own` — no
|
|
804
|
+
* transform above its path — and each repair below an added one dropped. The
|
|
805
|
+
* rendered issues read a union whose selection lifts a branch inside that
|
|
806
|
+
* branch (#570), so the original repair of the union's value read whole — a
|
|
807
|
+
* stringified list beside a branch that splits a string — has no counterpart
|
|
808
|
+
* here, and a call needing it beside a repair only this pass makes would
|
|
809
|
+
* validate under neither. Below a transform the in-place placement already
|
|
810
|
+
* carries the original repair.
|
|
811
|
+
*/
|
|
812
|
+
function withOriginal(repairs, original, claimed, own) {
|
|
813
|
+
const added = original.filter((repair) => !claimed.has(JSON.stringify(repair.path)) && own(repair.path));
|
|
814
|
+
if (added.length === 0)
|
|
815
|
+
return repairs;
|
|
816
|
+
const root = { children: new Map() };
|
|
817
|
+
for (const { path } of added) {
|
|
818
|
+
let node = root;
|
|
819
|
+
for (const step of path) {
|
|
820
|
+
const key = String(step);
|
|
821
|
+
let child = node.children.get(key);
|
|
822
|
+
if (!child) {
|
|
823
|
+
child = { children: new Map() };
|
|
824
|
+
node.children.set(key, child);
|
|
825
|
+
}
|
|
826
|
+
node = child;
|
|
827
|
+
}
|
|
828
|
+
node.added = true;
|
|
829
|
+
}
|
|
830
|
+
const below = (path) => {
|
|
831
|
+
let node = root;
|
|
832
|
+
for (let depth = 0; node && depth < path.length - 1; depth++) {
|
|
833
|
+
node = node.children.get(String(path[depth]));
|
|
834
|
+
if (node?.added)
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
return false;
|
|
838
|
+
};
|
|
839
|
+
return [...repairs.filter((repair) => !below(repair.path)), ...added];
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* The substitution before {@link withOriginal} widened it — the rendered
|
|
843
|
+
* issues' own repairs alone, `rendered` — for when the widened one fails: a
|
|
844
|
+
* lifted branch's repairs can be what a call needs where the original reading
|
|
845
|
+
* of the union's value is refused, as integers inside the list a branch
|
|
846
|
+
* decodes from JSON text are. Empty when nothing was added, or when those
|
|
847
|
+
* repairs leave the arguments as sent.
|
|
848
|
+
*/
|
|
849
|
+
function unaidedBy(args, rendered, repairs) {
|
|
850
|
+
if (repairs === rendered || rendered.length === 0)
|
|
851
|
+
return {};
|
|
852
|
+
const unaided = applyRepairs(args, rendered);
|
|
853
|
+
return sameValue(unaided, args) ? {} : { unaided: { args: unaided, kinds: kindsOf(rendered) } };
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Every value {@link repairRepresentations} reads for `issues`, in order: each
|
|
857
|
+
* entry at its own path — a discriminated union's unmatched discriminator read
|
|
858
|
+
* as the values its variants take ({@link unmatchedDiscriminator}), an entry
|
|
859
|
+
* with {@link LocatedIssue.rivals} as the plain union field holding those
|
|
860
|
+
* branches ({@link asUnionIssue}) — then each path below a union the rendering
|
|
861
|
+
* left whole at which every branch listed the values it takes
|
|
862
|
+
* ({@link everyBranchRefuses}).
|
|
863
|
+
*/
|
|
864
|
+
function* valueReads(issues) {
|
|
865
|
+
for (const entry of issues) {
|
|
866
|
+
const read = unmatchedDiscriminator(entry.issue) ?? entry.issue;
|
|
867
|
+
const { path, rivals } = entry;
|
|
868
|
+
if (rivals) {
|
|
869
|
+
// Each rival's path is branch-relative, so the union sits that many steps up.
|
|
870
|
+
const at = path.slice(0, path.length - (rivals[0]?.path.length ?? 0));
|
|
871
|
+
yield { issue: asUnionIssue(read, rivals), path, tag: { at, own: read, rivals } };
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
yield { issue: read, path };
|
|
875
|
+
}
|
|
876
|
+
yield* everyBranchRefuses(entry);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Whether a repaired tag is a value its own branch's literal refuses and a
|
|
881
|
+
* dropped branch's literal takes: `"2"` repaired to `2` where the lifted branch
|
|
882
|
+
* is tagged `1` and a dropped one `2`. The dropped branch failed on that tag
|
|
883
|
+
* alone, so the repaired call selects it, and every other repair the lifted
|
|
884
|
+
* branch's issues asked for belongs to a branch the call no longer selects —
|
|
885
|
+
* one that would turn `id: 7`, valid in the selected branch, into `"7"`.
|
|
886
|
+
*/
|
|
887
|
+
function selectsRival({ own, rivals }, value) {
|
|
888
|
+
const takes = (literal) => literal.code === 'invalid_value' && literal.values.includes(value);
|
|
889
|
+
return own.code === 'invalid_value' && !takes(own) && rivals.some(takes);
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Whether a repair's path lies below a union in `strayed` other than at that
|
|
893
|
+
* union's tag — a repair {@link selectsRival} says belongs to a branch the call
|
|
894
|
+
* no longer selects. The unions are held as a tree of their paths, so each
|
|
895
|
+
* question costs the depth of the path asked about.
|
|
896
|
+
*/
|
|
897
|
+
function belowStrayedBranch(strayed) {
|
|
898
|
+
const root = { children: new Map() };
|
|
899
|
+
for (const { at, path } of strayed) {
|
|
900
|
+
let node = root;
|
|
901
|
+
for (const step of at) {
|
|
902
|
+
const key = String(step);
|
|
903
|
+
let child = node.children.get(key);
|
|
904
|
+
if (!child) {
|
|
905
|
+
child = { children: new Map() };
|
|
906
|
+
node.children.set(key, child);
|
|
907
|
+
}
|
|
908
|
+
node = child;
|
|
909
|
+
}
|
|
910
|
+
node.tags ??= new Set();
|
|
911
|
+
node.tags.add(JSON.stringify(path.slice(at.length)));
|
|
912
|
+
}
|
|
913
|
+
return (path) => {
|
|
914
|
+
let node = root;
|
|
915
|
+
for (let depth = 0; node && depth < path.length; depth++) {
|
|
916
|
+
if (node.tags && !node.tags.has(JSON.stringify(path.slice(depth))))
|
|
917
|
+
return true;
|
|
918
|
+
node = node.children.get(String(path[depth]));
|
|
919
|
+
}
|
|
920
|
+
return false;
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* A discriminated union's issue for a discriminator no variant takes, which
|
|
925
|
+
* carries no branches, as the `invalid_value` a field holding every variant's
|
|
926
|
+
* tag raises at the discriminator (#714): Zod lists those tags as `options`.
|
|
927
|
+
* A tag a variant leaves optional is listed as `undefined`, which says nothing
|
|
928
|
+
* about a value the caller sent, so it is left out — as a plain union of the
|
|
929
|
+
* same variants leaves it out. `undefined` for any other issue.
|
|
930
|
+
*/
|
|
931
|
+
function unmatchedDiscriminator(issue) {
|
|
932
|
+
if (issue.code !== 'invalid_union' || issue.errors.length > 0 || !('options' in issue))
|
|
933
|
+
return;
|
|
934
|
+
const values = (issue.options ?? []).filter((value) => value !== undefined);
|
|
935
|
+
if (values.length === 0)
|
|
936
|
+
return;
|
|
937
|
+
return { code: 'invalid_value', message: issue.message, path: issue.path, values };
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* The values a union the rendering left whole refused in every branch (#714):
|
|
941
|
+
* each path below the union at which every branch raised an `invalid_value` —
|
|
942
|
+
* a literal tag no branch takes, or any field each branch lists the values
|
|
943
|
+
* of — or an unmatched discriminator ({@link unmatchedDiscriminator}), read as
|
|
944
|
+
* the one issue `z.union()` of those literals raises there
|
|
945
|
+
* ({@link asUnionIssue}). The gates then read `"1"` against tags `1` and `2`
|
|
946
|
+
* exactly as they read it against `z.union([z.literal(1), z.literal(2)])`.
|
|
947
|
+
*
|
|
948
|
+
* Each branch's values are mapped by path once, and a candidate is looked up
|
|
949
|
+
* in the other branches' maps: scanning them for it instead costs the square
|
|
950
|
+
* of the issues on a union whose branches each report many values at paths
|
|
951
|
+
* the others lack.
|
|
952
|
+
*/
|
|
953
|
+
function* everyBranchRefuses({ issue, path }) {
|
|
954
|
+
if (issue.code !== 'invalid_union')
|
|
955
|
+
return;
|
|
956
|
+
const [first, ...others] = issue.errors.map((branch) => {
|
|
957
|
+
const byPath = new Map();
|
|
958
|
+
for (const branchIssue of branch) {
|
|
959
|
+
const listed = branchIssue.code === 'invalid_value' ? branchIssue : unmatchedDiscriminator(branchIssue);
|
|
960
|
+
if (!listed || listed.path.length === 0)
|
|
961
|
+
continue;
|
|
962
|
+
const at = JSON.stringify(listed.path);
|
|
963
|
+
if (!byPath.has(at))
|
|
964
|
+
byPath.set(at, listed);
|
|
965
|
+
}
|
|
966
|
+
return byPath;
|
|
967
|
+
});
|
|
968
|
+
if (!first)
|
|
969
|
+
return;
|
|
970
|
+
for (const [at, listed] of first) {
|
|
971
|
+
const rivals = others.map((branch) => branch.get(at));
|
|
972
|
+
if (rivals.every((rival) => rival !== undefined)) {
|
|
973
|
+
yield { issue: asUnionIssue(listed, rivals), path: [...path, ...listed.path] };
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/** Each kind `repairs` holds, in {@link COERCION_KINDS} order. */
|
|
978
|
+
function kindsOf(repairs) {
|
|
979
|
+
const fired = new Set(repairs.map((repair) => repair.kind));
|
|
980
|
+
return Object.keys(COERCION_KINDS).filter((kind) => fired.has(kind));
|
|
981
|
+
}
|
|
982
|
+
/** The kinds a repair written in place carries ({@link inPlaceRepair}). */
|
|
983
|
+
const IN_PLACE_KINDS = new Set([
|
|
984
|
+
'stringified_array',
|
|
985
|
+
'stringified_object',
|
|
986
|
+
'integer_as_string',
|
|
987
|
+
]);
|
|
988
|
+
/**
|
|
989
|
+
* The repair the value at `path` in the arguments as sent takes, written back
|
|
990
|
+
* at that same path — for a path below a transform, whose issue names a value
|
|
991
|
+
* in the transform's output, the value read can be another one or none at
|
|
992
|
+
* all. Only {@link IN_PLACE_KINDS}: each reads nothing but the value and the
|
|
993
|
+
* issue, so it needs no schema at a path the arguments as sent do not follow.
|
|
994
|
+
*/
|
|
995
|
+
function inPlaceRepair(args, path, issue) {
|
|
996
|
+
const sent = path.reduce((value, step) => stepInto(value, step), args);
|
|
997
|
+
const repair = repairValue(sent, issue);
|
|
998
|
+
if (!repair || !IN_PLACE_KINDS.has(repair.kind))
|
|
999
|
+
return;
|
|
1000
|
+
return { kind: repair.kind, path, sent, value: repair.value };
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* The repair 0.13.13 and earlier made, which every call it validated must
|
|
1004
|
+
* still get first: each of the failed parse's own issues at a non-root path,
|
|
1005
|
+
* read and written at that path in the arguments as sent
|
|
1006
|
+
* ({@link inPlaceRepair}). A value takes one kind of those repairs whichever
|
|
1007
|
+
* issue names it, and {@link applyRepairs} writes a path two repairs share
|
|
1008
|
+
* once. Zod's own list, not the one the rejection renders: a union whose
|
|
1009
|
+
* selection lifts a branch (#570) is read here as the one value it rejected,
|
|
1010
|
+
* so a stringified list for a field whose other branch splits or wraps a
|
|
1011
|
+
* string parses back into the list, as that reading gave it. `undefined` when
|
|
1012
|
+
* nothing repairs.
|
|
1013
|
+
*/
|
|
1014
|
+
export function repairAsSent(args, issues) {
|
|
1015
|
+
const repairs = [];
|
|
530
1016
|
for (const issue of issues) {
|
|
531
1017
|
if (issue.path.length === 0)
|
|
532
1018
|
continue;
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
repaired = writeAt(repaired, issue.path, repair.value);
|
|
537
|
-
fired.add(repair.kind);
|
|
538
|
-
}
|
|
1019
|
+
const repair = inPlaceRepair(args, issue.path, issue);
|
|
1020
|
+
if (repair)
|
|
1021
|
+
repairs.push(repair);
|
|
539
1022
|
}
|
|
1023
|
+
if (repairs.length === 0)
|
|
1024
|
+
return;
|
|
1025
|
+
return { args: applyRepairs(args, repairs), kinds: kindsOf(repairs), repairs };
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* `issue` and its {@link LocatedIssue.rivals}, as the one `invalid_union` a
|
|
1029
|
+
* plain union field holding those branches raises at that path — each
|
|
1030
|
+
* branch's issue at its root — so every gate reads the value exactly as it
|
|
1031
|
+
* reads that field.
|
|
1032
|
+
*/
|
|
1033
|
+
function asUnionIssue(issue, rivals) {
|
|
540
1034
|
return {
|
|
541
|
-
|
|
542
|
-
|
|
1035
|
+
code: 'invalid_union',
|
|
1036
|
+
errors: [issue, ...rivals].map((branch) => [{ ...branch, path: [] }]),
|
|
1037
|
+
message: issue.message,
|
|
1038
|
+
path: issue.path,
|
|
543
1039
|
};
|
|
544
1040
|
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Whether `null` at `path` means the key is unset: the enclosing `z.object()`
|
|
1043
|
+
* declares the key, and its field refuses `null` but accepts `undefined`
|
|
1044
|
+
* (`.optional()`, `.default()`). `.exactOptional()` fails the second test even
|
|
1045
|
+
* though its object accepts the key absent, so its `null` keeps the rejection.
|
|
1046
|
+
*/
|
|
1047
|
+
function unsetAt(input, args, path, transforms) {
|
|
1048
|
+
const key = path.at(-1);
|
|
1049
|
+
if (typeof key !== 'string')
|
|
1050
|
+
return false;
|
|
1051
|
+
const shape = objectSchemaAt(input, path.slice(0, -1), args, transforms)?.shape;
|
|
1052
|
+
if (!shape || !Object.hasOwn(shape, key))
|
|
1053
|
+
return false;
|
|
1054
|
+
const field = shape[key];
|
|
1055
|
+
// An author check or transform that throws on `undefined` throws for the
|
|
1056
|
+
// omitted key too; here it only means the `null` keeps its rejection.
|
|
1057
|
+
try {
|
|
1058
|
+
return !field.safeParse(null).success && field.safeParse(undefined).success;
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* The kinds that read a string as another type. Each is refused at a field
|
|
1066
|
+
* that routes its value through a transform ({@link routesThroughPipeAt}): there
|
|
1067
|
+
* the rejection can describe the transform's output, so the string the caller
|
|
1068
|
+
* sent may be one the field takes — a name its lookup lacks, not a number.
|
|
1069
|
+
*/
|
|
1070
|
+
const READS_A_STRING = new Set([
|
|
1071
|
+
'string_as_number',
|
|
1072
|
+
'string_as_boolean',
|
|
1073
|
+
'string_as_array',
|
|
1074
|
+
]);
|
|
545
1075
|
/**
|
|
546
1076
|
* The repair for one value the parse rejected, or `undefined` when none applies.
|
|
547
1077
|
*
|
|
548
1078
|
* A string is decoded only when its trimmed form opens a JSON array or object —
|
|
549
|
-
* the grammar then guarantees the decoded kind
|
|
550
|
-
*
|
|
1079
|
+
* the grammar then guarantees the decoded kind — and such a string is never
|
|
1080
|
+
* read any other way, even when it does not parse. A number becomes a string
|
|
1081
|
+
* only when it is a safe integer other than `-0` (`String(-0)` is `"0"`, and an
|
|
551
1082
|
* unsafe integer already lost digits in `JSON.parse`), and only when the issue
|
|
552
|
-
* says it failed for being a number ({@link failedAsNumber}).
|
|
1083
|
+
* says it failed for being a number ({@link failedAsNumber}). Any other string
|
|
1084
|
+
* is wrapped as a one-element array where the issue is `invalid_type`
|
|
1085
|
+
* expecting an array ({@link wrapsAsArray}) — never a tuple
|
|
1086
|
+
* (`expected: "tuple"`) or a union field (`invalid_union`) — and otherwise
|
|
1087
|
+
* becomes the number or boolean it spells, only where the issue accepts
|
|
1088
|
+
* nothing but those ({@link scalarTypesAt}). Each string matches one rule at
|
|
1089
|
+
* most, so the kinds never compete.
|
|
553
1090
|
*/
|
|
554
1091
|
function repairValue(value, issue) {
|
|
555
1092
|
if (typeof value === 'number') {
|
|
@@ -565,14 +1102,105 @@ function repairValue(value, issue) {
|
|
|
565
1102
|
: trimmed.startsWith('{')
|
|
566
1103
|
? 'stringified_object'
|
|
567
1104
|
: undefined;
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1105
|
+
if (kind) {
|
|
1106
|
+
try {
|
|
1107
|
+
return { kind, value: JSON.parse(trimmed) };
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
572
1112
|
}
|
|
573
|
-
|
|
574
|
-
return undefined;
|
|
1113
|
+
if (issue.code === 'invalid_type' && issue.expected === 'array') {
|
|
1114
|
+
return wrapsAsArray(value, trimmed) ? { kind: 'string_as_array', value: [value] } : undefined;
|
|
575
1115
|
}
|
|
1116
|
+
return scalarFromString(trimmed, scalarTypesAt(issue));
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Whether a string sent where an array was expected is its one element, as
|
|
1120
|
+
* sent (#602). Never a blank string — a form client's unset field — and never
|
|
1121
|
+
* one holding a comma or a line break, which may be a joined list: splitting it
|
|
1122
|
+
* is the server's call, and the rejection already says to send an array. A line
|
|
1123
|
+
* break is any Unicode mandatory break: `\n`, `\v`, `\f`, `\r`, next line
|
|
1124
|
+
* (U+0085), and the line and paragraph separators (U+2028, U+2029).
|
|
1125
|
+
*/
|
|
1126
|
+
function wrapsAsArray(value, trimmed) {
|
|
1127
|
+
return trimmed.length > 0 && !/[,\n\v\f\r\x85\p{Zl}\p{Zp}]/u.test(value);
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* The number or boolean a trimmed string spells, when `accepted` takes it.
|
|
1131
|
+
*
|
|
1132
|
+
* A number only when `String(n)` gives the string back — the decimal form
|
|
1133
|
+
* `JSON.stringify` writes — so the repair is an exact inverse: `"15"`, `"2.5"`,
|
|
1134
|
+
* `"-3"` convert, while `"01"`, `"1e3"`, `"-0"`, `"1.50"`, `"0x10"`, a blank,
|
|
1135
|
+
* and an integer past 2^53 (it would lose digits) never do. A boolean only from
|
|
1136
|
+
* `"true"` or `"false"` exactly.
|
|
1137
|
+
*/
|
|
1138
|
+
function scalarFromString(trimmed, accepted) {
|
|
1139
|
+
if (accepted.has('number')) {
|
|
1140
|
+
const number = Number(trimmed);
|
|
1141
|
+
if (Number.isFinite(number) && String(number) === trimmed) {
|
|
1142
|
+
return { kind: 'string_as_number', value: number };
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (accepted.has('boolean') && (trimmed === 'true' || trimmed === 'false')) {
|
|
1146
|
+
return { kind: 'string_as_boolean', value: trimmed === 'true' };
|
|
1147
|
+
}
|
|
1148
|
+
return undefined;
|
|
1149
|
+
}
|
|
1150
|
+
/** What {@link scalarTypesAt} returns for an issue that takes a string some way. */
|
|
1151
|
+
const NO_SCALARS = new Set();
|
|
1152
|
+
/**
|
|
1153
|
+
* The scalar types a string could be converted into at an issue — empty unless
|
|
1154
|
+
* the issue says the string failed for its type where only numbers and booleans
|
|
1155
|
+
* are accepted, the mirror of {@link failedAsNumber}'s gate (#707):
|
|
1156
|
+
*
|
|
1157
|
+
* - `invalid_type` expecting `number` or `boolean`.
|
|
1158
|
+
* - `invalid_value` listing only numbers, booleans, and `null` — a numeric
|
|
1159
|
+
* literal set or enum, `z.literal(true)`.
|
|
1160
|
+
* - `invalid_union` whose every branch failed one of those ways, or as
|
|
1161
|
+
* `invalid_type` expecting `null`, at its own root.
|
|
1162
|
+
*
|
|
1163
|
+
* Anything else takes a string some way and keeps its rejection: `"15"` against
|
|
1164
|
+
* `z.union([z.number(), z.literal('auto')])` fails a branch that lists a
|
|
1165
|
+
* string, so the field takes strings and `"15"` is a wrong one, and a branch
|
|
1166
|
+
* expecting an array gives the string a second reading. A discriminated
|
|
1167
|
+
* union's unmatched discriminator reports no branches and never passes:
|
|
1168
|
+
* {@link repairRepresentations} asks with the tags it lists instead
|
|
1169
|
+
* ({@link unmatchedDiscriminator}).
|
|
1170
|
+
*/
|
|
1171
|
+
function scalarTypesAt(issue) {
|
|
1172
|
+
const types = new Set();
|
|
1173
|
+
const admits = (candidate) => {
|
|
1174
|
+
switch (candidate.code) {
|
|
1175
|
+
case 'invalid_type':
|
|
1176
|
+
if (candidate.expected === 'number')
|
|
1177
|
+
types.add('number');
|
|
1178
|
+
else if (candidate.expected === 'boolean')
|
|
1179
|
+
types.add('boolean');
|
|
1180
|
+
else
|
|
1181
|
+
return candidate.expected === 'null';
|
|
1182
|
+
return true;
|
|
1183
|
+
case 'invalid_value':
|
|
1184
|
+
return (candidate.values.length > 0 &&
|
|
1185
|
+
candidate.values.every((value) => {
|
|
1186
|
+
if (typeof value === 'number')
|
|
1187
|
+
types.add('number');
|
|
1188
|
+
else if (typeof value === 'boolean')
|
|
1189
|
+
types.add('boolean');
|
|
1190
|
+
else
|
|
1191
|
+
return value === null;
|
|
1192
|
+
return true;
|
|
1193
|
+
}));
|
|
1194
|
+
default:
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const gated = issue.code === 'invalid_union'
|
|
1199
|
+
? issue.errors.length > 0 &&
|
|
1200
|
+
issue.errors.every((branch) => branch.length > 0 &&
|
|
1201
|
+
branch.every((branchIssue) => branchIssue.path.length === 0 && admits(branchIssue)))
|
|
1202
|
+
: admits(issue);
|
|
1203
|
+
return gated ? types : NO_SCALARS;
|
|
576
1204
|
}
|
|
577
1205
|
/**
|
|
578
1206
|
* Whether an issue says its value failed for being a number, rather than for
|
|
@@ -589,7 +1217,9 @@ function repairValue(value, issue) {
|
|
|
589
1217
|
* branch's own `too_small` — that union takes numbers, and `"-1"` would slip
|
|
590
1218
|
* past the author's constraint through the string branch — and `6` against
|
|
591
1219
|
* `z.union([z.literal(5), z.string()])` fails a branch that lists a number. A
|
|
592
|
-
* discriminator
|
|
1220
|
+
* discriminated union's unmatched discriminator reports no branches and never
|
|
1221
|
+
* passes: {@link repairRepresentations} asks with the tags it lists instead
|
|
1222
|
+
* ({@link unmatchedDiscriminator}).
|
|
593
1223
|
*/
|
|
594
1224
|
function failedAsNumber(issue) {
|
|
595
1225
|
switch (issue.code) {
|
|
@@ -605,51 +1235,185 @@ function failedAsNumber(issue) {
|
|
|
605
1235
|
return false;
|
|
606
1236
|
}
|
|
607
1237
|
}
|
|
608
|
-
/**
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1238
|
+
/**
|
|
1239
|
+
* `repairs` as a tree of their paths, each transform a repair sits inside
|
|
1240
|
+
* attached to the node at its position. A path two repairs share keeps the
|
|
1241
|
+
* first, and so does a position two repairs' transforms share.
|
|
1242
|
+
*/
|
|
1243
|
+
function repairTree(repairs) {
|
|
1244
|
+
const root = {};
|
|
1245
|
+
for (const repair of repairs) {
|
|
1246
|
+
const { path, within = [] } = repair;
|
|
1247
|
+
let node = root;
|
|
1248
|
+
let next = 0;
|
|
1249
|
+
for (let depth = 0;; depth++) {
|
|
1250
|
+
const first = next;
|
|
1251
|
+
while (within[next]?.depth === depth)
|
|
1252
|
+
next++;
|
|
1253
|
+
if (next > first && !node.sites)
|
|
1254
|
+
node.sites = within.slice(first, next);
|
|
1255
|
+
if (depth === path.length)
|
|
1256
|
+
break;
|
|
1257
|
+
node.children ??= new Map();
|
|
1258
|
+
const step = String(path[depth]);
|
|
1259
|
+
let child = node.children.get(step);
|
|
1260
|
+
if (!child) {
|
|
1261
|
+
child = {};
|
|
1262
|
+
node.children.set(step, child);
|
|
1263
|
+
}
|
|
1264
|
+
node = child;
|
|
1265
|
+
}
|
|
1266
|
+
node.repair ??= repair;
|
|
617
1267
|
}
|
|
618
|
-
return
|
|
1268
|
+
return root;
|
|
619
1269
|
}
|
|
620
1270
|
/**
|
|
621
|
-
*
|
|
622
|
-
* leaving the caller's own objects untouched
|
|
1271
|
+
* Writes `repairs` onto `args`, copying each container on their paths once and
|
|
1272
|
+
* leaving the caller's own objects untouched — so a thousand repaired elements
|
|
1273
|
+
* of one array cost one copy of it, not a thousand. A `null_as_absent` repair
|
|
1274
|
+
* leaves its key out of the copy of the object that holds it. A path two
|
|
1275
|
+
* repairs share takes the first. Any subset of one call's
|
|
1276
|
+
* {@link RepairedArguments.repairs} can be written, which is how
|
|
1277
|
+
* {@link parseToolArguments} builds a rejection from the repairs that held.
|
|
1278
|
+
*
|
|
1279
|
+
* A repair inside a transform's output (#599) is written into that output, and
|
|
1280
|
+
* the output takes the argument's place only when the transform, applied to
|
|
1281
|
+
* it, returns it unchanged — compared structurally, since a transform may
|
|
1282
|
+
* rebuild the list it was handed. That is the one check the validity re-parse
|
|
1283
|
+
* cannot make: a transform that reorders or rewrites its input could validate
|
|
1284
|
+
* with values the repair never wrote. Otherwise every repair inside that
|
|
1285
|
+
* output is dropped and the argument is left as sent, so the call keeps its
|
|
1286
|
+
* rejection. A transform that throws on the output counts as one that changed
|
|
1287
|
+
* it. Nested transforms are checked at each position, innermost first.
|
|
623
1288
|
*
|
|
624
1289
|
* Object copies go through `Object.fromEntries`, which *defines* each property
|
|
625
1290
|
* rather than assigning it. `copy[key] = value` would route a caller's own
|
|
626
1291
|
* `__proto__` key — which `JSON.parse` creates as an ordinary own data property
|
|
627
1292
|
* — through `Object.prototype`'s setter: the key would vanish and the copy's
|
|
628
1293
|
* prototype would become whatever the caller sent, which Zod then reads
|
|
629
|
-
* inherited values from. Defining keeps the key and the prototype,
|
|
630
|
-
*
|
|
1294
|
+
* inherited values from. Defining keeps the key and the prototype, so no key is
|
|
1295
|
+
* added or renamed, and none is dropped but a deleted one.
|
|
1296
|
+
*/
|
|
1297
|
+
export function applyRepairs(args, repairs) {
|
|
1298
|
+
return applyNode(args, repairTree(repairs));
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* The repairs a failed re-parse kept (#706): each one no issue of `issues` lies
|
|
1302
|
+
* at or below — the re-parse accepted the value it wrote, and whatever still
|
|
1303
|
+
* fails is somewhere else. A union's issue counts at its own path and every
|
|
1304
|
+
* branch's issues at theirs, whichever branch the rendering would select: a
|
|
1305
|
+
* repair one branch accepts and another refuses at the same path did not hold,
|
|
1306
|
+
* because the repaired value is what the refusing branch would then report.
|
|
1307
|
+
*
|
|
1308
|
+
* Each issue walks the tree of repair paths along its own path, so the check
|
|
1309
|
+
* is linear in the issues and the repairs together, however many of each one
|
|
1310
|
+
* array carries. An issue is walked from a node once (#648): Zod hands every
|
|
1311
|
+
* branch that parsed the same value the same issue objects, so a recursive
|
|
1312
|
+
* union whose `and` and `or` branches share a clause lists it under both, and
|
|
1313
|
+
* walking each listing would double the walk per level of the caller's
|
|
1314
|
+
* nesting.
|
|
631
1315
|
*/
|
|
632
|
-
function
|
|
633
|
-
const
|
|
634
|
-
|
|
1316
|
+
export function heldRepairs(repairs, issues) {
|
|
1317
|
+
const broken = new Set();
|
|
1318
|
+
const walked = new Map();
|
|
1319
|
+
const strike = (from, issue) => {
|
|
1320
|
+
const seen = walked.get(from) ?? new Set();
|
|
1321
|
+
if (seen.has(issue))
|
|
1322
|
+
return;
|
|
1323
|
+
walked.set(from, seen.add(issue));
|
|
1324
|
+
let node = from;
|
|
1325
|
+
for (const step of issue.path) {
|
|
1326
|
+
const child = node.children?.get(String(step));
|
|
1327
|
+
if (!child)
|
|
1328
|
+
return;
|
|
1329
|
+
node = child;
|
|
1330
|
+
if (node.repair)
|
|
1331
|
+
broken.add(node.repair);
|
|
1332
|
+
}
|
|
1333
|
+
if (issue.code !== 'invalid_union')
|
|
1334
|
+
return;
|
|
1335
|
+
for (const branch of issue.errors) {
|
|
1336
|
+
for (const branchIssue of branch)
|
|
1337
|
+
strike(node, branchIssue);
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
const tree = repairTree(repairs);
|
|
1341
|
+
for (const issue of issues)
|
|
1342
|
+
strike(tree, issue);
|
|
1343
|
+
return repairs.filter((repair) => !broken.has(repair));
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* {@link applyRepairs} for one value and the repairs at and below it. At a
|
|
1347
|
+
* transform's position the repairs below are written into its output instead,
|
|
1348
|
+
* kept only when every transform there returns the result unchanged.
|
|
1349
|
+
*/
|
|
1350
|
+
function applyNode(value, node) {
|
|
1351
|
+
const { repair, sites } = node;
|
|
1352
|
+
if (!sites || repair)
|
|
1353
|
+
return writeNode(value, node);
|
|
1354
|
+
const written = writeNode(sites[sites.length - 1]?.output, node);
|
|
1355
|
+
return sites.every((site) => returnsUnchanged(site.transform, written)) ? written : value;
|
|
1356
|
+
}
|
|
1357
|
+
/** Whether `transform` applied to `value` gives back a value structurally equal to it. */
|
|
1358
|
+
function returnsUnchanged(transform, value) {
|
|
1359
|
+
try {
|
|
1360
|
+
const again = transform.safeParse(value);
|
|
1361
|
+
return again.success && sameValue(again.data, value);
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Structural equality over what arguments are made of: the same value, or
|
|
1369
|
+
* arrays and plain objects holding equal entries under the same keys. Any
|
|
1370
|
+
* other object equals only itself.
|
|
1371
|
+
*/
|
|
1372
|
+
export function sameValue(a, b) {
|
|
1373
|
+
if (Object.is(a, b))
|
|
1374
|
+
return true;
|
|
1375
|
+
if (Array.isArray(a)) {
|
|
1376
|
+
return (Array.isArray(b) && a.length === b.length && a.every((entry, i) => sameValue(entry, b[i])));
|
|
1377
|
+
}
|
|
1378
|
+
if (!isPlainObject(a) || !isPlainObject(b))
|
|
1379
|
+
return false;
|
|
1380
|
+
const keys = Object.keys(a);
|
|
1381
|
+
return (keys.length === Object.keys(b).length &&
|
|
1382
|
+
keys.every((key) => Object.hasOwn(b, key) && sameValue(a[key], b[key])));
|
|
1383
|
+
}
|
|
1384
|
+
/** A `{}` or `Object.create(null)` object — what `JSON.parse` and a spread produce. */
|
|
1385
|
+
function isPlainObject(value) {
|
|
1386
|
+
if (value === null || typeof value !== 'object')
|
|
1387
|
+
return false;
|
|
1388
|
+
const proto = Object.getPrototypeOf(value);
|
|
1389
|
+
return proto === Object.prototype || proto === null;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Writes the repairs at and below `node` onto `value` itself. A deletion is
|
|
1393
|
+
* applied by the object holding the key, so a value reached through one comes
|
|
1394
|
+
* back as sent.
|
|
1395
|
+
*/
|
|
1396
|
+
function writeNode(value, node) {
|
|
1397
|
+
const { children, repair } = node;
|
|
1398
|
+
if (repair)
|
|
1399
|
+
return repair.kind === 'null_as_absent' ? value : repair.value;
|
|
1400
|
+
if (!children || value === null || typeof value !== 'object')
|
|
635
1401
|
return value;
|
|
636
|
-
if (Array.isArray(
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1402
|
+
if (Array.isArray(value)) {
|
|
1403
|
+
const copy = [...value];
|
|
1404
|
+
for (const [step, child] of children) {
|
|
1405
|
+
const index = Number(step);
|
|
1406
|
+
if (Number.isInteger(index) && index >= 0 && index < copy.length) {
|
|
1407
|
+
copy[index] = applyNode(value[index], child);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
642
1410
|
return copy;
|
|
643
1411
|
}
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
if (!
|
|
647
|
-
return
|
|
648
|
-
return
|
|
649
|
-
|
|
650
|
-
entryKey === key ? writeAt(entryValue, rest, value) : entryValue,
|
|
651
|
-
]));
|
|
652
|
-
}
|
|
653
|
-
return root;
|
|
1412
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
|
|
1413
|
+
const child = children.get(key);
|
|
1414
|
+
if (!child)
|
|
1415
|
+
return [[key, entry]];
|
|
1416
|
+
return child.repair?.kind === 'null_as_absent' ? [] : [[key, applyNode(entry, child)]];
|
|
1417
|
+
}));
|
|
654
1418
|
}
|
|
655
1419
|
//# sourceMappingURL=inputPrevalidation.js.map
|