@amritk/validation 0.5.0 → 0.6.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.
@@ -0,0 +1,335 @@
1
+ import { regexFlagsFor } from "@amritk/helpers/escape-regex-pattern";
2
+ import { readKey } from "@amritk/helpers/read-key";
3
+ import { refToName } from "@amritk/helpers/ref-to-name";
4
+ import { isSchemaObject } from "@amritk/helpers/schema-guards";
5
+ import { namedTest, schemaMap, subschema } from "./repair-context.js";
6
+ import { isReferenceSchema, leafKeywords } from "./repair-walkable.js";
7
+ const repairWalkNameFor = (typeName) => `walk${typeName}Repair`;
8
+ const SCALAR_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
9
+ const typeCondition = (type) => {
10
+ switch (type) {
11
+ case "string":
12
+ return `typeof v === 'string'`;
13
+ case "number":
14
+ return `(typeof v === 'number' && (!run.finite || Number.isFinite(v)))`;
15
+ case "integer":
16
+ return `Number.isInteger(v)`;
17
+ case "boolean":
18
+ return `typeof v === 'boolean'`;
19
+ case "null":
20
+ return `v === null`;
21
+ case "array":
22
+ return `Array.isArray(v)`;
23
+ default:
24
+ return `(typeof v === 'object' && v !== null && !Array.isArray(v) && (!run.plain || isPlainObject(v)))`;
25
+ }
26
+ };
27
+ const equalsCondition = (value) => {
28
+ if (typeof value === "number" && Number.isNaN(value))
29
+ return `Number.isNaN(v)`;
30
+ if (value !== null && typeof value === "object")
31
+ return `valuesEqual(v, ${JSON.stringify(value)})`;
32
+ return `(v as unknown) === ${JSON.stringify(value)}`;
33
+ };
34
+ const ownChecks = (node, ctx) => {
35
+ const checks = [];
36
+ const type = readKey(node, "type");
37
+ if (typeof type === "string") {
38
+ checks.push({
39
+ condition: typeCondition(type),
40
+ report: `found.push(walkError(run, 'type', ${JSON.stringify(`must be ${type}`)}, ${JSON.stringify({ type })}))`
41
+ });
42
+ } else if (Array.isArray(type)) {
43
+ const types = type;
44
+ checks.push({
45
+ condition: `(${types.map(typeCondition).join(" || ")})`,
46
+ report: `found.push(walkError(run, 'type', ${JSON.stringify(`must be ${types.join(" or ")}`)}, ${JSON.stringify({ type })}))`
47
+ });
48
+ }
49
+ const values = readKey(node, "enum");
50
+ if (Array.isArray(values)) {
51
+ const label = values.map((value) => JSON.stringify(value)).join(", ");
52
+ checks.push({
53
+ condition: values.length === 0 ? "false" : `(${values.map(equalsCondition).join(" || ")})`,
54
+ report: `found.push(walkError(run, 'enum', ${JSON.stringify(`must be one of: ${label}`)}, ${JSON.stringify({ allowedValues: values })}))`
55
+ });
56
+ }
57
+ if (Object.hasOwn(node, "const")) {
58
+ const value = node["const"];
59
+ checks.push({
60
+ condition: equalsCondition(value),
61
+ report: `found.push(walkError(run, 'const', ${JSON.stringify(`must be ${JSON.stringify(value)}`)}, ${JSON.stringify({ allowedValue: value })}))`
62
+ });
63
+ }
64
+ const leaf = leafKeywords(node);
65
+ if (leaf !== null) {
66
+ const test = namedTest(leaf, ctx.repair);
67
+ if (test !== true) {
68
+ const explain = ctx.repair.matcher.explain(leaf);
69
+ checks.push({
70
+ condition: test === false ? "false" : `${test}(v)`,
71
+ report: explain === null ? `found.push(walkError(run, 'false schema', 'boolean schema is false', {}))` : `for (const error of ${explain}(v, walkPath(run))) found.push(error)`
72
+ });
73
+ }
74
+ }
75
+ return checks;
76
+ };
77
+ const coercibleTypes = (node) => {
78
+ const type = readKey(node, "type");
79
+ const types = typeof type === "string" ? [type] : Array.isArray(type) ? type : [];
80
+ return types.length > 0 && types.every((entry) => SCALAR_TYPES.has(entry)) ? JSON.stringify(types) : null;
81
+ };
82
+ const walkChild = (walker, key, write) => [
83
+ `run.path.push(${key})`,
84
+ `const n = ${walker}(c, run)`,
85
+ `run.path.pop()`,
86
+ `if (n !== c) ${write}`
87
+ ];
88
+ const inlineTypeId = (node, typeSuffix) => {
89
+ const branchName = (key) => {
90
+ const branch = readKey(node, key);
91
+ if (typeof branch !== "object" || branch === null)
92
+ return null;
93
+ const ref = readKey(branch, "$ref");
94
+ return typeof ref === "string" && Object.keys(branch).length === 1 ? refToName(ref, typeSuffix) : null;
95
+ };
96
+ const thenName = branchName("then");
97
+ const elseName = branchName("else");
98
+ const text = JSON.stringify(node);
99
+ let hash = 2166136261;
100
+ for (let i = 0; i < text.length; i++)
101
+ hash = Math.imul(hash ^ text.charCodeAt(i), 16777619);
102
+ const digest = (hash >>> 0).toString(16).padStart(8, "0");
103
+ return thenName !== null && elseName !== null ? `${thenName}|${elseName}#${digest}` : `inline#${digest}`;
104
+ };
105
+ const falseWalker = (ctx, walkers) => {
106
+ const existing = walkers.get(false);
107
+ if (existing)
108
+ return existing;
109
+ const drop = ctx.lookupFor(false);
110
+ const name = "_walkFalse";
111
+ walkers.set(false, name);
112
+ ctx.repair.helpers.push([
113
+ `const ${name}: RepairWalk = (v, run) =>`,
114
+ ` walkRepair(run, v, [walkError(run, 'false schema', 'boolean schema is false', {})], ${drop}, () => false, null)`
115
+ ].join("\n"));
116
+ return name;
117
+ };
118
+ const emitWalk = (schema, ctx, walkers, isRoot) => {
119
+ if (schema === false)
120
+ return falseWalker(ctx, walkers);
121
+ if (!isSchemaObject(schema))
122
+ return null;
123
+ if (walkers.has(schema))
124
+ return walkers.get(schema) ?? null;
125
+ const node = schema;
126
+ const helpers = ctx.repair.helpers;
127
+ const ref = readKey(node, "$ref");
128
+ const refWalker = typeof ref === "string" ? repairWalkNameFor(refToName(ref, ctx.repair.typeSuffix)) : null;
129
+ const checks = ownChecks(node, ctx);
130
+ const properties = schemaMap(node, "properties") ?? {};
131
+ const required = readKey(node, "required")?.filter((key) => typeof key === "string");
132
+ const hasObjectPart = Object.keys(properties).length > 0 || required !== void 0 && required.length > 0 || readKey(node, "additionalProperties") !== void 0 || readKey(node, "patternProperties") !== void 0;
133
+ const items = subschema(node, "items");
134
+ const hasConditional = readKey(node, "if") !== void 0 || Array.isArray(readKey(node, "allOf"));
135
+ const ownsObjectType = readKey(node, "type") === "object";
136
+ if (refWalker !== null && checks.length === 0 && !hasObjectPart && items === void 0 && !hasConditional && !isRoot) {
137
+ walkers.set(schema, refWalker);
138
+ return refWalker;
139
+ }
140
+ const id = ctx.repair.next();
141
+ const name = `_walk${id}`;
142
+ walkers.set(schema, name);
143
+ const typeId = JSON.stringify(isRoot ? ctx.typeName : inlineTypeId(node, ctx.repair.typeSuffix));
144
+ const lookup = ctx.lookupFor(schema);
145
+ const body = [];
146
+ if (refWalker !== null) {
147
+ body.push(`v = ${refWalker}(v, run)`, `if (v === REPAIR_DROP) return v`);
148
+ }
149
+ if (checks.length > 0) {
150
+ const ok = checks.map((check) => check.condition).join(" && ");
151
+ const okName = `_walkOk${id}`;
152
+ const fixName = `_walkFix${id}`;
153
+ helpers.push(`const ${okName} = (v: unknown, ${/\brun\b/.test(ok) ? "run" : "_run"}: RepairRun): boolean => ${ok}`);
154
+ helpers.push([
155
+ `const ${fixName} = (v: unknown, run: RepairRun): unknown => {`,
156
+ ` const found: ValidationError[] = []`,
157
+ ...checks.map((check) => ` if (!(${check.condition})) ${check.report}`),
158
+ ` return walkRepair(run, v, found, ${lookup ?? "null"}, ${okName}, ${coercibleTypes(node) ?? "null"})`,
159
+ `}`
160
+ ].join("\n"));
161
+ body.push(`if (!(${ok})) {`, ` const fixed = ${fixName}(v, run)`, ` if (fixed !== v) return fixed === REPAIR_DROP ? fixed : walkVerified(run, fixed, ${name})`, `}`);
162
+ }
163
+ const container = [];
164
+ if (hasObjectPart || ownsObjectType)
165
+ container.push(...objectPart(node, properties, required ?? [], lookup, id, ctx, walkers));
166
+ if (items !== void 0)
167
+ container.push(...arrayPart(items, ctx, walkers));
168
+ const conditional = conditionalPart(node, name, typeId, lookup, ctx, walkers);
169
+ if (container.length > 0) {
170
+ body.push(`if (++run.work > run.maxWork || run.path.length > run.maxDepth) return walkHalt(run, v)`);
171
+ body.push(...container);
172
+ }
173
+ body.push(...conditional);
174
+ if (body.length === 0) {
175
+ walkers.set(schema, null);
176
+ return null;
177
+ }
178
+ const bodyName = container.length > 0 ? `_walkBody${id}` : name;
179
+ helpers.push([`const ${bodyName}: RepairWalk = (v, run) => {`, ...body.map((line) => ` ${line}`), ` return v`, `}`].join("\n"));
180
+ if (container.length > 0) {
181
+ helpers.push(`const ${name}: RepairWalk = (v, run) => (run.memo === null ? ${bodyName}(v, run) : walkMemo(run, v, ${bodyName}))`);
182
+ }
183
+ return name;
184
+ };
185
+ const objectPart = (node, properties, required, lookup, id, ctx, walkers) => {
186
+ const inner = [];
187
+ const write = (key) => `o = walkSet(run, o, o !== src, ${key}, n)`;
188
+ const requiredSet = new Set(required);
189
+ for (const [key, child] of Object.entries(properties)) {
190
+ const walker = emitWalk(child, ctx, walkers, false);
191
+ const quoted = JSON.stringify(key);
192
+ if (requiredSet.has(key)) {
193
+ if (walker === null) {
194
+ inner.push(`if (!Object.hasOwn(o, ${quoted})) o = walkMissing(run, o, o !== src, ${quoted}, ${lookup ?? "null"}, null)`);
195
+ } else {
196
+ inner.push(`if (Object.hasOwn(o, ${quoted})) {`, ` const c = o[${quoted}]`, ...walkChild(walker, quoted, write(quoted)).map((line) => ` ${line}`), `} else o = walkMissing(run, o, o !== src, ${quoted}, ${lookup ?? "null"}, ${walker})`);
197
+ }
198
+ } else if (walker !== null) {
199
+ inner.push(`{`, ` const c = o[${quoted}]`, ` if (c !== undefined && Object.hasOwn(o, ${quoted})) {`, ...walkChild(walker, quoted, write(quoted)).map((line) => ` ${line}`), ` }`, `}`);
200
+ }
201
+ }
202
+ for (const key of required) {
203
+ if (Object.hasOwn(properties, key))
204
+ continue;
205
+ const quoted = JSON.stringify(key);
206
+ inner.push(`if (!Object.hasOwn(o, ${quoted})) o = walkMissing(run, o, o !== src, ${quoted}, ${lookup ?? "null"}, null)`);
207
+ }
208
+ inner.push(...mapPart(node, properties, id, ctx, walkers));
209
+ return [
210
+ `if (typeof v === 'object' && v !== null && !Array.isArray(v)) {`,
211
+ ` const src = v as Record<string, unknown>`,
212
+ ` let o = src`,
213
+ ` if (run.plain) o = walkProto(run, o, false)`,
214
+ ...inner.map((line) => ` ${line}`),
215
+ ` v = o`,
216
+ `}`
217
+ ];
218
+ };
219
+ const mapPart = (node, properties, id, ctx, walkers) => {
220
+ const patterns = Object.entries(schemaMap(node, "patternProperties") ?? {}).map(([source, child]) => ({
221
+ source,
222
+ walker: emitWalk(child, ctx, walkers, false)
223
+ }));
224
+ const additional = subschema(node, "additionalProperties");
225
+ const additionalWalker = additional === void 0 || additional === false ? null : emitWalk(additional, ctx, walkers, false);
226
+ const closed = additional === false;
227
+ const patternsMatter = patterns.some((pattern) => pattern.walker !== null) || closed || additionalWalker !== null;
228
+ if (!patternsMatter)
229
+ return [];
230
+ const helpers = ctx.repair.helpers;
231
+ const lines = [`for (const k in src) {`, ` if (!Object.hasOwn(src, k)) continue`];
232
+ const declared = Object.keys(properties);
233
+ if (declared.length > 0) {
234
+ const setName = `_walkDeclared${id}`;
235
+ helpers.push(`const ${setName} = new Set(${JSON.stringify(declared)})`);
236
+ lines.push(` if (${setName}.has(k)) continue`);
237
+ }
238
+ lines.push(` if (o !== src && !Object.hasOwn(o, k)) continue`);
239
+ const needsMatched = patterns.length > 0 && (closed || additionalWalker !== null);
240
+ if (needsMatched)
241
+ lines.push(` let matched = false`);
242
+ patterns.forEach((pattern, index) => {
243
+ const regexName = `_walkPattern${id}_${index}`;
244
+ helpers.push(`const ${regexName} = new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(regexFlagsFor(pattern.source))})`);
245
+ const walk = pattern.walker === null ? [] : [
246
+ `const c = o[k]`,
247
+ ...walkChild(pattern.walker, "k", `{`),
248
+ ` o = walkSet(run, o, o !== src, k, n)`,
249
+ ` if (n === REPAIR_DROP) continue`,
250
+ `}`
251
+ ];
252
+ if (!needsMatched && walk.length === 0)
253
+ return;
254
+ lines.push(` if (${regexName}.test(k)) {`);
255
+ if (needsMatched)
256
+ lines.push(` matched = true`);
257
+ lines.push(...walk.map((line) => ` ${line}`), ` }`);
258
+ });
259
+ const otherwise = needsMatched ? `if (!matched) ` : "";
260
+ if (closed) {
261
+ lines.push(` ${otherwise}o = walkExtra(run, o, o !== src, k)`);
262
+ } else if (additionalWalker !== null) {
263
+ lines.push(` ${otherwise}{`, ` const c = o[k]`, ...walkChild(additionalWalker, "k", `o = walkSet(run, o, o !== src, k, n)`).map((line) => ` ${line}`), ` }`);
264
+ }
265
+ lines.push(`}`);
266
+ return lines;
267
+ };
268
+ const arrayPart = (items, ctx, walkers) => {
269
+ const walker = emitWalk(items, ctx, walkers, false);
270
+ if (walker === null)
271
+ return [];
272
+ return [
273
+ `if (Array.isArray(v)) {`,
274
+ ` const src: unknown[] = v`,
275
+ ` let a = src`,
276
+ ` let dropped = false`,
277
+ ` for (let i = 0; i < src.length; i++) {`,
278
+ ` const c = src[i]`,
279
+ ...walkChild(walker, "i", `{`).map((line) => ` ${line}`),
280
+ ` if (n === REPAIR_DROP) dropped = true`,
281
+ ` a = walkSetItem(run, a, a !== src, i, n)`,
282
+ ` }`,
283
+ ` }`,
284
+ ` v = dropped ? walkCompact(a) : a`,
285
+ `}`
286
+ ];
287
+ };
288
+ const conditionalPart = (node, self, typeId, lookup, ctx, walkers) => {
289
+ const lines = [];
290
+ const leave = lookup === null ? "return v" : "return walkUnscope(run, v)";
291
+ const branch = (schema) => {
292
+ if (schema === void 0)
293
+ return [];
294
+ const walker = emitWalk(schema, ctx, walkers, false);
295
+ const out = walker === null ? [] : [`v = ${walker}(v, run)`, `if (v === REPAIR_DROP) ${leave}`];
296
+ if (isReferenceSchema(schema, ctx.rootSchema))
297
+ out.push(`walkRef(run, v, ${self}, ${typeId})`);
298
+ return out;
299
+ };
300
+ const ifSchema = subschema(node, "if");
301
+ const thenSchema = subschema(node, "then");
302
+ const elseSchema = subschema(node, "else");
303
+ if (ifSchema !== void 0 && (thenSchema !== void 0 || elseSchema !== void 0)) {
304
+ const test = namedTest(ifSchema, ctx.repair);
305
+ if (test === true)
306
+ lines.push(...branch(thenSchema));
307
+ else if (test === false)
308
+ lines.push(...branch(elseSchema));
309
+ else {
310
+ const thenLines = branch(thenSchema);
311
+ const elseLines = branch(elseSchema);
312
+ if (thenLines.length > 0) {
313
+ lines.push(`if (${test}(v)) {`, ...thenLines.map((line) => ` ${line}`));
314
+ if (elseLines.length > 0)
315
+ lines.push(`} else {`, ...elseLines.map((line) => ` ${line}`));
316
+ lines.push(`}`);
317
+ } else if (elseLines.length > 0) {
318
+ lines.push(`if (!${test}(v)) {`, ...elseLines.map((line) => ` ${line}`), `}`);
319
+ }
320
+ }
321
+ }
322
+ const allOf = readKey(node, "allOf");
323
+ if (Array.isArray(allOf)) {
324
+ for (const entry of allOf)
325
+ lines.push(...branch(entry));
326
+ }
327
+ if (lines.length === 0 || lookup === null)
328
+ return lines;
329
+ return [`walkScope(run, ${lookup})`, ...lines, `walkUnscope(run, v)`];
330
+ };
331
+ const emitRepairWalk = (schema, ctx) => emitWalk(schema, ctx, /* @__PURE__ */ new Map(), true);
332
+ export {
333
+ emitRepairWalk,
334
+ repairWalkNameFor
335
+ };
@@ -26,7 +26,12 @@ export declare const generateBooleanGuard: (schema: JSONSchema, typeName: string
26
26
  * fallback runs the whole validator and builds every error on the way to `false`,
27
27
  * so there the coercer walks first instead.
28
28
  */
29
- export declare const generateBooleanGuardSource: (schema: JSONSchema, typeName: string, unknownKeys?: UnknownKeysStrategy, formats?: ReadonlySet<string>) => {
29
+ export declare const generateBooleanGuardSource: (schema: JSONSchema, typeName: string, unknownKeys?: UnknownKeysStrategy, formats?: ReadonlySet<string>, refs?: {
30
+ /** The suffix `$ref`-derived names carry, so a ref's guard is called by its real name. */
31
+ readonly suffix?: string;
32
+ /** The whole document, for a ref that resolves inside it. */
33
+ readonly rootSchema?: Record<string, unknown>;
34
+ }) => {
30
35
  code: string;
31
36
  standalone: boolean;
32
37
  };
@@ -96,11 +101,24 @@ export declare const createSubschemaMatcher: (suffix: string, rootSchema: Record
96
101
  * tested, which on a union-heavy document was most of what coercing it cost.
97
102
  */
98
103
  test: (sub: JSONSchema) => boolean | string;
104
+ /**
105
+ * The early-return body of {@link test}, or `null` where `test` would have to
106
+ * fall back to its buffered expression — a guard then calls the validator.
107
+ */
108
+ guardBody: (sub: JSONSchema) => boolean | string | null;
99
109
  /**
100
110
  * The same test as a named function, hoisted with the rest and shared with
101
111
  * every other test of the same subschema in this file — or a constant, or
102
112
  * `null` for a body only the buffered form of {@link test} can express.
103
113
  */
104
114
  named: (sub: JSONSchema) => boolean | string | null;
115
+ /**
116
+ * The name of a hoisted function that says why a value fails `sub` — every
117
+ * error `validateX` would report for it, rooted at the path it is handed — or
118
+ * `null` when `sub` has no checks to explain anything with. The repair walk
119
+ * calls it once a position has already failed, so it costs nothing on valid
120
+ * input.
121
+ */
122
+ explain: (sub: JSONSchema) => string | null;
105
123
  declarations: (text: string) => string[];
106
124
  };
@@ -20,6 +20,7 @@ const checkerName = (typeName) => `check${typeName}`;
20
20
  const propertyRead = (objVar, key) => safeAccessor(objVar, key);
21
21
  const PROTOTYPE_MEMBERS = new Set(Object.getOwnPropertyNames(Object.prototype));
22
22
  const protoLocalName = (key, depth) => `_own${depth}_${key}`;
23
+ const propertyLocalName = (key, index, depth) => PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, depth) : `_v${depth}_${index}`;
23
24
  const hasOwnCheck = (objVar, key) => {
24
25
  if (PROTOTYPE_MEMBERS.has(key))
25
26
  return `Object.hasOwn(${objVar}, ${JSON.stringify(key)})`;
@@ -214,7 +215,7 @@ const generateMissingRequiredChecks = (schema, ctx) => {
214
215
  }
215
216
  return lines;
216
217
  };
217
- const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
218
+ const generatePropertyChecks = (key, index, propSchema, isRequired, suffix, ctx) => {
218
219
  if (propSchema === false) {
219
220
  const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
220
221
  const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
@@ -234,18 +235,18 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
234
235
  ` }`
235
236
  ];
236
237
  }
237
- const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
238
+ const raw = propertyLocalName(key, index, ctx.depth);
238
239
  const lines = [
239
- ...generatePropertyCheckLines(key, propSchema, isRequired, suffix, ctx),
240
+ ...generatePropertyCheckLines(key, raw, propSchema, isRequired, suffix, ctx),
240
241
  // `unevaluated*` is a sibling of everything else the property declares, and
241
242
  // several of the branches above return early, so it is appended here rather
242
243
  // than emitted inside them. The checks carry their own runtime type guard, so
243
244
  // an absent optional property is inert and needs no presence gate.
244
245
  ...generateUnevaluatedChecks(raw, `\`${ctx.pathPrefix}/${pointerSegment(key)}\``, propSchema, suffix, ctx)
245
246
  ];
246
- if (lines.length === 0 || !PROTOTYPE_MEMBERS.has(key))
247
+ if (!readsBinding(raw, lines.join("\n")))
247
248
  return lines;
248
- return [` const ${protoLocalName(key, ctx.depth)} = ${propertyRead(ctx.objVar, key)}`, ...lines];
249
+ return [` const ${raw} = ${propertyRead(ctx.objVar, key)}`, ...lines];
249
250
  };
250
251
  const singleIfBlock = (lines) => {
251
252
  if (lines.length < 3)
@@ -267,7 +268,11 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
267
268
  const target = refToName(schema.$ref, suffix);
268
269
  const delegate = ctx.sink === FAIL_FAST_SINK ? [` const _r = ${checkerName(target)}(${raw}, ${path})`, ` if (_r !== true) return _r`] : ctx.sink === MATCH_BOOL_SINK ? [` if (!${ctx.refGuard(target)}(${raw})) return false`] : [
269
270
  ` const _r = ${validatorName(target)}(${raw}, ${path})`,
270
- ` if (_r !== true) ${ctx.sink}.push(..._r.errors)`
271
+ // A loop rather than a spread: spreading passes every error as an
272
+ // argument, and a document with tens of thousands of errors under
273
+ // one reference overflows the engine's argument limit with a
274
+ // RangeError instead of returning them.
275
+ ` if (_r !== true) for (const _e of _r.errors) ${ctx.sink}.push(_e)`
271
276
  ];
272
277
  if (presence === "")
273
278
  lines.push(` {`, ...delegate.map((line) => ` ${line}`), ` }`);
@@ -330,7 +335,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
330
335
  }
331
336
  return lines;
332
337
  };
333
- const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) => {
338
+ const generatePropertyCheckLines = (key, raw, propSchema, isRequired, suffix, ctx) => {
334
339
  const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
335
340
  const missing = [
336
341
  ` if (${missingCheck(ctx.objVar, key)}) {`,
@@ -341,7 +346,6 @@ const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) =>
341
346
  return [...missing, ` }`];
342
347
  return [];
343
348
  }
344
- const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
345
349
  const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
346
350
  if (isRequired) {
347
351
  const valueLines = generateKeywordChecks(key, raw, path, propSchema, suffix, ctx, "");
@@ -425,7 +429,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
425
429
  if (hasRef(itemSchema) && !declaresKeywordOutside(itemSchema, ["$ref"])) {
426
430
  const failFast = ctx.sink === FAIL_FAST_SINK;
427
431
  const target = refToName(itemSchema.$ref, suffix);
428
- const onFailure = failFast ? "return _ir" : `${ctx.sink}.push(..._ir.errors)`;
432
+ const onFailure = failFast ? "return _ir" : `for (const _e of _ir.errors) ${ctx.sink}.push(_e)`;
429
433
  lines.push(` if (Array.isArray(${raw})) {`);
430
434
  lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
431
435
  if (ctx.sink === MATCH_BOOL_SINK) {
@@ -864,8 +868,8 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
864
868
  const required = new Set(hasRequired(propSchema) ? propSchema.required : []);
865
869
  const properties = hasProperties(propSchema) ? propSchema.properties : {};
866
870
  const innerLines = [];
867
- for (const [childKey, childSchema] of Object.entries(properties)) {
868
- innerLines.push(...generatePropertyChecks(childKey, childSchema, required.has(childKey), suffix, child));
871
+ for (const [index, [childKey, childSchema]] of Object.entries(properties).entries()) {
872
+ innerLines.push(...generatePropertyChecks(childKey, index, childSchema, required.has(childKey), suffix, child));
869
873
  }
870
874
  innerLines.push(...generateMissingRequiredChecks(propSchema, child));
871
875
  innerLines.push(...generatePatternAndAdditionalChecks(propSchema, suffix, child));
@@ -1193,8 +1197,8 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema, unknownKe
1193
1197
  const properties = hasProperties(schema) ? schema.properties : {};
1194
1198
  const ctx = createRootContext(rootSchema, formats, branchErrors, failFast);
1195
1199
  const propertyLines = [];
1196
- for (const [key, propSchema] of Object.entries(properties)) {
1197
- const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix, ctx);
1200
+ for (const [index, [key, propSchema]] of Object.entries(properties).entries()) {
1201
+ const checks = generatePropertyChecks(key, index, propSchema, required.has(key), suffix, ctx);
1198
1202
  if (checks.length > 0) {
1199
1203
  propertyLines.push(...checks);
1200
1204
  }
@@ -1467,18 +1471,38 @@ const typeDescribesEveryAcceptedValue = (schema) => {
1467
1471
  return !IMPLICIT_OBJECT_KEYWORDS.some((keyword) => declaresKey(s, keyword));
1468
1472
  };
1469
1473
  const generateBooleanGuard = (schema, typeName, _suffix = "", unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => generateBooleanGuardSource(schema, typeName, unknownKeys, formats).code;
1470
- const generateBooleanGuardSource = (schema, typeName, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => {
1474
+ const generateBooleanGuardSource = (schema, typeName, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS, refs = {}) => {
1471
1475
  const name = guardName(typeName);
1472
1476
  const returns = typeDescribesEveryAcceptedValue(rewriteNullable(schema)) ? `input is ${typeName}` : "boolean";
1473
- const fallback = `export const ${name} = (input: unknown): ${returns} => ${validatorName(typeName)}(input) === true`;
1477
+ const fallback = () => {
1478
+ const matcher = createSubschemaMatcher(refs.suffix ?? "", refs.rootSchema ?? (isSchemaObject(schema) ? schema : void 0), formats, "_guard");
1479
+ const body = matcher.guardBody(schema);
1480
+ if (typeof body !== "string" || !readsBinding("input", body)) {
1481
+ return {
1482
+ code: `export const ${name} = (input: unknown): ${returns} => ${validatorName(typeName)}(input) === true`,
1483
+ standalone: false
1484
+ };
1485
+ }
1486
+ const declarations = matcher.declarations(body);
1487
+ const code = [
1488
+ ...declarations.length > 0 ? [declarations.join("\n\n"), ""] : [],
1489
+ `export const ${name} = (input: unknown): ${returns} => {`,
1490
+ // Some checks build a path for a key or an index before they know whether
1491
+ // they fail. Nothing reports it here, so it is rooted at nothing.
1492
+ ...readsBinding("_path", body) ? [` const _path = ''`] : [],
1493
+ body,
1494
+ `}`
1495
+ ].join("\n");
1496
+ return { code, standalone: true };
1497
+ };
1474
1498
  const rewritten = rewriteNullable(schema);
1475
1499
  const ctx = createGuardContext(unknownKeys, formats);
1476
1500
  if (declaresObjectType(rewritten)) {
1477
1501
  if (!objectRootIsSelfContained(rewritten))
1478
- return { code: fallback, standalone: false };
1502
+ return fallback();
1479
1503
  const block = booleanObjectParts(rewritten, "input", "obj", ctx);
1480
1504
  if (block === null)
1481
- return { code: fallback, standalone: false };
1505
+ return fallback();
1482
1506
  const body = isFlatGuardBlock(block) ? [` return (`, block.conditions.map((part) => ` ${part}`).join(" &&\n"), ` )`] : [...renderGuardBlock(block, "return false", " ", "lines", unknownKeys), ` return true`];
1483
1507
  const code = [
1484
1508
  `export const ${name} = (input: unknown): ${returns} => {`,
@@ -1492,7 +1516,7 @@ const generateBooleanGuardSource = (schema, typeName, unknownKeys = DEFAULT_UNKN
1492
1516
  }
1493
1517
  const expr = booleanLeafExpr(rewritten, "input", ctx);
1494
1518
  if (expr === null)
1495
- return { code: fallback, standalone: false };
1519
+ return fallback();
1496
1520
  return { code: `export const ${name} = (input: unknown): ${returns} => ${expr}`, standalone: true };
1497
1521
  };
1498
1522
  const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats, branchErrors, failFast) => {
@@ -1790,12 +1814,23 @@ const createSubschemaMatcher = (suffix, rootSchema, formats, hoistNamespace, ref
1790
1814
  return true`;
1791
1815
  return ` return ${generateMatchesExpr("input", rewritten, suffix, ctx, true)}`;
1792
1816
  },
1817
+ guardBody: (sub) => {
1818
+ if (sub === false)
1819
+ return false;
1820
+ const state = { on: false, blocked: false };
1821
+ const checks = generateValueChecks("", "input", "`${_path}`", rewriteNullable(sub), suffix, { ...ctx, sink: MATCH_BOOL_SINK, failFast: state, branchErrors: false }, true);
1822
+ if (checks.length === 0)
1823
+ return true;
1824
+ return state.blocked ? null : `${checks.join("\n")}
1825
+ return true`;
1826
+ },
1793
1827
  named: (sub) => {
1794
1828
  if (sub === false)
1795
1829
  return false;
1796
1830
  const name = namedMatcher(rewriteNullable(sub), suffix, ctx);
1797
1831
  return name === "true" ? true : name;
1798
1832
  },
1833
+ explain: (sub) => namedBranchErrors(rewriteNullable(sub), suffix, ctx),
1799
1834
  declarations: (text) => keptHoisted(ctx.hoisted, text)
1800
1835
  };
1801
1836
  };
@@ -0,0 +1,39 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ import type { createSubschemaMatcher } from './generate-validator-function.js';
3
+ /**
4
+ * What the repairing half of one file is built from: its declarations, its
5
+ * numbering, and the tests the lookup and the walk share. One context per file,
6
+ * so the lookup and the walk emitted into it can never name two things alike.
7
+ */
8
+ export type RepairContext = {
9
+ readonly typeSuffix: string;
10
+ /** Declarations, in the order they must appear. */
11
+ readonly helpers: string[];
12
+ /** Supplies the next unique suffix for a declaration. */
13
+ next: () => number;
14
+ /** The validator's own yes/no tests, for `propertyNames`, an `if`, and the walk. */
15
+ readonly matcher: ReturnType<typeof createSubschemaMatcher>;
16
+ /**
17
+ * The lookup already emitted for a subschema, by identity. The walk asks for
18
+ * the lookup of every node it checks, and a node must answer with the one
19
+ * function rather than a second copy of it.
20
+ */
21
+ readonly lookups: Map<unknown, string | null>;
22
+ /** The yes/no test already emitted for a subschema, by identity. */
23
+ readonly tests: Map<unknown, boolean | string>;
24
+ /** The shared lookup a `false` schema answers with, once something needs it. */
25
+ drop: string | null;
26
+ };
27
+ /**
28
+ * A named yes/no test of a value against `sub`, `true` when every value passes,
29
+ * or `false` when none does.
30
+ *
31
+ * It is the validator's own test, so the lookup, the walk and `validateX` agree
32
+ * on which branch a document takes. A body only the buffered form can express
33
+ * gets a function of its own here, as the coercer does it.
34
+ */
35
+ export declare const namedTest: (sub: JSONSchema, ctx: RepairContext) => boolean | string;
36
+ /** A schema map keyword (`properties`, …), read as own properties only. */
37
+ export declare const schemaMap: (schema: Record<string, unknown>, key: string) => Record<string, JSONSchema> | undefined;
38
+ /** A subschema keyword (`then`, `items`, …), when it holds a schema at all. */
39
+ export declare const subschema: (schema: Record<string, unknown>, key: string) => JSONSchema | undefined;
@@ -0,0 +1,35 @@
1
+ import { readKey } from "@amritk/helpers/read-key";
2
+ const namedTest = (sub, ctx) => {
3
+ const known = ctx.tests.get(sub);
4
+ if (known !== void 0)
5
+ return known;
6
+ const named = ctx.matcher.named(sub);
7
+ if (named !== null) {
8
+ ctx.tests.set(sub, named);
9
+ return named;
10
+ }
11
+ const body = ctx.matcher.test(sub);
12
+ if (typeof body !== "string") {
13
+ ctx.tests.set(sub, body);
14
+ return body;
15
+ }
16
+ const name = `_repairTest${ctx.next()}`;
17
+ const parameter = /\binput\b/.test(body) ? "input" : "_input";
18
+ const path = /\b_path\b/.test(body) ? [` const _path = ''`] : [];
19
+ ctx.helpers.push([`const ${name} = (${parameter}: unknown): boolean => {`, ...path, body, `}`].join("\n"));
20
+ ctx.tests.set(sub, name);
21
+ return name;
22
+ };
23
+ const schemaMap = (schema, key) => {
24
+ const value = readKey(schema, key);
25
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
26
+ };
27
+ const subschema = (schema, key) => {
28
+ const value = readKey(schema, key);
29
+ return typeof value === "boolean" || typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
30
+ };
31
+ export {
32
+ namedTest,
33
+ schemaMap,
34
+ subschema
35
+ };