@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.
- package/AI.md +11 -0
- package/README.md +50 -3
- package/dist/validators/generators/build-schema.d.ts +1 -1
- package/dist/validators/generators/build-schema.js +32 -217
- package/dist/validators/generators/collect-validator-imports.d.ts +7 -0
- package/dist/validators/generators/collect-validator-imports.js +6 -3
- package/dist/validators/generators/generate-files.d.ts +6 -0
- package/dist/validators/generators/generate-files.js +34 -9
- package/dist/validators/generators/generate-repair-function.d.ts +13 -17
- package/dist/validators/generators/generate-repair-function.js +224 -118
- package/dist/validators/generators/generate-repair-walk.d.ts +24 -0
- package/dist/validators/generators/generate-repair-walk.js +335 -0
- package/dist/validators/generators/generate-validator-function.d.ts +19 -1
- package/dist/validators/generators/generate-validator-function.js +53 -18
- package/dist/validators/generators/repair-context.d.ts +39 -0
- package/dist/validators/generators/repair-context.js +35 -0
- package/dist/validators/generators/repair-runtime.d.ts +25 -0
- package/dist/validators/generators/repair-runtime.js +1071 -0
- package/dist/validators/generators/repair-walkable.d.ts +33 -0
- package/dist/validators/generators/repair-walkable.js +148 -0
- package/package.json +1 -1
|
@@ -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 =
|
|
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.
|
|
247
|
+
if (!readsBinding(raw, lines.join("\n")))
|
|
247
248
|
return lines;
|
|
248
|
-
return [` const ${
|
|
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
|
-
|
|
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" :
|
|
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 =
|
|
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
|
|
1502
|
+
return fallback();
|
|
1479
1503
|
const block = booleanObjectParts(rewritten, "input", "obj", ctx);
|
|
1480
1504
|
if (block === null)
|
|
1481
|
-
return
|
|
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
|
|
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
|
+
};
|