@amritk/validation 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI.md +9 -7
- package/README.md +34 -9
- package/dist/validators/generators/generate-files.js +4 -1
- package/dist/validators/generators/generate-repair-function.d.ts +6 -1
- package/dist/validators/generators/generate-repair-function.js +70 -11
- package/dist/validators/generators/generate-validator-function.js +55 -38
- package/package.json +1 -1
package/AI.md
CHANGED
|
@@ -44,14 +44,16 @@ Every mode is reachable from `mjst` without dropping to the programmatic API:
|
|
|
44
44
|
|:---|:---|
|
|
45
45
|
| `types` | `--types-only` (alone), otherwise always emitted |
|
|
46
46
|
| `guard`, `validate` | `--validators`, or `--validators-only` for no parser |
|
|
47
|
-
| `check` | `--check` |
|
|
48
|
-
| `coerce` | `--coerce` |
|
|
49
|
-
| `repair` | `--repair` (emits `coerceX` too) |
|
|
47
|
+
| `check` | `--validators --check` |
|
|
48
|
+
| `coerce` | `--validators --coerce` |
|
|
49
|
+
| `repair` | `--validators --repair` (emits `coerceX` too) |
|
|
50
50
|
| `parse` | the default on any run that is not `--types-only`/`--validators-only` |
|
|
51
51
|
| `parseStrict` | `--strict` |
|
|
52
52
|
|
|
53
53
|
`--validators-only` is this package's own default (`['types', 'guard', 'validate']`)
|
|
54
54
|
spelled as a flag: everything that judges a document, nothing that builds one.
|
|
55
|
+
`--check`, `--coerce` and `--repair` shape the validator half, so the CLI rejects
|
|
56
|
+
any of them without one of those two flags.
|
|
55
57
|
|
|
56
58
|
## Gotchas — where agents fail
|
|
57
59
|
|
|
@@ -83,10 +85,10 @@ spelled as a flag: everything that judges a document, nothing that builds one.
|
|
|
83
85
|
validateX(v)` pays for both passes and is no faster than `validateX`.
|
|
84
86
|
|
|
85
87
|
8. **It emits each engine's exact bytes.** A single-mode build is byte-identical
|
|
86
|
-
to what
|
|
87
|
-
difference to reason about and no speedup to claim — do not tell a
|
|
88
|
-
faster. What it changes is that the whole matrix comes out as one
|
|
89
|
-
of two, and that a build asking for no validator mode ships no
|
|
88
|
+
to what the engine that owns that mode emits when called directly, so there is
|
|
89
|
+
no runtime difference to reason about and no speedup to claim — do not tell a
|
|
90
|
+
user this is faster. What it changes is that the whole matrix comes out as one
|
|
91
|
+
type instead of two, and that a build asking for no validator mode ships no
|
|
90
92
|
`validation-result.ts`.
|
|
91
93
|
|
|
92
94
|
9. **`coerceX` is not Ajv's `coerceTypes`, on purpose.** It never modifies its
|
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ so no two can disagree about whether a document is valid.
|
|
|
74
74
|
| `types` | — | — | — | — |
|
|
75
75
|
| `guard` | `isX(input): input is X` | — | a boolean | the first problem |
|
|
76
76
|
| `validate` | `validateX(input)` | — | every error | the end |
|
|
77
|
+
| `check` | `checkX(input)` | — | the first error | the first problem |
|
|
77
78
|
| `coerce` | `coerceX(input)` | coerced | every error | the end |
|
|
78
79
|
| `repair` | `repairX(input)` | repaired | repairs **and** errors | the end |
|
|
79
80
|
| `parse` | `parseX(input)` | repaired | nothing | never fails |
|
|
@@ -84,7 +85,9 @@ it stops at the first thing wrong and builds no error object, which on invalid
|
|
|
84
85
|
input is dramatically cheaper than reporting. `validate` is the other half: every
|
|
85
86
|
error, each with a JSON Pointer, the keyword that rejected the value, and that
|
|
86
87
|
keyword's own values. Reach for `guard` when the answer is a branch and
|
|
87
|
-
`validate` when a human or an API client has to be told what to fix.
|
|
88
|
+
`validate` when a human or an API client has to be told what to fix. `check` sits
|
|
89
|
+
between them: `checkX` returns the same result type as `validateX`, holding only
|
|
90
|
+
the error `validateX` would have reported first, and walks no further.
|
|
88
91
|
|
|
89
92
|
**Coerce and repair, with or without the errors.** `coerceX` and `repairX` hand
|
|
90
93
|
back the value *and* the diagnostics; `parseX` hands back only the value and
|
|
@@ -92,6 +95,14 @@ never fails. Coercion moves a value that is already right but written in the
|
|
|
92
95
|
wrong type; repair also substitutes a value the schema supplies for one that
|
|
93
96
|
cannot be coerced, and reports the validator's own errors as the repairs.
|
|
94
97
|
|
|
98
|
+
That makes `repairX` an autofix tool rather than a stricter validator. It accepts
|
|
99
|
+
documents `validateX` rejects (a missing required name gets one; an unknown
|
|
100
|
+
`enum` value becomes the first member), and accepting only `valid` with an empty
|
|
101
|
+
`repairs` gives exactly the verdicts `coerceX` gives. An empty `repairs` alone is
|
|
102
|
+
not enough: a document nothing could repair comes back with none either, and
|
|
103
|
+
`valid: false`. To reject a bad config, use `coerceX`. To hand back a fixed one,
|
|
104
|
+
use `repairX`.
|
|
105
|
+
|
|
95
106
|
**`parseX` agrees with `coerceX`.** Wherever `coerceX` accepts a document, the
|
|
96
107
|
coercing `parseX` returns the very same value — through a union, an `allOf`, an
|
|
97
108
|
`if`, a `$ref`, recursion included — and it only repairs what `coerceX` would
|
|
@@ -119,11 +130,12 @@ schemas with `anyOf`, `oneOf`, `allOf` and `if`/`then`/`else` checks every
|
|
|
119
130
|
accepted document against an Ajv that does not coerce. Where the two differ, it
|
|
120
131
|
is on purpose:
|
|
121
132
|
|
|
122
|
-
- **`null` is never coerced**, in either direction
|
|
123
|
-
|
|
124
|
-
`
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
- **`null` is never coerced**, in either direction, and it is the difference
|
|
134
|
+
you will meet most: on a real config corpus it was nearly every case where Ajv
|
|
135
|
+
accepted and `coerceX` did not. Ajv reads `null` as `""`, `0` or `false` for a
|
|
136
|
+
string, number or boolean field, and reads those back as `null`. A `null`
|
|
137
|
+
usually means "not set", and turning it into a real value erases that. Here it
|
|
138
|
+
goes to the validator as written, and the validator rejects it.
|
|
127
139
|
- **Only clean numerals become numbers.** `" "`, `" 1 "`, `"0x10"`, `"Infinity"`
|
|
128
140
|
and `"1."` are rejected. Ajv turns them into `0`, `1`, `16`, a value JSON
|
|
129
141
|
cannot hold, and `1`. `"007"` and `"1e3"` are still coerced.
|
|
@@ -146,8 +158,8 @@ is on purpose:
|
|
|
146
158
|
document has to clone it first. `coerceX` returns a new value that shares
|
|
147
159
|
everything it did not touch, or the input itself when nothing needed coercing.
|
|
148
160
|
|
|
149
|
-
**It is faster.** Against Ajv cloning first, so
|
|
150
|
-
alone (`bun run bench:validators:coerce`, or
|
|
161
|
+
**It is faster where the input has to survive.** Against Ajv cloning first, so
|
|
162
|
+
both leave the caller's document alone (`bun run bench:validators:coerce`, or
|
|
151
163
|
`bench:validators:coerce:node`):
|
|
152
164
|
|
|
153
165
|
| schema | runtime | valid input | needs coercing | cannot be coerced |
|
|
@@ -166,7 +178,11 @@ each engine timed in its own process (Bun 1.3.11, Node 22.22, Linux x64). A
|
|
|
166
178
|
valid document is the common case and the widest gap: where `isX` is a
|
|
167
179
|
standalone guard, `coerceX` answers it with that guard and hands the input
|
|
168
180
|
straight back, without walking it. On valid input Ajv rewrites nothing, so it can
|
|
169
|
-
also be timed without the clone; `coerceX` is still ahead there
|
|
181
|
+
also be timed without the clone; `coerceX` is still ahead there on these
|
|
182
|
+
schemas, by 1.2–3.3×, but not on every one: on a large, union-heavy document
|
|
183
|
+
that the caller lets Ajv coerce in place, Ajv can come out modestly ahead. The
|
|
184
|
+
gap above is the clone, so it is real exactly where the caller cannot let Ajv
|
|
185
|
+
mutate its input.
|
|
170
186
|
The config case is the shape `--coerce` exists for: every coercible scalar sits
|
|
171
187
|
inside a union reached through `$ref`, and every union branch is tested by a
|
|
172
188
|
named function rather than a closure built per call.</sub>
|
|
@@ -196,10 +212,17 @@ holding it (`/readme`). `params.additionalProperty` names the key in both.
|
|
|
196
212
|
| `options.readonly` | `boolean` | `false` | Emit `readonly` type members. |
|
|
197
213
|
| `options.caseInsensitive` | `boolean` | `false` | Normalize a mis-cased string onto an `enum` member it matches. |
|
|
198
214
|
| `options.helpersMode` | `'package' \| 'embedded'` | `'package'` | Where the parser half gets its runtime helpers. |
|
|
215
|
+
| `options.helpersImportPrefix` | `string` | `'./'` | Prefix on emitted-helper import specifiers, when `helpersMode` is `'embedded'`. |
|
|
199
216
|
| `options.extensions` | `SchemaExtensions` | — | Schema extensions, passed through to the parser half. |
|
|
217
|
+
| `options.importExt` | `'js' \| 'ts'` | `'js'` | Extension on every emitted relative import. `'ts'` emits the on-disk paths, so the output runs under Node's type stripping with no build step. |
|
|
218
|
+
| `options.logWarnings` | `boolean` | `false` | Print the parser half's warnings about the schema, such as a keyword it cannot enforce. |
|
|
200
219
|
|
|
201
220
|
Returns: `Promise<GeneratedFile[]>` where `GeneratedFile = { filename: string; content: string }`.
|
|
202
221
|
|
|
222
|
+
The package also exports `ALL_MODES` (every mode but `parseStrict`, for a caller
|
|
223
|
+
who wants the lot) and the types `Mode`, `GenerateOptions`, `GeneratedFile` and
|
|
224
|
+
`ImportExtension`.
|
|
225
|
+
|
|
203
226
|
---
|
|
204
227
|
|
|
205
228
|
## Output layout
|
|
@@ -208,6 +231,8 @@ Returns: `Promise<GeneratedFile[]>` where `GeneratedFile = { filename: string; c
|
|
|
208
231
|
config.ts the type, plus isX / validateX / coerceX / repairX
|
|
209
232
|
config.parse.ts parseX, importing the type from ./config.js
|
|
210
233
|
validation-result.ts the shared error and result types, and the runtime helpers
|
|
234
|
+
formats.ts the format checks, only when `formats` enforces one the schema names
|
|
235
|
+
_helpers/ the parser's runtime helpers, only when `helpersMode` is 'embedded'
|
|
211
236
|
index.ts a barrel over all of it
|
|
212
237
|
```
|
|
213
238
|
|
|
@@ -25,7 +25,10 @@ const generateValidatorFile = (schema, typeName, options) => {
|
|
|
25
25
|
formats,
|
|
26
26
|
standaloneGuard
|
|
27
27
|
}).code : "";
|
|
28
|
-
const repairer = options?.repair === true ? generateRepairFunction(schema, typeName, typeSuffix
|
|
28
|
+
const repairer = options?.repair === true ? generateRepairFunction(schema, typeName, typeSuffix, {
|
|
29
|
+
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
|
|
30
|
+
formats
|
|
31
|
+
}).code : "";
|
|
29
32
|
const appended = [checker, coercer, repairer].filter((part) => part !== "");
|
|
30
33
|
const body = validatorFunction + booleanGuard + (appended.length === 0 ? "" : "\n\n" + appended.join("\n\n"));
|
|
31
34
|
const mentions = identifierMentions(typeDefinition + body);
|
|
@@ -22,6 +22,11 @@ export declare const repairLookupNameFor: (ref: string, typeSuffix: string) => s
|
|
|
22
22
|
* then can its own children be judged. A position is repaired at most once, so
|
|
23
23
|
* the loop terminates on progress rather than on the pass cap.
|
|
24
24
|
*/
|
|
25
|
-
export declare const generateRepairFunction: (schema: JSONSchema, typeName: string, typeSuffix?: string
|
|
25
|
+
export declare const generateRepairFunction: (schema: JSONSchema, typeName: string, typeSuffix?: string, options?: {
|
|
26
|
+
/** The whole document, for a `propertyNames` test that reads a `$ref`'s target. */
|
|
27
|
+
readonly rootSchema?: Record<string, unknown>;
|
|
28
|
+
/** The `format` names the validator enforces, so a key test agrees with it. */
|
|
29
|
+
readonly formats?: ReadonlySet<string>;
|
|
30
|
+
}) => {
|
|
26
31
|
code: string;
|
|
27
32
|
};
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
import { regexFlagsFor } from "@amritk/helpers/escape-regex-pattern";
|
|
1
2
|
import { getDefaultValue } from "@amritk/helpers/get-default-value";
|
|
2
3
|
import { readKey } from "@amritk/helpers/read-key";
|
|
3
4
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
4
5
|
import { isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
6
|
+
import { NO_FORMATS } from "./enforced-keywords.js";
|
|
7
|
+
import { createSubschemaMatcher } from "./generate-validator-function.js";
|
|
5
8
|
const repairLookupNameFor = (ref, typeSuffix) => `repair${refToName(ref, typeSuffix)}At`;
|
|
9
|
+
const keyTest = (nameSchema, ctx) => {
|
|
10
|
+
const named = ctx.matcher.named(nameSchema);
|
|
11
|
+
if (named !== null)
|
|
12
|
+
return named;
|
|
13
|
+
const body = ctx.matcher.test(nameSchema);
|
|
14
|
+
if (typeof body !== "string")
|
|
15
|
+
return body;
|
|
16
|
+
const name = `_repairKey${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
|
+
return name;
|
|
21
|
+
};
|
|
6
22
|
const fallbackLiteral = (schema) => {
|
|
7
23
|
const literal = getDefaultValue(schema);
|
|
8
24
|
return literal === "undefined" ? null : literal;
|
|
@@ -21,12 +37,20 @@ const emitWalker = (schema, ctx) => {
|
|
|
21
37
|
const own = fallbackLiteral(schema);
|
|
22
38
|
const properties = schemaMap(node, "properties") ?? {};
|
|
23
39
|
const children = Object.entries(properties).map(([key, child]) => [key, emitWalker(child, ctx)]).filter((entry) => entry[1] !== null);
|
|
40
|
+
const patternProperties = schemaMap(node, "patternProperties") ?? {};
|
|
41
|
+
const patterns = Object.entries(patternProperties).map(([source, child]) => ({
|
|
42
|
+
source,
|
|
43
|
+
walker: emitWalker(child, ctx)
|
|
44
|
+
}));
|
|
45
|
+
const additional = readKey(node, "additionalProperties");
|
|
46
|
+
const additionalWalker = typeof additional === "object" && additional !== null ? emitWalker(additional, ctx) : null;
|
|
47
|
+
const mapped = additionalWalker !== null || patterns.some((pattern) => pattern.walker !== null);
|
|
24
48
|
const prefixItems = readKey(node, "prefixItems");
|
|
25
49
|
const tuple = Array.isArray(prefixItems) ? prefixItems : [];
|
|
26
50
|
const tupleWalkers = tuple.map((item) => emitWalker(item, ctx));
|
|
27
51
|
const items = readKey(node, "items");
|
|
28
52
|
const itemsWalker = typeof items === "object" && items !== null && !Array.isArray(items) ? emitWalker(items, ctx) : null;
|
|
29
|
-
const hasChildren = children.length > 0 || itemsWalker !== null || tupleWalkers.some((walker) => walker !== null);
|
|
53
|
+
const hasChildren = children.length > 0 || mapped || itemsWalker !== null || tupleWalkers.some((walker) => walker !== null);
|
|
30
54
|
if (own === null && !hasChildren)
|
|
31
55
|
return null;
|
|
32
56
|
const id = ctx.next();
|
|
@@ -36,15 +60,28 @@ const emitWalker = (schema, ctx) => {
|
|
|
36
60
|
` if (segments.length === 0) return ${own === null ? "null" : `() => (${own})`}`
|
|
37
61
|
];
|
|
38
62
|
if (hasChildren) {
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
const declared = mapped ? Object.keys(properties) : children.map(([key]) => key);
|
|
64
|
+
const indexed = tupleWalkers.some((walker) => walker !== null) || itemsWalker !== null;
|
|
65
|
+
const arrayKeywords = tuple.length > 0 || typeof items === "object" && items !== null && !Array.isArray(items);
|
|
66
|
+
const indexBlock = indexed || mapped && arrayKeywords;
|
|
67
|
+
const nameSchema = readKey(node, "propertyNames");
|
|
68
|
+
const keyCheck = nameSchema === void 0 ? true : keyTest(nameSchema, ctx);
|
|
69
|
+
const readsHead = declared.length > 0 || indexBlock || mapped && patterns.length > 0 || typeof keyCheck === "string";
|
|
70
|
+
lines.push(` const [${readsHead ? "head" : ""}, ...rest] = segments as [string, ...string[]]`);
|
|
71
|
+
if (keyCheck === false)
|
|
72
|
+
lines.push(` if (rest.length === 0) return null`);
|
|
73
|
+
if (typeof keyCheck === "string")
|
|
74
|
+
lines.push(` if (rest.length === 0 && !${keyCheck}(head)) return null`);
|
|
75
|
+
if (declared.length > 0) {
|
|
76
|
+
const walkers = new Map(children);
|
|
41
77
|
lines.push(` switch (head) {`);
|
|
42
|
-
for (const
|
|
43
|
-
|
|
78
|
+
for (const key of declared) {
|
|
79
|
+
const walker = walkers.get(key);
|
|
80
|
+
lines.push(` case ${JSON.stringify(key)}: return ${walker === void 0 ? "null" : `${walker}(rest)`}`);
|
|
81
|
+
}
|
|
44
82
|
lines.push(` }`);
|
|
45
83
|
}
|
|
46
|
-
|
|
47
|
-
if (indexed) {
|
|
84
|
+
if (indexBlock) {
|
|
48
85
|
lines.push(` const index = Number(head)`);
|
|
49
86
|
lines.push(` if (Number.isInteger(index) && index >= 0) {`);
|
|
50
87
|
tupleWalkers.forEach((walker, position) => {
|
|
@@ -55,21 +92,43 @@ const emitWalker = (schema, ctx) => {
|
|
|
55
92
|
if (itemsWalker !== null) {
|
|
56
93
|
lines.push(` if (index >= ${tuple.length}) return ${itemsWalker}(rest)`);
|
|
57
94
|
}
|
|
95
|
+
if (mapped && arrayKeywords)
|
|
96
|
+
lines.push(` return null`);
|
|
58
97
|
lines.push(` }`);
|
|
59
98
|
}
|
|
99
|
+
if (mapped && patterns.length > 0) {
|
|
100
|
+
const regexNames = patterns.map((pattern, index) => {
|
|
101
|
+
const regexName = `_repairPattern${id}_${index}`;
|
|
102
|
+
ctx.helpers.push(`const ${regexName} = new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(regexFlagsFor(pattern.source))})`);
|
|
103
|
+
return regexName;
|
|
104
|
+
});
|
|
105
|
+
const answer = (walker) => walker === null ? "null" : `${walker}(rest)`;
|
|
106
|
+
const only = patterns[0];
|
|
107
|
+
if (patterns.length === 1 && only !== void 0) {
|
|
108
|
+
lines.push(` if (${regexNames[0]}.test(head)) return ${answer(only.walker)}`);
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(` const claims = [${regexNames.join(", ")}].filter((pattern) => pattern.test(head))`);
|
|
111
|
+
lines.push(` if (claims.length > 1) return null`);
|
|
112
|
+
patterns.forEach((pattern, index) => {
|
|
113
|
+
lines.push(` if (claims[0] === ${regexNames[index]}) return ${answer(pattern.walker)}`);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
60
117
|
}
|
|
61
|
-
lines.push(` return null`, `}`);
|
|
118
|
+
lines.push(additionalWalker === null ? ` return null` : ` return ${additionalWalker}(rest)`, `}`);
|
|
62
119
|
ctx.helpers.push(lines.join("\n"));
|
|
63
120
|
return name;
|
|
64
121
|
};
|
|
65
|
-
const generateRepairFunction = (schema, typeName, typeSuffix = "") => {
|
|
122
|
+
const generateRepairFunction = (schema, typeName, typeSuffix = "", options = {}) => {
|
|
66
123
|
const helpers = [];
|
|
67
124
|
let counter = 0;
|
|
68
|
-
const
|
|
125
|
+
const matcher = createSubschemaMatcher(typeSuffix, options.rootSchema ?? (isSchemaObject(schema) ? schema : void 0), options.formats ?? NO_FORMATS, "_repair");
|
|
126
|
+
const ctx = { typeSuffix, helpers, next: () => counter++, matcher };
|
|
69
127
|
const walker = emitWalker(schema, ctx);
|
|
70
128
|
const lookupName = `repair${typeName}At`;
|
|
129
|
+
const declarations = [...matcher.declarations(helpers.join("\n")), ...helpers];
|
|
71
130
|
const parts = [
|
|
72
|
-
...
|
|
131
|
+
...declarations.length > 0 ? [declarations.join("\n\n"), ""] : [],
|
|
73
132
|
`export const ${lookupName}: RepairLookup = ${walker === null ? "() => null" : walker}`,
|
|
74
133
|
"",
|
|
75
134
|
`export const repair${typeName} = (input: unknown, _path = ''): RepairResult<${typeName}> => {`,
|
|
@@ -292,7 +292,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
|
|
|
292
292
|
lines.push(` }`);
|
|
293
293
|
}
|
|
294
294
|
if (hasConst(schema)) {
|
|
295
|
-
const mismatch = constMismatchCondition(raw, schema.const);
|
|
295
|
+
const mismatch = constMismatchCondition(typeTestAccessor(raw, ctx), schema.const);
|
|
296
296
|
const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
|
|
297
297
|
lines.push(` if (${presence}${mismatch}) {`);
|
|
298
298
|
lines.push(` ${pushError(ctx.sink, msg, path, "const", JSON.stringify({ allowedValue: schema.const }))}`);
|
|
@@ -300,7 +300,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
|
|
|
300
300
|
}
|
|
301
301
|
if (hasEnum(schema)) {
|
|
302
302
|
const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
303
|
-
lines.push(` if (${presence}!${enumMembershipExpr(schema.enum, raw)}) {`);
|
|
303
|
+
lines.push(` if (${presence}!${enumMembershipExpr(schema.enum, typeTestAccessor(raw, ctx))}) {`);
|
|
304
304
|
lines.push(` ${pushError(ctx.sink, JSON.stringify(`must be one of: ${label}`), path, "enum", JSON.stringify({ allowedValues: schema.enum }))}`);
|
|
305
305
|
lines.push(` }`);
|
|
306
306
|
}
|
|
@@ -609,7 +609,31 @@ const namedMatcher = (sub, suffix, ctx) => {
|
|
|
609
609
|
ctx.hoisted.push({ declaration, reference: `${name}(`, calls: true });
|
|
610
610
|
return name;
|
|
611
611
|
};
|
|
612
|
-
const
|
|
612
|
+
const namedBranchErrors = (sub, suffix, ctx) => {
|
|
613
|
+
if (!isSchemaObject(sub))
|
|
614
|
+
return null;
|
|
615
|
+
const key = `explain:${JSON.stringify(sub)}`;
|
|
616
|
+
const existing = ctx.namedMatchers.get(key);
|
|
617
|
+
if (existing !== void 0)
|
|
618
|
+
return existing;
|
|
619
|
+
const fnCtx = { ...ctx, objVar: "obj", pathPrefix: "${_path}", depth: 0, sink: MATCH_ERROR_SINK };
|
|
620
|
+
const checks = generateValueChecks("", "input", "`${_path}`", sub, suffix, fnCtx, true);
|
|
621
|
+
if (checks.length === 0)
|
|
622
|
+
return null;
|
|
623
|
+
const body = checks.join("\n");
|
|
624
|
+
const name = `${hoistPrefix(ctx)}_explain${ctx.namedMatchers.size}`;
|
|
625
|
+
const declaration = [
|
|
626
|
+
`const ${name} = (${readsBinding("input", body) ? "input" : "_input"}: unknown, _path: string): ValidationError[] => {`,
|
|
627
|
+
` const ${MATCH_ERROR_SINK}: ValidationError[] = []`,
|
|
628
|
+
body,
|
|
629
|
+
` return ${MATCH_ERROR_SINK}`,
|
|
630
|
+
`}`
|
|
631
|
+
].join("\n");
|
|
632
|
+
ctx.namedMatchers.set(key, name);
|
|
633
|
+
ctx.hoisted.push({ declaration, reference: `${name}(`, calls: true });
|
|
634
|
+
return name;
|
|
635
|
+
};
|
|
636
|
+
const generateMatchesExpr = (raw, sub, suffix, ctx, required = false) => {
|
|
613
637
|
if (sub === true)
|
|
614
638
|
return "true";
|
|
615
639
|
if (sub === false)
|
|
@@ -617,31 +641,26 @@ const generateMatchesExpr = (raw, sub, suffix, ctx, required = false, branch) =>
|
|
|
617
641
|
if (!isSchemaObject(sub))
|
|
618
642
|
return "true";
|
|
619
643
|
const value = NARROWABLE_REFERENCE.test(raw) ? raw : `_mv${ctx.depth}`;
|
|
620
|
-
const valuePath =
|
|
644
|
+
const valuePath = "`${_path}`";
|
|
621
645
|
const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
|
|
622
646
|
`;
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return `((): boolean => {
|
|
647
|
+
const named = namedMatcher(sub, suffix, ctx);
|
|
648
|
+
if (named !== null)
|
|
649
|
+
return named === "true" ? "true" : `${named}(${raw})`;
|
|
650
|
+
const matchState = { on: ctx.failFast.on, blocked: false };
|
|
651
|
+
const matchCtx = { ...ctx, sink: MATCH_BOOL_SINK, failFast: matchState, branchErrors: false };
|
|
652
|
+
const boolChecks = generateValueChecks("", value, valuePath, sub, suffix, matchCtx, required);
|
|
653
|
+
if (boolChecks.length === 0)
|
|
654
|
+
return "true";
|
|
655
|
+
if (!matchState.blocked)
|
|
656
|
+
return `((): boolean => {
|
|
634
657
|
${binding}${boolChecks.join("\n")}
|
|
635
658
|
return true })()`;
|
|
636
|
-
}
|
|
637
659
|
const checks = generateValueChecks("", value, valuePath, sub, suffix, { ...ctx, sink: MATCH_ERROR_SINK }, required);
|
|
638
660
|
if (checks.length === 0)
|
|
639
661
|
return "true";
|
|
640
|
-
const body = checks.join("\n");
|
|
641
|
-
const collect = branch === void 0 ? "" : `
|
|
642
|
-
if (_m.length !== 0) (${branch.buffer} ??= []).push(_m)`;
|
|
643
662
|
return `((): boolean => { const _m: ValidationError[] = []
|
|
644
|
-
${binding}${
|
|
663
|
+
${binding}${checks.join("\n")}
|
|
645
664
|
return _m.length === 0 })()`;
|
|
646
665
|
};
|
|
647
666
|
const unevaluatedMatcher = (suffix, ctx) => (accessor, schema, depth) => (
|
|
@@ -691,17 +710,18 @@ const generateUnevaluatedChecks = (raw, path, schema, suffix, ctx) => {
|
|
|
691
710
|
}
|
|
692
711
|
return lines;
|
|
693
712
|
};
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
713
|
+
const branchErrorsReport = (guard, raw, explain, message, keyword, path, ctx) => [
|
|
714
|
+
` if (${guard}) {`,
|
|
715
|
+
` ${pushError(ctx.sink, message, path, keyword)}`,
|
|
716
|
+
...explain.length === 0 ? [] : [
|
|
717
|
+
` const _bp = ${path}`,
|
|
718
|
+
` const _br: ValidationError[][] = []`,
|
|
719
|
+
` for (const _b of [${explain.map((name) => `${name}(${raw}, _bp)`).join(", ")}]) if (_b.length !== 0) _br.push(_b)`,
|
|
720
|
+
` for (const _e of selectBranchErrors(_br, _bp)) ${ctx.sink}.push(_e)`
|
|
721
|
+
],
|
|
703
722
|
` }`
|
|
704
723
|
];
|
|
724
|
+
const branchExplainers = (branches, suffix, ctx) => branches.map((branch) => namedBranchErrors(branch, suffix, ctx)).filter((name) => name !== null);
|
|
705
725
|
const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
706
726
|
if (!isSchemaObject(schema))
|
|
707
727
|
return [];
|
|
@@ -711,11 +731,10 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
|
711
731
|
lines.push(...generateValueChecks(key, raw, path, branch, suffix, ctx, true));
|
|
712
732
|
}
|
|
713
733
|
if (hasAnyOf(schema) && schema.anyOf.length > 0) {
|
|
714
|
-
const
|
|
715
|
-
const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx, true, collector));
|
|
734
|
+
const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx, true));
|
|
716
735
|
if (!conds.includes("true")) {
|
|
717
736
|
if (ctx.branchErrors) {
|
|
718
|
-
lines.push(...
|
|
737
|
+
lines.push(...branchErrorsReport(`!(${conds.join(" || ")})`, raw, branchExplainers(schema.anyOf, suffix, ctx), `'must match a schema in anyOf'`, "anyOf", path, ctx));
|
|
719
738
|
} else {
|
|
720
739
|
const condition = `!(${conds.join(" || ")})`;
|
|
721
740
|
if (LITERAL_CONDITION.test(condition))
|
|
@@ -727,20 +746,18 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
|
727
746
|
}
|
|
728
747
|
}
|
|
729
748
|
if (hasOneOf(schema) && schema.oneOf.length > 0) {
|
|
730
|
-
const
|
|
731
|
-
const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx, true, collector)} ? 1 : 0)`);
|
|
749
|
+
const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx, true)} ? 1 : 0)`);
|
|
732
750
|
const message = `'must match exactly one schema in oneOf'`;
|
|
733
751
|
if (!ctx.branchErrors) {
|
|
734
752
|
lines.push(` if ((${conds.join(" + ")}) !== 1) {`);
|
|
735
753
|
lines.push(` ${pushError(ctx.sink, message, path, "oneOf")}`);
|
|
736
754
|
lines.push(` }`);
|
|
737
755
|
} else {
|
|
756
|
+
const explain = branchExplainers(schema.oneOf, suffix, ctx);
|
|
738
757
|
lines.push(` {`);
|
|
739
|
-
lines.push(` let ${BRANCH_BUFFER}: ValidationError[][] | null = null`);
|
|
740
758
|
lines.push(` const _n = ${conds.join(" + ")}`);
|
|
741
|
-
|
|
742
|
-
lines.push(
|
|
743
|
-
lines.push(` ${selectBranchErrorsLine(path, ctx)}`);
|
|
759
|
+
const report = branchErrorsReport("_n === 0", raw, explain, message, "oneOf", path, ctx).map((line) => ` ${line}`);
|
|
760
|
+
lines.push(...report.slice(0, -1));
|
|
744
761
|
lines.push(` } else if (_n !== 1) {`);
|
|
745
762
|
lines.push(` ${pushError(ctx.sink, message, path, "oneOf")}`);
|
|
746
763
|
lines.push(` }`);
|
package/package.json
CHANGED